From bd60fd754633890cb7e6ee404d26b59d14280028 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 23 Jan 2025 10:01:11 -0500 Subject: [PATCH 001/384] Added initial Behavior API Allows for arbitrary behaviors to be attached to Actors in a way that doesn't require the use of Inventory tokens. This list can't be freely modified nor can it have duplicates meaning it has far better deterministic behavior than using Inventory items for this purpose. --- src/namedef_custom.h | 1 + src/playsim/actor.h | 13 ++ src/playsim/p_mobj.cpp | 252 +++++++++++++++++++++++++- src/serializer_doom.cpp | 27 +++ src/serializer_doom.h | 1 + wadsrc/static/zscript/actors/actor.zs | 15 ++ 6 files changed, 308 insertions(+), 1 deletion(-) diff --git a/src/namedef_custom.h b/src/namedef_custom.h index dbf94dbaf..af61b6499 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -452,6 +452,7 @@ xx(Readthismenu) xx(Playermenu) // more stuff +xx(Behavior) xx(ColorSet) xx(NeverSwitchOnPickup) xx(MoveBob) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 2f461e108..f5bfefb5a 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -793,6 +793,7 @@ public: virtual void OnDestroy() override; virtual void Serialize(FSerializer &arc) override; + virtual size_t PropagateMark() override; virtual void PostSerialize() override; virtual void PostBeginPlay() override; // Called immediately before the actor's first tick virtual void Tick() override; @@ -1373,6 +1374,7 @@ public: // landing speed from a jump with normal gravity (squats the player's view) // (note: this is put into AActor instead of the PlayerPawn because non-players also use the value) double LandingSpeed; + TMap Behaviors; // ThingIDs @@ -1434,6 +1436,17 @@ public: return GetClass()->FindState(numnames, names, exact); } + DObject* FindBehavior(const PClass& type) const + { + auto b = Behaviors.CheckKey(type.TypeName); + return b != nullptr && *b != nullptr && !((*b)->ObjectFlags & OF_EuthanizeMe) ? *b : nullptr; + } + DObject* AddBehavior(const PClass& type); + bool RemoveBehavior(const PClass& type); + void TickBehaviors(); + void MoveBehaviors(AActor& from); + void ClearBehaviors(); + bool HasSpecialDeathStates () const; double X() const diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 970011850..743843852 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -197,6 +197,22 @@ void AActor::EnableNetworking(const bool enable) Super::EnableNetworking(true); } +//========================================================================== +// +// AActor :: PropagateMark +// +//========================================================================== + +size_t AActor::PropagateMark() +{ + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + GC::Mark(pair->Value); + + return Super::PropagateMark(); +} + //========================================================================== // // AActor :: Serialize @@ -402,7 +418,8 @@ void AActor::Serialize(FSerializer &arc) ("morphflags", MorphFlags) ("premorphproperties", PremorphProperties) ("morphexitflash", MorphExitFlash) - ("damagesource", damagesource); + ("damagesource", damagesource) + ("behaviors", Behaviors); SerializeTerrain(arc, "floorterrain", floorterrain, &def->floorterrain); @@ -444,6 +461,229 @@ void AActor::PostSerialize() UpdateWaterLevel(false); } +//========================================================================== +// +// Behaviors allow for actions to be defined on Actors not coupled to +// specific inventory tokens. Only one can be attached at a time. +// +//========================================================================== + +bool AActor::RemoveBehavior(const PClass& type) +{ + if (Behaviors.CheckKey(type.TypeName)) + { + auto b = Behaviors[type.TypeName]; + + Behaviors.Remove(type.TypeName); + if (b != nullptr && !(b->ObjectFlags & OF_EuthanizeMe)) + { + b->Destroy(); + return true; + } + } + + return false; +} + +static int RemoveBehavior(AActor* self, PClass* type) +{ + return self->RemoveBehavior(*type); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, RemoveBehavior, RemoveBehavior) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_CLASS_NOT_NULL(type, DObject); + ACTION_RETURN_BOOL(self->RemoveBehavior(*type)); +} + +DObject* AActor::AddBehavior(const PClass& type) +{ + if (type.bAbstract || !type.IsDescendantOf(NAME_Behavior)) + return nullptr; + + auto b = FindBehavior(type); + if (b == nullptr) + { + b = Create(); + if (b == nullptr) + return nullptr; + + auto& owner = b->PointerVar(NAME_Owner); + owner = this; + + Behaviors[type.TypeName] = b; + IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Initialize) + { + VMValue params[] = { b }; + VMCall(func, params, 1, nullptr, 0); + + if (b->ObjectFlags & OF_EuthanizeMe) + { + RemoveBehavior(type); + return nullptr; + } + } + } + else + { + IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Readded) + { + VMValue params[] = { b }; + VMCall(func, params, 1, nullptr, 0); + + if (b->ObjectFlags & OF_EuthanizeMe) + { + RemoveBehavior(type); + return nullptr; + } + } + } + + return b; +} + +static DObject* AddBehavior(AActor* self, PClass* type) +{ + return self->AddBehavior(*type); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, AddBehavior, AddBehavior) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_CLASS_NOT_NULL(type, DObject); + ACTION_RETURN_OBJECT(self->AddBehavior(*type)); +} + +void AActor::TickBehaviors() +{ + TArray toRemove = {}; + + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + { + auto b = pair->Value; + if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + { + toRemove.Push(pair->Key); + continue; + } + + auto& owner = b->PointerVar(NAME_Owner); + if (owner != this) + { + toRemove.Push(pair->Key); + continue; + } + + IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Tick) + { + VMValue params[] = { b }; + VMCall(func, params, 1, nullptr, 0); + + if (b->ObjectFlags & OF_EuthanizeMe) + toRemove.Push(pair->Key); + } + } + + for (auto& type : toRemove) + RemoveBehavior(*PClass::FindClass(type)); +} + +static void TickBehaviors(AActor* self) +{ + self->TickBehaviors(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, TickBehaviors, TickBehaviors) +{ + PARAM_SELF_PROLOGUE(AActor); + self->TickBehaviors(); + return 0; +} + +static DObject* FindBehavior(AActor* self, PClass* type) +{ + return self->FindBehavior(*type); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, FindBehavior, FindBehavior) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_CLASS_NOT_NULL(type, DObject); + ACTION_RETURN_OBJECT(self->FindBehavior(*type)); +} + +void AActor::MoveBehaviors(AActor& from) +{ + // Clean these up properly before transferring. + ClearBehaviors(); + + Behaviors.TransferFrom(from.Behaviors); + + TArray toRemove = {}; + + // Clean up any empty behaviors that remained as well while + // changing the owner. + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + { + auto b = pair->Value; + if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + { + toRemove.Push(pair->Key); + continue; + } + + auto owner = b->PointerVar(NAME_Owner); + owner = this; + } + + for (auto& type : toRemove) + RemoveBehavior(*PClass::FindClass(type)); +} + +static void MoveBehaviors(AActor* self, AActor* from) +{ + self->MoveBehaviors(*from); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, MoveBehaviors, MoveBehaviors) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_OBJECT_NOT_NULL(from, AActor); + self->MoveBehaviors(*from); + return 0; +} + +void AActor::ClearBehaviors() +{ + TArray toRemove = {}; + + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + toRemove.Push(pair->Key); + + for (auto& type : toRemove) + RemoveBehavior(*PClass::FindClass(type)); + + Behaviors.Clear(); +} + +static void ClearBehaviors(AActor* self) +{ + self->ClearBehaviors(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, ClearBehaviors, ClearBehaviors) +{ + PARAM_SELF_PROLOGUE(AActor); + self->ClearBehaviors(); + return 0; +} //========================================================================== // @@ -3931,6 +4171,11 @@ void AActor::Tick () return; } + // These should always tick regardless of prediction or not (let the behavior itself + // handle this). + if (!isFrozen()) + TickBehaviors(); + if (flags5 & MF5_NOINTERACTION) { // only do the minimally necessary things here to save time: @@ -5251,6 +5496,9 @@ void AActor::OnDestroy () Level->localEventManager->WorldThingDestroyed(this); } + + ClearBehaviors(); + DeleteAttachedLights(); ClearRenderSectorList(); ClearRenderLineList(); @@ -5497,6 +5745,8 @@ int MorphPointerSubstitution(AActor* from, AActor* to) VMCall(func, params, 2, nullptr, 0); } + to->MoveBehaviors(*from); + // Go through player infos. for (int i = 0; i < MAXPLAYERS; ++i) { diff --git a/src/serializer_doom.cpp b/src/serializer_doom.cpp index 2ec1c8b86..a46d8c3f3 100644 --- a/src/serializer_doom.cpp +++ b/src/serializer_doom.cpp @@ -226,6 +226,33 @@ FSerializer& FDoomSerializer::StatePointer(const char* key, void* ptraddr, bool } +FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def) +{ + if (!arc.BeginObject(key)) + return arc; + + if (arc.isWriting()) + { + TMap::Iterator it = { value }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + arc(pair->Key.GetChars(), pair->Value); + } + else + { + const char* k = nullptr; + while ((k = arc.GetKey()) != nullptr) + { + DObject* obj = nullptr; + arc(k, obj); + value[k] = obj; + } + } + + arc.EndObject(); + return arc; +} + template<> FSerializer &Serialize(FSerializer &ar, const char *key, FPolyObj *&value, FPolyObj **defval) { diff --git a/src/serializer_doom.h b/src/serializer_doom.h index 40b6710a1..ec1a54fb8 100644 --- a/src/serializer_doom.h +++ b/src/serializer_doom.h @@ -43,6 +43,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &sid, ticcmd_ FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, FInterpolator &rs, FInterpolator *def); FSerializer& Serialize(FSerializer& arc, const char* key, struct FStandaloneAnimation& value, struct FStandaloneAnimation* defval); +FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def); template<> FSerializer &Serialize(FSerializer &arc, const char *key, FPolyObj *&value, FPolyObj **defval); template<> FSerializer &Serialize(FSerializer &arc, const char *key, sector_t *&value, sector_t **defval); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index a809e674d..7861258fe 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -73,6 +73,14 @@ class ViewPosition native native readonly int Flags; } +class Behavior play abstract +{ + readonly Actor Owner; + + virtual void Initialize() {} + virtual void Readded() {} + virtual void Tick() {} +} class Actor : Thinker native { @@ -500,6 +508,13 @@ class Actor : Thinker native return sin(fb * (180./32)) * 8; } + native clearscope Behavior FindBehavior(class type) const; + native bool RemoveBehavior(class type); + native Behavior AddBehavior(class type); + native void TickBehaviors(); + native void ClearBehaviors(); + native void MoveBehaviors(Actor from); + native clearscope bool isFrozen() const; virtual native void BeginPlay(); virtual native void Activate(Actor activator); From f301e6cc0ffd82dadcd2e72813d45d6691a34b7d Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 23 Jan 2025 10:21:26 -0500 Subject: [PATCH 002/384] Improve Behavior Tick() behavior Collect all objects first to account for the map being modified mid-iteration. --- src/playsim/actor.h | 2 +- src/playsim/p_mobj.cpp | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index f5bfefb5a..ffe48fdfd 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -1441,7 +1441,7 @@ public: auto b = Behaviors.CheckKey(type.TypeName); return b != nullptr && *b != nullptr && !((*b)->ObjectFlags & OF_EuthanizeMe) ? *b : nullptr; } - DObject* AddBehavior(const PClass& type); + DObject* AddBehavior(PClass& type); bool RemoveBehavior(const PClass& type); void TickBehaviors(); void MoveBehaviors(AActor& from); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 743843852..9fe615d0d 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -497,7 +497,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, RemoveBehavior, RemoveBehavior) ACTION_RETURN_BOOL(self->RemoveBehavior(*type)); } -DObject* AActor::AddBehavior(const PClass& type) +DObject* AActor::AddBehavior(PClass& type) { if (type.bAbstract || !type.IsDescendantOf(NAME_Behavior)) return nullptr; @@ -505,7 +505,7 @@ DObject* AActor::AddBehavior(const PClass& type) auto b = FindBehavior(type); if (b == nullptr) { - b = Create(); + b = type.CreateNew(); if (b == nullptr) return nullptr; @@ -558,6 +558,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, AddBehavior, AddBehavior) void AActor::TickBehaviors() { TArray toRemove = {}; + TArray toTick = {}; TMap::Iterator it = { Behaviors }; TMap::Pair* pair = nullptr; @@ -570,6 +571,11 @@ void AActor::TickBehaviors() continue; } + toTick.Push(b); + } + + for (auto& b : toTick) + { auto& owner = b->PointerVar(NAME_Owner); if (owner != this) { From 08de93b2fa682e78c13f482fbb79b47609440a4d Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 23 Jan 2025 10:27:44 -0500 Subject: [PATCH 003/384] Fixed missing ref in MoveBehaviors --- src/playsim/p_mobj.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 9fe615d0d..603d5958a 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -643,7 +643,7 @@ void AActor::MoveBehaviors(AActor& from) continue; } - auto owner = b->PointerVar(NAME_Owner); + auto& owner = b->PointerVar(NAME_Owner); owner = this; } From e27bf3816568d1a42926e3a67a520eace0834b7b Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 23 Jan 2025 18:45:50 -0500 Subject: [PATCH 004/384] Added behavior iterator for Actors Renamed Readded to Reinitialize. --- src/playsim/p_mobj.cpp | 2 +- src/scripting/vmiterators.cpp | 89 +++++++++++++++++++++++++++ wadsrc/static/zscript/actors/actor.zs | 10 ++- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 603d5958a..82f7a181e 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -527,7 +527,7 @@ DObject* AActor::AddBehavior(PClass& type) } else { - IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Readded) + IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Reinitialize) { VMValue params[] = { b }; VMCall(func, params, 1, nullptr, 0); diff --git a/src/scripting/vmiterators.cpp b/src/scripting/vmiterators.cpp index 32d1ca1f8..1b6eae509 100644 --- a/src/scripting/vmiterators.cpp +++ b/src/scripting/vmiterators.cpp @@ -388,6 +388,95 @@ DEFINE_ACTION_FUNCTION_NATIVE(DActorIterator, Reinit, ReinitActI) return 0; } +//=========================================================================== +// +// Behavior iterator. This can be created in two ways; +// -Across an Actor's behaviors +// -Across all behaviors of a given type +// +//=========================================================================== + +class DBehaviorIterator : public DObject +{ + DECLARE_ABSTRACT_CLASS(DBehaviorIterator, DObject) + size_t _index; + TArray _behaviors; + +public: + DBehaviorIterator(const AActor& mobj, FName type) + { + TMap::ConstIterator it = { mobj.Behaviors }; + TMap::ConstPair* pair = nullptr; + while (it.NextPair(pair)) + { + auto b = pair->Value; + if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + continue; + + if (type == NAME_None || b->IsKindOf(type)) + _behaviors.Push(b); + } + + Reinit(); + } + + // TODO: List for all behaviors + + DObject* Next() + { + if (_index >= _behaviors.Size()) + return nullptr; + + return _behaviors[_index++]; + } + + void Reinit() { _index = 0u; } + + void OnDestroy() override + { + _behaviors.Reset(); + Super::OnDestroy(); + } +}; + +IMPLEMENT_CLASS(DBehaviorIterator, true, false); + +static DBehaviorIterator* CreateBehaviorItFromActor(AActor* mobj, PClass* type) +{ + return Create(*mobj, type == nullptr ? NAME_None : type->TypeName); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, CreateFrom, CreateBehaviorItFromActor) +{ + PARAM_PROLOGUE; + PARAM_OBJECT_NOT_NULL(mobj, AActor); + PARAM_CLASS(type, DObject); + ACTION_RETURN_OBJECT(CreateBehaviorItFromActor(mobj, type)); +} + +static DObject* NextBehavior(DBehaviorIterator* self) +{ + return self->Next(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, Next, NextBehavior) +{ + PARAM_SELF_PROLOGUE(DBehaviorIterator); + ACTION_RETURN_OBJECT(self->Next()); +} + +static void ReinitBehavior(DBehaviorIterator* self) +{ + self->Reinit(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, Reinit, ReinitBehavior) +{ + PARAM_SELF_PROLOGUE(DBehaviorIterator); + self->Reinit(); + return 0; +} + DEFINE_FIELD_NAMED(DBlockLinesIterator, cres.line, curline); DEFINE_FIELD_NAMED(DBlockLinesIterator, cres.Position, position); DEFINE_FIELD_NAMED(DBlockLinesIterator, cres.portalflags, portalflags); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 7861258fe..ee20183e3 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -78,10 +78,18 @@ class Behavior play abstract readonly Actor Owner; virtual void Initialize() {} - virtual void Readded() {} + virtual void Reinitialize() {} virtual void Tick() {} } +class BehaviorIterator native abstract final +{ + native static BehaviorIterator CreateFrom(Actor mobj, class type = null); + + native Behavior Next(); + native void Reinit(); +} + class Actor : Thinker native { const DEFAULT_HEALTH = 1000; From 14f7a10ae692817174c547a59472dad7c192fb4d Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 24 Jan 2025 12:15:48 -0500 Subject: [PATCH 005/384] Added global iterator for behaviors Imported Behaviors to the engine to allow them to properly clean up their level list. Restrict Behaviors from being new'd in ZScript as they need an owner to function. --- src/g_levellocals.h | 24 +++++++++ src/p_saveg.cpp | 3 +- src/playsim/actor.h | 18 +++++-- src/playsim/p_mobj.cpp | 71 +++++++++++++++++--------- src/scripting/backend/codegen_doom.cpp | 8 ++- src/scripting/vmiterators.cpp | 44 ++++++++++++---- src/serializer_doom.cpp | 8 +-- src/serializer_doom.h | 3 +- wadsrc/static/zscript/actors/actor.zs | 6 ++- 9 files changed, 140 insertions(+), 45 deletions(-) diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 2002e35b5..da3bc58c2 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -706,6 +706,7 @@ public: DVisualThinker* VisualThinkerHead = nullptr; // links to global game objects + TArray ActorBehaviors; TArray> CorpseQueue; TObjPtr FraggleScriptThinker = MakeObjPtr(nullptr); TObjPtr ACSThinker = MakeObjPtr(nullptr); @@ -717,6 +718,29 @@ public: // //========================================================================== + void AddActorBehavior(DBehavior& b) + { + if (b.Level == nullptr) + { + b.Level = this; + ActorBehaviors.Push(&b); + } + } + + void RemoveActorBehavior(DBehavior& b) + { + if (b.Level == this) + { + b.Level = nullptr; + ActorBehaviors.Delete(ActorBehaviors.Find(&b)); + } + } + + //========================================================================== + // + // + //========================================================================== + bool IsJumpingAllowed() const { if (dmflags & DF_NO_JUMP) diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index ce26ff39c..d759add72 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -991,7 +991,8 @@ void FLevelLocals::Serialize(FSerializer &arc, bool hubload) ("automap", automap) ("interpolator", interpolator) ("frozenstate", frozenstate) - ("visualthinkerhead", VisualThinkerHead); + ("visualthinkerhead", VisualThinkerHead) + ("actorbehaviors", ActorBehaviors); // Hub transitions must keep the current total time diff --git a/src/playsim/actor.h b/src/playsim/actor.h index ffe48fdfd..b85205373 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -778,6 +778,18 @@ public: void Serialize(FSerializer& arc) override; }; +class DBehavior : public DObject +{ + DECLARE_CLASS(DBehavior, DObject) + HAS_OBJECT_POINTERS +public: + TObjPtr Owner; + FLevelLocals* Level; + + void Serialize(FSerializer& arc) override; + void OnDestroy() override; +}; + const double MinVel = EQUAL_EPSILON; // Map Object definition. @@ -1374,7 +1386,7 @@ public: // landing speed from a jump with normal gravity (squats the player's view) // (note: this is put into AActor instead of the PlayerPawn because non-players also use the value) double LandingSpeed; - TMap Behaviors; + TMap Behaviors; // ThingIDs @@ -1436,12 +1448,12 @@ public: return GetClass()->FindState(numnames, names, exact); } - DObject* FindBehavior(const PClass& type) const + DBehavior* FindBehavior(const PClass& type) const { auto b = Behaviors.CheckKey(type.TypeName); return b != nullptr && *b != nullptr && !((*b)->ObjectFlags & OF_EuthanizeMe) ? *b : nullptr; } - DObject* AddBehavior(PClass& type); + DBehavior* AddBehavior(PClass& type); bool RemoveBehavior(const PClass& type); void TickBehaviors(); void MoveBehaviors(AActor& from); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 82f7a181e..93480d6fa 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -180,6 +180,14 @@ IMPLEMENT_POINTERS_START(AActor) IMPLEMENT_POINTER(modelData) IMPLEMENT_POINTERS_END +IMPLEMENT_CLASS(DBehavior, false, true) +IMPLEMENT_POINTERS_START(DBehavior) + IMPLEMENT_POINTER(Owner) +IMPLEMENT_POINTERS_END + +DEFINE_FIELD(DBehavior, Owner) +DEFINE_FIELD(DBehavior, Level) + //========================================================================== // // Make sure Actors can never have their networking disabled. @@ -205,8 +213,8 @@ void AActor::EnableNetworking(const bool enable) size_t AActor::PropagateMark() { - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; while (it.NextPair(pair)) GC::Mark(pair->Value); @@ -468,6 +476,21 @@ void AActor::PostSerialize() // //========================================================================== +void DBehavior::Serialize(FSerializer& arc) +{ + Super::Serialize(arc); + arc("owner", Owner) + ("level", Level); +} + +void DBehavior::OnDestroy() +{ + if (Level != nullptr) + Level->RemoveActorBehavior(*this); + + Super::OnDestroy(); +} + bool AActor::RemoveBehavior(const PClass& type) { if (Behaviors.CheckKey(type.TypeName)) @@ -493,11 +516,11 @@ static int RemoveBehavior(AActor* self, PClass* type) DEFINE_ACTION_FUNCTION_NATIVE(AActor, RemoveBehavior, RemoveBehavior) { PARAM_SELF_PROLOGUE(AActor); - PARAM_CLASS_NOT_NULL(type, DObject); + PARAM_CLASS_NOT_NULL(type, DBehavior); ACTION_RETURN_BOOL(self->RemoveBehavior(*type)); } -DObject* AActor::AddBehavior(PClass& type) +DBehavior* AActor::AddBehavior(PClass& type) { if (type.bAbstract || !type.IsDescendantOf(NAME_Behavior)) return nullptr; @@ -505,14 +528,13 @@ DObject* AActor::AddBehavior(PClass& type) auto b = FindBehavior(type); if (b == nullptr) { - b = type.CreateNew(); + b = dyn_cast(type.CreateNew()); if (b == nullptr) return nullptr; - auto& owner = b->PointerVar(NAME_Owner); - owner = this; - + b->Owner = this; Behaviors[type.TypeName] = b; + Level->AddActorBehavior(*b); IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Initialize) { VMValue params[] = { b }; @@ -543,7 +565,7 @@ DObject* AActor::AddBehavior(PClass& type) return b; } -static DObject* AddBehavior(AActor* self, PClass* type) +static DBehavior* AddBehavior(AActor* self, PClass* type) { return self->AddBehavior(*type); } @@ -551,17 +573,17 @@ static DObject* AddBehavior(AActor* self, PClass* type) DEFINE_ACTION_FUNCTION_NATIVE(AActor, AddBehavior, AddBehavior) { PARAM_SELF_PROLOGUE(AActor); - PARAM_CLASS_NOT_NULL(type, DObject); + PARAM_CLASS_NOT_NULL(type, DBehavior); ACTION_RETURN_OBJECT(self->AddBehavior(*type)); } void AActor::TickBehaviors() { TArray toRemove = {}; - TArray toTick = {}; + TArray toTick = {}; - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; while (it.NextPair(pair)) { auto b = pair->Value; @@ -576,8 +598,7 @@ void AActor::TickBehaviors() for (auto& b : toTick) { - auto& owner = b->PointerVar(NAME_Owner); - if (owner != this) + if (b->Owner != this) { toRemove.Push(pair->Key); continue; @@ -609,7 +630,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, TickBehaviors, TickBehaviors) return 0; } -static DObject* FindBehavior(AActor* self, PClass* type) +static DBehavior* FindBehavior(AActor* self, PClass* type) { return self->FindBehavior(*type); } @@ -617,7 +638,7 @@ static DObject* FindBehavior(AActor* self, PClass* type) DEFINE_ACTION_FUNCTION_NATIVE(AActor, FindBehavior, FindBehavior) { PARAM_SELF_PROLOGUE(AActor); - PARAM_CLASS_NOT_NULL(type, DObject); + PARAM_CLASS_NOT_NULL(type, DBehavior); ACTION_RETURN_OBJECT(self->FindBehavior(*type)); } @@ -632,8 +653,8 @@ void AActor::MoveBehaviors(AActor& from) // Clean up any empty behaviors that remained as well while // changing the owner. - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; while (it.NextPair(pair)) { auto b = pair->Value; @@ -643,8 +664,12 @@ void AActor::MoveBehaviors(AActor& from) continue; } - auto& owner = b->PointerVar(NAME_Owner); - owner = this; + b->Owner = this; + if (b->Level != b->Owner->Level) + { + b->Level->RemoveActorBehavior(*b); + b->Owner->Level->AddActorBehavior(*b); + } } for (auto& type : toRemove) @@ -668,8 +693,8 @@ void AActor::ClearBehaviors() { TArray toRemove = {}; - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; while (it.NextPair(pair)) toRemove.Push(pair->Key); diff --git a/src/scripting/backend/codegen_doom.cpp b/src/scripting/backend/codegen_doom.cpp index 0e52790d7..9feab0f9c 100644 --- a/src/scripting/backend/codegen_doom.cpp +++ b/src/scripting/backend/codegen_doom.cpp @@ -936,7 +936,7 @@ static DObject *BuiltinNewDoom(PClass *cls, int outerside, int backwardscompatib // Creating actors here must be outright prohibited, if (cls->IsDescendantOf(NAME_Actor)) { - ThrowAbortException(X_OTHER, "Cannot create actors with 'new'"); + ThrowAbortException(X_OTHER, "Cannot create actors with 'new'. Use Actor.Spawn instead."); return nullptr; } if (cls->IsDescendantOf(NAME_VisualThinker)) // Same for VisualThinkers. @@ -944,6 +944,12 @@ static DObject *BuiltinNewDoom(PClass *cls, int outerside, int backwardscompatib ThrowAbortException(X_OTHER, "Cannot create VisualThinker or inheriting classes with 'new'. Use 'VisualThinker.Spawn' instead."); return nullptr; } + // These don't make sense without an owning Actor so don't allow creating them. + if (cls->IsDescendantOf(NAME_Behavior)) + { + ThrowAbortException(X_OTHER, "Behaviors must be added to existing Actors and cannot be created with 'new'"); + return nullptr; + } if ((vm_warnthinkercreation || !backwardscompatible) && cls->IsDescendantOf(NAME_Thinker)) { // This must output a diagnostic warning diff --git a/src/scripting/vmiterators.cpp b/src/scripting/vmiterators.cpp index 1b6eae509..f6a9c30b7 100644 --- a/src/scripting/vmiterators.cpp +++ b/src/scripting/vmiterators.cpp @@ -400,29 +400,41 @@ class DBehaviorIterator : public DObject { DECLARE_ABSTRACT_CLASS(DBehaviorIterator, DObject) size_t _index; - TArray _behaviors; + TArray _behaviors; public: - DBehaviorIterator(const AActor& mobj, FName type) + DBehaviorIterator(const AActor& mobj, PClass* type) { - TMap::ConstIterator it = { mobj.Behaviors }; - TMap::ConstPair* pair = nullptr; + TMap::ConstIterator it = { mobj.Behaviors }; + TMap::ConstPair* pair = nullptr; while (it.NextPair(pair)) { auto b = pair->Value; if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) continue; - if (type == NAME_None || b->IsKindOf(type)) + if (type == nullptr || b->IsKindOf(type)) _behaviors.Push(b); } Reinit(); } - // TODO: List for all behaviors + DBehaviorIterator(const FLevelLocals& level, PClass* type) + { + for (auto& b : level.ActorBehaviors) + { + if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + continue; - DObject* Next() + if (type == nullptr || b->IsKindOf(type)) + _behaviors.Push(b); + } + + Reinit(); + } + + DBehavior* Next() { if (_index >= _behaviors.Size()) return nullptr; @@ -443,18 +455,30 @@ IMPLEMENT_CLASS(DBehaviorIterator, true, false); static DBehaviorIterator* CreateBehaviorItFromActor(AActor* mobj, PClass* type) { - return Create(*mobj, type == nullptr ? NAME_None : type->TypeName); + return Create(*mobj, type); } DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, CreateFrom, CreateBehaviorItFromActor) { PARAM_PROLOGUE; PARAM_OBJECT_NOT_NULL(mobj, AActor); - PARAM_CLASS(type, DObject); + PARAM_CLASS(type, DBehavior); ACTION_RETURN_OBJECT(CreateBehaviorItFromActor(mobj, type)); } -static DObject* NextBehavior(DBehaviorIterator* self) +static DBehaviorIterator* CreateBehaviorIt(PClass* type) +{ + return Create(*primaryLevel, type); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, Create, CreateBehaviorIt) +{ + PARAM_PROLOGUE; + PARAM_CLASS(type, DBehavior); + ACTION_RETURN_OBJECT(CreateBehaviorIt(type)); +} + +static DBehavior* NextBehavior(DBehaviorIterator* self) { return self->Next(); } diff --git a/src/serializer_doom.cpp b/src/serializer_doom.cpp index a46d8c3f3..566aa3164 100644 --- a/src/serializer_doom.cpp +++ b/src/serializer_doom.cpp @@ -226,15 +226,15 @@ FSerializer& FDoomSerializer::StatePointer(const char* key, void* ptraddr, bool } -FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def) +FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def) { if (!arc.BeginObject(key)) return arc; if (arc.isWriting()) { - TMap::Iterator it = { value }; - TMap::Pair* pair = nullptr; + TMap::Iterator it = { value }; + TMap::Pair* pair = nullptr; while (it.NextPair(pair)) arc(pair->Key.GetChars(), pair->Value); } @@ -243,7 +243,7 @@ FSerializer& Serialize(FSerializer& arc, const char* key, TMap& const char* k = nullptr; while ((k = arc.GetKey()) != nullptr) { - DObject* obj = nullptr; + DBehavior* obj = nullptr; arc(k, obj); value[k] = obj; } diff --git a/src/serializer_doom.h b/src/serializer_doom.h index ec1a54fb8..4abc9fb19 100644 --- a/src/serializer_doom.h +++ b/src/serializer_doom.h @@ -3,6 +3,7 @@ #include "serializer.h" class player_t; +class DBehavior; struct sector_t; struct line_t; struct side_t; @@ -43,7 +44,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &sid, ticcmd_ FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, FInterpolator &rs, FInterpolator *def); FSerializer& Serialize(FSerializer& arc, const char* key, struct FStandaloneAnimation& value, struct FStandaloneAnimation* defval); -FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def); +FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def); template<> FSerializer &Serialize(FSerializer &arc, const char *key, FPolyObj *&value, FPolyObj **defval); template<> FSerializer &Serialize(FSerializer &arc, const char *key, sector_t *&value, sector_t **defval); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index ee20183e3..6f8577855 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -73,9 +73,10 @@ class ViewPosition native native readonly int Flags; } -class Behavior play abstract +class Behavior native play abstract { - readonly Actor Owner; + native readonly Actor Owner; + native readonly LevelLocals Level; virtual void Initialize() {} virtual void Reinitialize() {} @@ -85,6 +86,7 @@ class Behavior play abstract class BehaviorIterator native abstract final { native static BehaviorIterator CreateFrom(Actor mobj, class type = null); + native static BehaviorIterator Create(class type = null); native Behavior Next(); native void Reinit(); From 3d6506df5bcea2c5071b8e87c73c4d41fb38689e Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 24 Jan 2025 18:23:35 -0500 Subject: [PATCH 006/384] Added owner type parameter to iterator --- src/scripting/vmiterators.cpp | 12 ++++++++---- wadsrc/static/zscript/actors/actor.zs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/scripting/vmiterators.cpp b/src/scripting/vmiterators.cpp index f6a9c30b7..7762d176b 100644 --- a/src/scripting/vmiterators.cpp +++ b/src/scripting/vmiterators.cpp @@ -420,13 +420,16 @@ public: Reinit(); } - DBehaviorIterator(const FLevelLocals& level, PClass* type) + DBehaviorIterator(const FLevelLocals& level, PClass* type, PClass* ownerType) { for (auto& b : level.ActorBehaviors) { if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) continue; + if (ownerType != nullptr && !b->Owner->IsKindOf(ownerType)) + continue; + if (type == nullptr || b->IsKindOf(type)) _behaviors.Push(b); } @@ -466,16 +469,17 @@ DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, CreateFrom, CreateBehaviorItFro ACTION_RETURN_OBJECT(CreateBehaviorItFromActor(mobj, type)); } -static DBehaviorIterator* CreateBehaviorIt(PClass* type) +static DBehaviorIterator* CreateBehaviorIt(PClass* type, PClass* ownerType) { - return Create(*primaryLevel, type); + return Create(*primaryLevel, type, ownerType); } DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, Create, CreateBehaviorIt) { PARAM_PROLOGUE; PARAM_CLASS(type, DBehavior); - ACTION_RETURN_OBJECT(CreateBehaviorIt(type)); + PARAM_CLASS(ownerType, AActor); + ACTION_RETURN_OBJECT(CreateBehaviorIt(type, ownerType)); } static DBehavior* NextBehavior(DBehaviorIterator* self) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 6f8577855..5d24f34b0 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -86,7 +86,7 @@ class Behavior native play abstract class BehaviorIterator native abstract final { native static BehaviorIterator CreateFrom(Actor mobj, class type = null); - native static BehaviorIterator Create(class type = null); + native static BehaviorIterator Create(class type = null, class ownerType = null); native Behavior Next(); native void Reinit(); From ad874776502d1832b08e21c2d072cb2001d4cea5 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 25 Jan 2025 09:51:28 -0500 Subject: [PATCH 007/384] Make DBehavior final Specifics should be implemented in ZScript. --- src/playsim/actor.h | 2 +- src/playsim/p_mobj.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index b85205373..7d1bb8740 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -778,7 +778,7 @@ public: void Serialize(FSerializer& arc) override; }; -class DBehavior : public DObject +class DBehavior final : public DObject { DECLARE_CLASS(DBehavior, DObject) HAS_OBJECT_POINTERS diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 93480d6fa..6677e4fa4 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -665,10 +665,10 @@ void AActor::MoveBehaviors(AActor& from) } b->Owner = this; - if (b->Level != b->Owner->Level) + if (b->Level != Level) { b->Level->RemoveActorBehavior(*b); - b->Owner->Level->AddActorBehavior(*b); + Level->AddActorBehavior(*b); } } From 1e05bb2a553b5279723da38fc0f37ccbcd52309e Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 25 Jan 2025 10:52:54 -0500 Subject: [PATCH 008/384] Clean up behaviors properly when moving levels Also adds support for foreach loops with the behavior iterator. --- src/g_level.cpp | 4 +++ src/namedef_custom.h | 1 + src/p_setup.cpp | 1 + src/playsim/actor.h | 3 ++ src/playsim/p_mobj.cpp | 38 ++++++++++++++++++++++++++ src/scripting/backend/codegen_doom.cpp | 10 +++++-- 6 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/g_level.cpp b/src/g_level.cpp index a1759cdce..9fe3c9699 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1607,6 +1607,7 @@ void FLevelLocals::StartTravel () if (Players[i]->health > 0) { pawn->UnlinkFromWorld (nullptr); + pawn->UnlinkBehaviorsFromLevel(); int tid = pawn->tid; // Save TID pawn->SetTID(0); pawn->tid = tid; // Restore TID (but no longer linked into the hash chain) @@ -1617,6 +1618,7 @@ void FLevelLocals::StartTravel () { inv->ChangeStatNum (STAT_TRAVELLING); inv->UnlinkFromWorld (nullptr); + inv->UnlinkBehaviorsFromLevel(); inv->DeleteAttachedLights(); } } @@ -1720,6 +1722,7 @@ int FLevelLocals::FinishTravel () pawndup->Destroy(); } pawn->LinkToWorld (nullptr); + pawn->LinkBehaviorsToLevel(); pawn->ClearInterpolation(); pawn->ClearFOVInterpolation(); const int tid = pawn->tid; // Save TID (actor isn't linked into the hash chain yet) @@ -1733,6 +1736,7 @@ int FLevelLocals::FinishTravel () inv->ChangeStatNum (STAT_INVENTORY); inv->LinkToWorld (nullptr); P_FindFloorCeiling(inv, FFCF_ONLYSPAWNPOS); + inv->LinkBehaviorsToLevel(); IFVIRTUALPTRNAME(inv, NAME_Inventory, Travelled) { diff --git a/src/namedef_custom.h b/src/namedef_custom.h index af61b6499..c2925bb83 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -453,6 +453,7 @@ xx(Playermenu) // more stuff xx(Behavior) +xx(BehaviorIterator) xx(ColorSet) xx(NeverSwitchOnPickup) xx(MoveBob) diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 2775e259e..85b592bb3 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -380,6 +380,7 @@ void FLevelLocals::ClearLevelData(bool fullgc) aabbTree = nullptr; levelMesh = nullptr; VisualThinkerHead = nullptr; + ActorBehaviors.Clear(); if (screen) screen->SetAABBTree(nullptr); } diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 7d1bb8740..db069bc32 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -1458,6 +1458,9 @@ public: void TickBehaviors(); void MoveBehaviors(AActor& from); void ClearBehaviors(); + // Internal only, mostly for traveling. + void UnlinkBehaviorsFromLevel(); + void LinkBehaviorsToLevel(); bool HasSpecialDeathStates () const; diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 6677e4fa4..5c7979c7c 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -716,6 +716,44 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, ClearBehaviors, ClearBehaviors) return 0; } +void AActor::UnlinkBehaviorsFromLevel() +{ + TArray toRemove = {}; + + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + { + auto b = pair->Value; + if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + toRemove.Push(pair->Key); + else + b->Level->RemoveActorBehavior(*b); + } + + for (auto& type : toRemove) + RemoveBehavior(*PClass::FindClass(type)); +} + +void AActor::LinkBehaviorsToLevel() +{ + TArray toRemove = {}; + + TMap::Iterator it = { Behaviors }; + TMap::Pair* pair = nullptr; + while (it.NextPair(pair)) + { + auto b = pair->Value; + if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + toRemove.Push(pair->Key); + else + Level->AddActorBehavior(*b); + } + + for (auto& type : toRemove) + RemoveBehavior(*PClass::FindClass(type)); +} + //========================================================================== // // AActor::InStateSequence diff --git a/src/scripting/backend/codegen_doom.cpp b/src/scripting/backend/codegen_doom.cpp index 9feab0f9c..8ca5437e3 100644 --- a/src/scripting/backend/codegen_doom.cpp +++ b/src/scripting/backend/codegen_doom.cpp @@ -1034,6 +1034,10 @@ FxExpression *FxCastForEachLoop::Resolve(FCompileContext &ctx) { fieldName = "Thinker"; } + else if (itType->TypeName == NAME_BehaviorIterator) + { + fieldName = "Behavior"; + } else { ScriptPosition.Message(MSG_ERROR, "foreach(Type var : it ) - 'it' must be an actor or thinker iterator, but is a %s",Expr->ValueType->DescriptiveName()); @@ -1218,6 +1222,7 @@ bool IsGameSpecificForEachLoop(FxForEachLoop * loop) || ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_BlockThingsIterator || ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_ActorIterator || ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_ThinkerIterator + || ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_BehaviorIterator )); } @@ -1232,7 +1237,7 @@ FxExpression * ResolveGameSpecificForEachLoop(FxForEachLoop * loop) delete loop; return blockIt; } - else if(cname == NAME_ActorIterator || cname == NAME_ThinkerIterator) + else if(cname == NAME_ActorIterator || cname == NAME_ThinkerIterator || cname == NAME_BehaviorIterator) { auto castIt = new FxCastForEachLoop(NAME_None, loop->loopVarName, loop->Array, loop->Code, loop->ScriptPosition); loop->Array = loop->Code = nullptr; @@ -1324,13 +1329,14 @@ bool IsGameSpecificTypedForEachLoop(FxTypedForEachLoop * loop) return (vt->isObjectPointer() && ( ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_ActorIterator || ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_ThinkerIterator + || ((PObjectPointer*)vt)->PointedClass()->TypeName == NAME_BehaviorIterator )); } FxExpression * ResolveGameSpecificTypedForEachLoop(FxTypedForEachLoop * loop) { assert(loop->Expr->ValueType->isObjectPointer()); - assert(((PObjectPointer*)loop->Expr->ValueType)->PointedClass()->TypeName == NAME_ActorIterator || ((PObjectPointer*)loop->Expr->ValueType)->PointedClass()->TypeName == NAME_ThinkerIterator); + assert(((PObjectPointer*)loop->Expr->ValueType)->PointedClass()->TypeName == NAME_ActorIterator || ((PObjectPointer*)loop->Expr->ValueType)->PointedClass()->TypeName == NAME_ThinkerIterator || ((PObjectPointer*)loop->Expr->ValueType)->PointedClass()->TypeName == NAME_BehaviorIterator); FxExpression * castIt = new FxCastForEachLoop(loop->className, loop->varName, loop->Expr, loop->Code, loop->ScriptPosition); loop->Expr = loop->Code = nullptr; From 7efd3018101d90816e014860a37756736dbb88b2 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 28 Jan 2025 07:38:52 -0500 Subject: [PATCH 009/384] Clean up for Behavior API Use TObjPtr to improve null checking. Add option to clear only specific kinds of Behaviors. Fixed removal checks while ticking. Don't stop at now-null behaviors when iterating. --- src/playsim/actor.h | 12 +-- src/playsim/p_mobj.cpp | 108 ++++++++++++++------------ src/scripting/vmiterators.cpp | 27 +++---- src/serializer_doom.cpp | 12 ++- src/serializer_doom.h | 2 +- wadsrc/static/zscript/actors/actor.zs | 2 +- 6 files changed, 88 insertions(+), 75 deletions(-) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index db069bc32..cbc2781de 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -1386,7 +1386,7 @@ public: // landing speed from a jump with normal gravity (squats the player's view) // (note: this is put into AActor instead of the PlayerPawn because non-players also use the value) double LandingSpeed; - TMap Behaviors; + TMap> Behaviors; // ThingIDs @@ -1448,16 +1448,16 @@ public: return GetClass()->FindState(numnames, names, exact); } - DBehavior* FindBehavior(const PClass& type) const + DBehavior* FindBehavior(FName type) const { - auto b = Behaviors.CheckKey(type.TypeName); - return b != nullptr && *b != nullptr && !((*b)->ObjectFlags & OF_EuthanizeMe) ? *b : nullptr; + auto b = Behaviors.CheckKey(type); + return b != nullptr ? b->Get() : nullptr; } DBehavior* AddBehavior(PClass& type); - bool RemoveBehavior(const PClass& type); + bool RemoveBehavior(FName type); void TickBehaviors(); void MoveBehaviors(AActor& from); - void ClearBehaviors(); + void ClearBehaviors(PClass* type = nullptr); // Internal only, mostly for traveling. void UnlinkBehaviorsFromLevel(); void LinkBehaviorsToLevel(); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 5c7979c7c..c6ebe85e4 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -213,8 +213,8 @@ void AActor::EnableNetworking(const bool enable) size_t AActor::PropagateMark() { - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { Behaviors }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) GC::Mark(pair->Value); @@ -491,33 +491,34 @@ void DBehavior::OnDestroy() Super::OnDestroy(); } -bool AActor::RemoveBehavior(const PClass& type) +bool AActor::RemoveBehavior(FName type) { - if (Behaviors.CheckKey(type.TypeName)) + bool res = false; + auto b = Behaviors.CheckKey(type); + if (b != nullptr) { - auto b = Behaviors[type.TypeName]; - - Behaviors.Remove(type.TypeName); - if (b != nullptr && !(b->ObjectFlags & OF_EuthanizeMe)) + if (b->Get() != nullptr) { - b->Destroy(); - return true; + b->ForceGet()->Destroy(); + res = true; } + + Behaviors.Remove(type); } - return false; + return res; } static int RemoveBehavior(AActor* self, PClass* type) { - return self->RemoveBehavior(*type); + return self->RemoveBehavior(type->TypeName); } DEFINE_ACTION_FUNCTION_NATIVE(AActor, RemoveBehavior, RemoveBehavior) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS_NOT_NULL(type, DBehavior); - ACTION_RETURN_BOOL(self->RemoveBehavior(*type)); + ACTION_RETURN_BOOL(self->RemoveBehavior(type->TypeName)); } DBehavior* AActor::AddBehavior(PClass& type) @@ -525,7 +526,7 @@ DBehavior* AActor::AddBehavior(PClass& type) if (type.bAbstract || !type.IsDescendantOf(NAME_Behavior)) return nullptr; - auto b = FindBehavior(type); + auto b = FindBehavior(type.TypeName); if (b == nullptr) { b = dyn_cast(type.CreateNew()); @@ -542,7 +543,7 @@ DBehavior* AActor::AddBehavior(PClass& type) if (b->ObjectFlags & OF_EuthanizeMe) { - RemoveBehavior(type); + RemoveBehavior(type.TypeName); return nullptr; } } @@ -556,7 +557,7 @@ DBehavior* AActor::AddBehavior(PClass& type) if (b->ObjectFlags & OF_EuthanizeMe) { - RemoveBehavior(type); + RemoveBehavior(type.TypeName); return nullptr; } } @@ -582,12 +583,12 @@ void AActor::TickBehaviors() TArray toRemove = {}; TArray toTick = {}; - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { Behaviors }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) { - auto b = pair->Value; - if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + auto b = pair->Value.Get(); + if (b == nullptr) { toRemove.Push(pair->Key); continue; @@ -598,9 +599,9 @@ void AActor::TickBehaviors() for (auto& b : toTick) { - if (b->Owner != this) + if ((b->ObjectFlags & OF_EuthanizeMe) || b->Owner != this) { - toRemove.Push(pair->Key); + toRemove.Push(b->GetClass()->TypeName); continue; } @@ -610,12 +611,12 @@ void AActor::TickBehaviors() VMCall(func, params, 1, nullptr, 0); if (b->ObjectFlags & OF_EuthanizeMe) - toRemove.Push(pair->Key); + toRemove.Push(b->GetClass()->TypeName); } } for (auto& type : toRemove) - RemoveBehavior(*PClass::FindClass(type)); + RemoveBehavior(type); } static void TickBehaviors(AActor* self) @@ -632,14 +633,14 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, TickBehaviors, TickBehaviors) static DBehavior* FindBehavior(AActor* self, PClass* type) { - return self->FindBehavior(*type); + return self->FindBehavior(type->TypeName); } DEFINE_ACTION_FUNCTION_NATIVE(AActor, FindBehavior, FindBehavior) { PARAM_SELF_PROLOGUE(AActor); PARAM_CLASS_NOT_NULL(type, DBehavior); - ACTION_RETURN_OBJECT(self->FindBehavior(*type)); + ACTION_RETURN_OBJECT(self->FindBehavior(type->TypeName)); } void AActor::MoveBehaviors(AActor& from) @@ -653,12 +654,12 @@ void AActor::MoveBehaviors(AActor& from) // Clean up any empty behaviors that remained as well while // changing the owner. - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { Behaviors }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) { - auto b = pair->Value; - if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + auto b = pair->Value.Get(); + if (b == nullptr) { toRemove.Push(pair->Key); continue; @@ -673,7 +674,7 @@ void AActor::MoveBehaviors(AActor& from) } for (auto& type : toRemove) - RemoveBehavior(*PClass::FindClass(type)); + RemoveBehavior(type); } static void MoveBehaviors(AActor* self, AActor* from) @@ -689,30 +690,37 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, MoveBehaviors, MoveBehaviors) return 0; } -void AActor::ClearBehaviors() +void AActor::ClearBehaviors(PClass* type) { TArray toRemove = {}; - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { Behaviors }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) - toRemove.Push(pair->Key); + { + auto b = pair->Value.Get(); + if (type == nullptr || b == nullptr || b->IsKindOf(type)) + toRemove.Push(pair->Key); + } for (auto& type : toRemove) - RemoveBehavior(*PClass::FindClass(type)); + RemoveBehavior(type); - Behaviors.Clear(); + // If not removing a specific type, clear whatever remains. + if (type == nullptr) + Behaviors.Clear(); } -static void ClearBehaviors(AActor* self) +static void ClearBehaviors(AActor* self, PClass* type) { - self->ClearBehaviors(); + self->ClearBehaviors(type); } DEFINE_ACTION_FUNCTION_NATIVE(AActor, ClearBehaviors, ClearBehaviors) { PARAM_SELF_PROLOGUE(AActor); - self->ClearBehaviors(); + PARAM_CLASS(type, DBehavior); + self->ClearBehaviors(type); return 0; } @@ -720,38 +728,38 @@ void AActor::UnlinkBehaviorsFromLevel() { TArray toRemove = {}; - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { Behaviors }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) { - auto b = pair->Value; - if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + auto b = pair->Value.Get(); + if (b == nullptr) toRemove.Push(pair->Key); else b->Level->RemoveActorBehavior(*b); } for (auto& type : toRemove) - RemoveBehavior(*PClass::FindClass(type)); + RemoveBehavior(type); } void AActor::LinkBehaviorsToLevel() { TArray toRemove = {}; - TMap::Iterator it = { Behaviors }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { Behaviors }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) { - auto b = pair->Value; - if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + auto b = pair->Value.Get(); + if (b == nullptr) toRemove.Push(pair->Key); else Level->AddActorBehavior(*b); } for (auto& type : toRemove) - RemoveBehavior(*PClass::FindClass(type)); + RemoveBehavior(type); } //========================================================================== diff --git a/src/scripting/vmiterators.cpp b/src/scripting/vmiterators.cpp index 7762d176b..ec053e5fe 100644 --- a/src/scripting/vmiterators.cpp +++ b/src/scripting/vmiterators.cpp @@ -400,21 +400,21 @@ class DBehaviorIterator : public DObject { DECLARE_ABSTRACT_CLASS(DBehaviorIterator, DObject) size_t _index; - TArray _behaviors; + TArray> _behaviors; public: DBehaviorIterator(const AActor& mobj, PClass* type) { - TMap::ConstIterator it = { mobj.Behaviors }; - TMap::ConstPair* pair = nullptr; + TMap>::ConstIterator it = { mobj.Behaviors }; + TMap>::ConstPair* pair = nullptr; while (it.NextPair(pair)) { - auto b = pair->Value; - if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) + auto b = pair->Value.Get(); + if (b == nullptr) continue; if (type == nullptr || b->IsKindOf(type)) - _behaviors.Push(b); + _behaviors.Push(pair->Value); } Reinit(); @@ -424,14 +424,11 @@ public: { for (auto& b : level.ActorBehaviors) { - if (b == nullptr || (b->ObjectFlags & OF_EuthanizeMe)) - continue; - if (ownerType != nullptr && !b->Owner->IsKindOf(ownerType)) continue; if (type == nullptr || b->IsKindOf(type)) - _behaviors.Push(b); + _behaviors.Push(MakeObjPtr(b)); } Reinit(); @@ -439,10 +436,14 @@ public: DBehavior* Next() { - if (_index >= _behaviors.Size()) - return nullptr; + while (_index < _behaviors.Size()) + { + auto b = _behaviors[_index++].Get(); + if (b != nullptr) + return b; + } - return _behaviors[_index++]; + return nullptr; } void Reinit() { _index = 0u; } diff --git a/src/serializer_doom.cpp b/src/serializer_doom.cpp index 566aa3164..a950a5d8e 100644 --- a/src/serializer_doom.cpp +++ b/src/serializer_doom.cpp @@ -226,17 +226,21 @@ FSerializer& FDoomSerializer::StatePointer(const char* key, void* ptraddr, bool } -FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def) +FSerializer& Serialize(FSerializer& arc, const char* key, TMap>& value, TMap>* def) { if (!arc.BeginObject(key)) return arc; if (arc.isWriting()) { - TMap::Iterator it = { value }; - TMap::Pair* pair = nullptr; + TMap>::Iterator it = { value }; + TMap>::Pair* pair = nullptr; while (it.NextPair(pair)) - arc(pair->Key.GetChars(), pair->Value); + { + auto b = pair->Value.Get(); + if (b != nullptr) + arc(pair->Key.GetChars(), b); + } } else { diff --git a/src/serializer_doom.h b/src/serializer_doom.h index 4abc9fb19..1ce88c0e0 100644 --- a/src/serializer_doom.h +++ b/src/serializer_doom.h @@ -44,7 +44,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &sid, ticcmd_ FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, FInterpolator &rs, FInterpolator *def); FSerializer& Serialize(FSerializer& arc, const char* key, struct FStandaloneAnimation& value, struct FStandaloneAnimation* defval); -FSerializer& Serialize(FSerializer& arc, const char* key, TMap& value, TMap* def); +FSerializer& Serialize(FSerializer& arc, const char* key, TMap>& value, TMap>* def); template<> FSerializer &Serialize(FSerializer &arc, const char *key, FPolyObj *&value, FPolyObj **defval); template<> FSerializer &Serialize(FSerializer &arc, const char *key, sector_t *&value, sector_t **defval); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 5d24f34b0..1471dec61 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -522,7 +522,7 @@ class Actor : Thinker native native bool RemoveBehavior(class type); native Behavior AddBehavior(class type); native void TickBehaviors(); - native void ClearBehaviors(); + native void ClearBehaviors(class type = null); native void MoveBehaviors(Actor from); native clearscope bool isFrozen() const; From 7a4927fdf5b9fdf85407c1441f461be50ffa18f4 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 28 Jan 2025 09:15:56 -0500 Subject: [PATCH 010/384] Added TransferredOwner virtual Allows for proper cleaning up of behaviors when moving between Actors. Important for player respawn handling and morphing. --- src/playsim/actor.h | 4 ++++ src/playsim/p_mobj.cpp | 34 +++++++++++++++++++++++---- wadsrc/static/zscript/actors/actor.zs | 1 + 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index cbc2781de..3339adeb9 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -1453,6 +1453,10 @@ public: auto b = Behaviors.CheckKey(type); return b != nullptr ? b->Get() : nullptr; } + bool IsValidBehavior(const DBehavior& b) const + { + return !(b.ObjectFlags & OF_EuthanizeMe) && b.Owner.ForceGet() == this; + } DBehavior* AddBehavior(PClass& type); bool RemoveBehavior(FName type); void TickBehaviors(); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index c6ebe85e4..d040f3de0 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -541,7 +541,7 @@ DBehavior* AActor::AddBehavior(PClass& type) VMValue params[] = { b }; VMCall(func, params, 1, nullptr, 0); - if (b->ObjectFlags & OF_EuthanizeMe) + if (!IsValidBehavior(*b)) { RemoveBehavior(type.TypeName); return nullptr; @@ -555,7 +555,7 @@ DBehavior* AActor::AddBehavior(PClass& type) VMValue params[] = { b }; VMCall(func, params, 1, nullptr, 0); - if (b->ObjectFlags & OF_EuthanizeMe) + if (!IsValidBehavior(*b)) { RemoveBehavior(type.TypeName); return nullptr; @@ -599,7 +599,7 @@ void AActor::TickBehaviors() for (auto& b : toTick) { - if ((b->ObjectFlags & OF_EuthanizeMe) || b->Owner != this) + if (!IsValidBehavior(*b)) { toRemove.Push(b->GetClass()->TypeName); continue; @@ -610,7 +610,7 @@ void AActor::TickBehaviors() VMValue params[] = { b }; VMCall(func, params, 1, nullptr, 0); - if (b->ObjectFlags & OF_EuthanizeMe) + if (!IsValidBehavior(*b)) toRemove.Push(b->GetClass()->TypeName); } } @@ -645,12 +645,16 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, FindBehavior, FindBehavior) void AActor::MoveBehaviors(AActor& from) { + if (&from == this) + return; + // Clean these up properly before transferring. ClearBehaviors(); Behaviors.TransferFrom(from.Behaviors); TArray toRemove = {}; + TArray toTransfer = {}; // Clean up any empty behaviors that remained as well while // changing the owner. @@ -671,6 +675,26 @@ void AActor::MoveBehaviors(AActor& from) b->Level->RemoveActorBehavior(*b); Level->AddActorBehavior(*b); } + + toTransfer.Push(b); + } + + for (auto& b : toTransfer) + { + if (!IsValidBehavior(*b)) + { + toRemove.Push(b->GetClass()->TypeName); + continue; + } + + IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, TransferredOwner) + { + VMValue params[] = { b, &from }; + VMCall(func, params, 2, nullptr, 0); + + if (!IsValidBehavior(*b)) + toRemove.Push(b->GetClass()->TypeName); + } } for (auto& type : toRemove) @@ -5990,6 +6014,8 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag const auto heldWeap = state == PST_REBORN && (dmflags3 & DF3_REMEMBER_LAST_WEAP) ? p->ReadyWeapon : nullptr; if (state == PST_REBORN || state == PST_ENTER) { + if (state == PST_REBORN && oldactor != nullptr) + p->mo->MoveBehaviors(*oldactor); PlayerReborn (playernum); } else if (oldactor != NULL && oldactor->player == p && !(flags & SPF_TEMPPLAYER)) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 1471dec61..56fa7ad10 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -80,6 +80,7 @@ class Behavior native play abstract virtual void Initialize() {} virtual void Reinitialize() {} + virtual void TransferredOwner(Actor oldOwner) {} virtual void Tick() {} } From 606bc1d04e3517694c585907bc43493c2f77750a Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 20 Feb 2025 02:11:43 -0500 Subject: [PATCH 011/384] Added versioning for Behaviors --- wadsrc/static/zscript/actors/actor.zs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 56fa7ad10..a2e587cd8 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -73,7 +73,7 @@ class ViewPosition native native readonly int Flags; } -class Behavior native play abstract +class Behavior native play abstract version("4.15") { native readonly Actor Owner; native readonly LevelLocals Level; @@ -84,7 +84,7 @@ class Behavior native play abstract virtual void Tick() {} } -class BehaviorIterator native abstract final +class BehaviorIterator native abstract final version("4.15") { native static BehaviorIterator CreateFrom(Actor mobj, class type = null); native static BehaviorIterator Create(class type = null, class ownerType = null); @@ -519,12 +519,12 @@ class Actor : Thinker native return sin(fb * (180./32)) * 8; } - native clearscope Behavior FindBehavior(class type) const; - native bool RemoveBehavior(class type); - native Behavior AddBehavior(class type); - native void TickBehaviors(); - native void ClearBehaviors(class type = null); - native void MoveBehaviors(Actor from); + native version("4.15") clearscope Behavior FindBehavior(class type) const; + native version("4.15") bool RemoveBehavior(class type); + native version("4.15") Behavior AddBehavior(class type); + native version("4.15") void TickBehaviors(); + native version("4.15") void ClearBehaviors(class type = null); + native version("4.15") void MoveBehaviors(Actor from); native clearscope bool isFrozen() const; virtual native void BeginPlay(); From b54da619adf36f18e25612832bbeb9e2c5d7236a Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Fri, 7 Feb 2025 02:28:59 +0200 Subject: [PATCH 012/384] Added spawn origin flags. - Added new spawn flags that allow for checking if an actor was spawned by the level, the console, or ACS/DECORATE/ZScript. --- src/d_net.cpp | 1 + src/doomdata.h | 5 +++++ src/playsim/p_mobj.cpp | 1 + wadsrc/static/zscript/constants.zs | 5 +++++ 4 files changed, 12 insertions(+) diff --git a/src/d_net.cpp b/src/d_net.cpp index 07586bbad..e1f665eec 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -2423,6 +2423,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) AActor *spawned = Spawn (primaryLevel, typeinfo, spawnpos, ALLOW_REPLACE); if (spawned != NULL) { + spawned->SpawnFlags |= MTF_CONSOLETHING; if (type == DEM_SUMMONFRIEND || type == DEM_SUMMONFRIEND2 || type == DEM_SUMMONMBF) { if (spawned->CountsAsKill()) diff --git a/src/doomdata.h b/src/doomdata.h index 49ba77421..c420fde03 100644 --- a/src/doomdata.h +++ b/src/doomdata.h @@ -424,6 +424,11 @@ enum EMapThingFlags MTF_NOINFIGHTING = 0x100000, MTF_NOCOUNT = 0x200000, // Removes COUNTKILL/COUNTITEM + // Thing spawn origins, what created this thing? + MTF_MAPTHING = 0x400000, // Map spawned + MTF_CONSOLETHING = 0x800000, // Console spawned (i.e summon) + MTF_NONSPAWNTHING = (MTF_MAPTHING|MTF_CONSOLETHING), // [inkoalawetrust]: Rachael didn't want a dedicated MTF_SPAWNTHING flag taking up the field, so check if the other 2 flags aren't true instead. + // BOOM and DOOM compatible versions of some of the above BTF_NOTSINGLE = 0x0010, // (TF_COOPERATIVE|TF_DEATHMATCH) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index d040f3de0..b140823b2 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -5352,6 +5352,7 @@ void AActor::LevelSpawned () { flags &= ~MF_DROPPED; } + SpawnFlags |= MTF_MAPTHING; HandleSpawnFlags (); } diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index 9b0a0f709..cd3eceb15 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1008,6 +1008,11 @@ enum EMapThingFlags MTF_SECRET = 0x080000, // Secret pickup MTF_NOINFIGHTING = 0x100000, MTF_NOCOUNT = 0x200000, // Removes COUNTKILL/COUNTITEM + + // Thing spawn origins, what created this thing? + MTF_MAPTHING = 0x400000, // Map spawned + MTF_CONSOLETHING = 0x800000, // Console spawned (i.e summon) + MTF_NONSPAWNTHING = (MTF_MAPTHING|MTF_CONSOLETHING), // [inkoalawetrust]: Rachael didn't want a dedicated MTF_SPAWNTHING flag taking up the field, so check if the other 2 flags aren't true instead. }; enum ESkillProperty From f1e6445e820cd9c5a7cb2f63c3b787c20788cfd4 Mon Sep 17 00:00:00 2001 From: f7cjo <71151647+f7cjo@users.noreply.github.com> Date: Sun, 16 Feb 2025 16:16:02 +0100 Subject: [PATCH 013/384] Add GetSelfObituary --- src/playsim/p_interaction.cpp | 11 ++++++++++- wadsrc/static/zscript/actors/actor.zs | 7 +++++++ wadsrc/static/zscript/actors/player/player.zs | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/playsim/p_interaction.cpp b/src/playsim/p_interaction.cpp index 60b6f3059..8d3e5022b 100644 --- a/src/playsim/p_interaction.cpp +++ b/src/playsim/p_interaction.cpp @@ -244,7 +244,16 @@ void ClientObituary (AActor *self, AActor *inflictor, AActor *attacker, int dmgf { if (attacker == self) { - message = "$OB_KILLEDSELF"; + messagename = "$OB_KILLEDSELF"; + + IFVIRTUALPTR(self, AActor, GetSelfObituary) + { + VMValue params[] = { self, inflictor, mod.GetIndex() }; + VMReturn rett(&ret); + VMCall(func, params, countof(params), &rett, 1); + if (ret.IsNotEmpty()) message = ret.GetChars(); + } + } else { diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index a2e587cd8..898185701 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -287,6 +287,7 @@ class Actor : Thinker native meta String Obituary; // Player was killed by this actor meta String HitObituary; // Player was killed by this actor in melee + meta String SelfObituary; // Player killed himself using this actor meta double DeathHeight; // Height on normal death meta double BurnHeight; // Height on burning death meta int GibHealth; // Negative health below which this monster dies an extreme death @@ -313,6 +314,7 @@ class Actor : Thinker native Property prefix: none; Property Obituary: Obituary; Property HitObituary: HitObituary; + Property SelfObituary: SelfObituary; Property MeleeDamage: MeleeDamage; Property MeleeSound: MeleeSound; Property MissileHeight: MissileHeight; @@ -693,6 +695,11 @@ class Actor : Thinker native } return Obituary; } + + virtual String GetSelfObituary(Actor inflictor, Name mod) + { + return SelfObituary; + } virtual int OnDrain(Actor victim, int damage, Name dmgtype) { diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index 6bfb58344..22454dc65 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -317,6 +317,22 @@ class PlayerPawn : Actor return message; } } + + override String GetSelfObituary(Actor inflictor, Name mod) + { + String message; + + if (inflictor && inflictor != self) + { + message = inflictor.GetSelfObituary(inflictor, mod); + } + if (message.Length() == 0) + { + message = SelfObituary; + } + + return message; + } //---------------------------------------------------------------------------- // From 9f6c1d65c53a29a6d1c6220375d9b3abb8ce551a Mon Sep 17 00:00:00 2001 From: marrub Date: Sun, 16 Feb 2025 21:21:38 -0700 Subject: [PATCH 014/384] add default cvar getters --- src/common/scripting/interface/vmnatives.cpp | 21 ++++++++++++++++++++ wadsrc/static/zscript/engine/base.zs | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/src/common/scripting/interface/vmnatives.cpp b/src/common/scripting/interface/vmnatives.cpp index ca967c264..c1bb2a2b5 100644 --- a/src/common/scripting/interface/vmnatives.cpp +++ b/src/common/scripting/interface/vmnatives.cpp @@ -902,6 +902,27 @@ DEFINE_ACTION_FUNCTION(_CVar, GetString) ACTION_RETURN_STRING(v.String); } +DEFINE_ACTION_FUNCTION(_CVar, GetDefaultInt) +{ + PARAM_SELF_STRUCT_PROLOGUE(FBaseCVar); + auto v = self->GetGenericRepDefault(CVAR_Int); + ACTION_RETURN_INT(v.Int); +} + +DEFINE_ACTION_FUNCTION(_CVar, GetDefaultFloat) +{ + PARAM_SELF_STRUCT_PROLOGUE(FBaseCVar); + auto v = self->GetGenericRepDefault(CVAR_Float); + ACTION_RETURN_FLOAT(v.Float); +} + +DEFINE_ACTION_FUNCTION(_CVar, GetDefaultString) +{ + PARAM_SELF_STRUCT_PROLOGUE(FBaseCVar); + auto v = self->GetGenericRepDefault(CVAR_String); + ACTION_RETURN_STRING(v.String); +} + DEFINE_ACTION_FUNCTION(_CVar, SetInt) { // Only menus are allowed to change CVARs. diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index 28d8a5604..70710d7de 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -696,6 +696,10 @@ struct CVar native native int GetInt(); native double GetFloat(); native String GetString(); + bool GetDefaultBool() { return GetDefaultInt(); } + native int GetDefaultInt(); + native double GetDefaultFloat(); + native String GetDefaultString(); void SetBool(bool b) { SetInt(b); } native void SetInt(int v); native void SetFloat(double v); From 236c9b4224d04cf43fddfd9d179e309c68f51a36 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 20 Feb 2025 01:58:34 -0500 Subject: [PATCH 015/384] Added FindClass Allows for classes to be looked up during run time without having to use string casting. --- src/common/scripting/backend/codegen.cpp | 2 +- wadsrc/static/zscript/engine/base.zs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/scripting/backend/codegen.cpp b/src/common/scripting/backend/codegen.cpp index 92b68d703..1fcaeae2c 100644 --- a/src/common/scripting/backend/codegen.cpp +++ b/src/common/scripting/backend/codegen.cpp @@ -12367,7 +12367,7 @@ static PClass *NativeNameToClass(int _clsname, PClass *desttype) if (clsname != NAME_None) { cls = PClass::FindClass(clsname); - if (cls != nullptr && (cls->VMType == nullptr || !cls->IsDescendantOf(desttype))) + if (cls != nullptr && (cls->VMType == nullptr || (desttype != nullptr && !cls->IsDescendantOf(desttype)))) { // does not match required parameters or is invalid. return nullptr; diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index 70710d7de..fa212ba38 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -771,6 +771,7 @@ class Object native private native static bool CheckDeprecatedFlags(Object obj, int index); native static Name ValidateNameIndex(int index); + static class FindClass(Name cls, class baseType = null) { return BuiltinNameToClass(cls, baseType); } native static uint MSTime(); native static double MSTimeF(); From 7e86116ab17214863bf0a8d04229ae08fbf2a7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 7 Feb 2025 19:39:27 -0300 Subject: [PATCH 016/384] make const actually work, and add `unsafe(const)` for old behavior --- src/common/engine/sc_man_scanner.re | 1 + src/common/engine/sc_man_tokens.h | 1 + src/common/scripting/backend/codegen.cpp | 16 ++++++++-------- src/common/scripting/backend/codegen.h | 7 ++++--- src/common/scripting/core/types.h | 1 + src/common/scripting/frontend/zcc-parse.lemon | 5 +++-- src/common/scripting/frontend/zcc_compile.cpp | 8 +++++--- src/common/scripting/frontend/zcc_parser.cpp | 1 + src/common/scripting/frontend/zcc_parser.h | 1 + 9 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/common/engine/sc_man_scanner.re b/src/common/engine/sc_man_scanner.re index 5af7e62f9..a1283778e 100644 --- a/src/common/engine/sc_man_scanner.re +++ b/src/common/engine/sc_man_scanner.re @@ -177,6 +177,7 @@ std2: /* Other keywords from UnrealScript */ 'abstract' { RET(TK_Abstract); } 'foreach' { RET(ParseVersion >= MakeVersion(4, 10, 0)? TK_ForEach : TK_Identifier); } + 'unsafe' { RET(ParseVersion >= MakeVersion(4, 15, 0)? TK_Unsafe : TK_Identifier); } 'true' { RET(TK_True); } 'false' { RET(TK_False); } 'none' { RET(TK_None); } diff --git a/src/common/engine/sc_man_tokens.h b/src/common/engine/sc_man_tokens.h index 487003125..85bb55d42 100644 --- a/src/common/engine/sc_man_tokens.h +++ b/src/common/engine/sc_man_tokens.h @@ -80,6 +80,7 @@ xx(TK_Color, "'color'") xx(TK_Goto, "'goto'") xx(TK_Abstract, "'abstract'") xx(TK_ForEach, "'foreach'") +xx(TK_Unsafe, "'unsafe'") xx(TK_True, "'true'") xx(TK_False, "'false'") xx(TK_None, "'none'") diff --git a/src/common/scripting/backend/codegen.cpp b/src/common/scripting/backend/codegen.cpp index 1fcaeae2c..1ce7d6acf 100644 --- a/src/common/scripting/backend/codegen.cpp +++ b/src/common/scripting/backend/codegen.cpp @@ -6701,7 +6701,7 @@ FxExpression *FxIdentifier::Resolve(FCompileContext& ctx) } FxExpression *self = new FxSelf(ScriptPosition); self = self->Resolve(ctx); - newex = ResolveMember(ctx, ctx.Function->Variants[0].SelfClass, self, ctx.Function->Variants[0].SelfClass); + newex = ResolveMember(ctx, ctx.Function->Variants[0].SelfClass, self, ctx.Function->Variants[0].SelfClass, ctx.Function->Variants[0].Flags & VARF_SafeConst); ABORT(newex); goto foundit; } @@ -6863,7 +6863,7 @@ foundit: // //========================================================================== -FxExpression *FxIdentifier::ResolveMember(FCompileContext &ctx, PContainerType *classctx, FxExpression *&object, PContainerType *objtype) +FxExpression *FxIdentifier::ResolveMember(FCompileContext &ctx, PContainerType *classctx, FxExpression *&object, PContainerType *objtype, bool isConst) { PSymbol *sym; PSymbolTable *symtbl; @@ -6956,7 +6956,7 @@ FxExpression *FxIdentifier::ResolveMember(FCompileContext &ctx, PContainerType * } } - auto x = isclass ? new FxClassMember(object, vsym, ScriptPosition) : new FxStructMember(object, vsym, ScriptPosition); + auto x = isclass ? new FxClassMember(object, vsym, ScriptPosition, isConst) : new FxStructMember(object, vsym, ScriptPosition, isConst); object = nullptr; return x->Resolve(ctx); } @@ -7611,8 +7611,8 @@ FxMemberBase::FxMemberBase(EFxType type, PField *f, const FScriptPosition &p) } -FxStructMember::FxStructMember(FxExpression *x, PField* mem, const FScriptPosition &pos) - : FxMemberBase(EFX_StructMember, mem, pos) +FxStructMember::FxStructMember(FxExpression *x, PField* mem, const FScriptPosition &pos, bool isConst) + : FxMemberBase(EFX_StructMember, mem, pos), IsConst(isConst) { classx = x; } @@ -7662,7 +7662,7 @@ bool FxStructMember::RequestAddress(FCompileContext &ctx, bool *writable) bWritable = false; } - *writable = bWritable; + *writable = bWritable && !IsConst; } return true; } @@ -7873,8 +7873,8 @@ ExpEmit FxStructMember::Emit(VMFunctionBuilder *build) // //========================================================================== -FxClassMember::FxClassMember(FxExpression *x, PField* mem, const FScriptPosition &pos) -: FxStructMember(x, mem, pos) +FxClassMember::FxClassMember(FxExpression *x, PField* mem, const FScriptPosition &pos, bool isConst) +: FxStructMember(x, mem, pos, isConst) { ExprType = EFX_ClassMember; } diff --git a/src/common/scripting/backend/codegen.h b/src/common/scripting/backend/codegen.h index cd37a7102..bfe08ec74 100644 --- a/src/common/scripting/backend/codegen.h +++ b/src/common/scripting/backend/codegen.h @@ -396,7 +396,7 @@ public: FxIdentifier(FName i, const FScriptPosition &p); FxExpression *Resolve(FCompileContext&); - FxExpression *ResolveMember(FCompileContext&, PContainerType*, FxExpression*&, PContainerType*); + FxExpression *ResolveMember(FCompileContext&, PContainerType*, FxExpression*&, PContainerType*, bool isConst = false); }; @@ -1475,8 +1475,9 @@ class FxStructMember : public FxMemberBase { public: FxExpression *classx; + bool IsConst; - FxStructMember(FxExpression*, PField*, const FScriptPosition&); + FxStructMember(FxExpression*, PField*, const FScriptPosition&, bool isConst = false); ~FxStructMember(); FxExpression *Resolve(FCompileContext&); bool RequestAddress(FCompileContext &ctx, bool *writable); @@ -1494,7 +1495,7 @@ class FxClassMember : public FxStructMember { public: - FxClassMember(FxExpression*, PField*, const FScriptPosition&); + FxClassMember(FxExpression*, PField*, const FScriptPosition&, bool isConst = false); }; //========================================================================== diff --git a/src/common/scripting/core/types.h b/src/common/scripting/core/types.h index f09ddb32b..69d73bd77 100644 --- a/src/common/scripting/core/types.h +++ b/src/common/scripting/core/types.h @@ -37,6 +37,7 @@ enum VARF_VirtualScope = (1<<22), // [ZZ] virtualscope: object should use the scope of the particular class it's being used with (methods only) VARF_ClearScope = (1<<23), // [ZZ] clearscope: this method ignores the member access chain that leads to it and is always plain data. VARF_Abstract = (1<<24), // [Player701] Function does not have a body and must be overridden in subclasses + VARF_SafeConst = (1<<25), // [Jay] properly-working const function/unsafe clearscope field }; // Basic information shared by all types ------------------------------------ diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index 199771949..72dbc35fa 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -1316,8 +1316,9 @@ decl_flag(X) ::= PLAY(T). { X.Int = ZCC_Play; X.SourceLoc = T.SourceLoc; } decl_flag(X) ::= CLEARSCOPE(T). { X.Int = ZCC_ClearScope; X.SourceLoc = T.SourceLoc; } decl_flag(X) ::= VIRTUALSCOPE(T). { X.Int = ZCC_VirtualScope; X.SourceLoc = T.SourceLoc; } -func_const(X) ::= . { X.Int = 0; X.SourceLoc = stat->sc->GetMessageLine(); } -func_const(X) ::= CONST(T). { X.Int = ZCC_FuncConst; X.SourceLoc = T.SourceLoc; } +func_const(X) ::= . { X.Int = 0; X.SourceLoc = stat->sc->GetMessageLine(); } +func_const(X) ::= CONST(T). { X.Int = ZCC_FuncConst; X.SourceLoc = T.SourceLoc; } +func_const(X) ::= UNSAFE(T) LPAREN CONST RPAREN. { X.Int = ZCC_FuncConstUnsafe; X.SourceLoc = T.SourceLoc; } opt_func_body(X) ::= SEMICOLON. { X = NULL; } opt_func_body(X) ::= function_body(X). diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index faef50959..51591c399 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -1475,8 +1475,8 @@ bool ZCCCompiler::CompileFields(PContainerType *type, TArrayFlags & ZCC_Override) varflags |= VARF_Override; if (f->Flags & ZCC_Abstract) varflags |= VARF_Abstract; if (f->Flags & ZCC_VarArg) varflags |= VARF_VarArg; - if (f->Flags & ZCC_FuncConst) varflags |= VARF_ReadOnly; // FuncConst method is internally marked as VARF_ReadOnly + if (f->Flags & ZCC_FuncConst) varflags |= (mVersion >= MakeVersion(4, 15, 0) ? VARF_ReadOnly | VARF_SafeConst : VARF_ReadOnly); // FuncConst method is internally marked as VARF_ReadOnly + if (f->Flags & ZCC_FuncConstUnsafe) varflags |= VARF_ReadOnly; + if (mVersion >= MakeVersion(2, 4, 0)) { if (c->Type()->ScopeFlags & Scope_UI) diff --git a/src/common/scripting/frontend/zcc_parser.cpp b/src/common/scripting/frontend/zcc_parser.cpp index 6ea3c316e..35c6cc74e 100644 --- a/src/common/scripting/frontend/zcc_parser.cpp +++ b/src/common/scripting/frontend/zcc_parser.cpp @@ -251,6 +251,7 @@ static void InitTokenMap() TOKENDEF (TK_Do, ZCC_DO); TOKENDEF (TK_For, ZCC_FOR); TOKENDEF (TK_ForEach, ZCC_FOREACH); + TOKENDEF (TK_Unsafe, ZCC_UNSAFE); TOKENDEF (TK_While, ZCC_WHILE); TOKENDEF (TK_Until, ZCC_UNTIL); TOKENDEF (TK_If, ZCC_IF); diff --git a/src/common/scripting/frontend/zcc_parser.h b/src/common/scripting/frontend/zcc_parser.h index d0b6264da..cf2574c28 100644 --- a/src/common/scripting/frontend/zcc_parser.h +++ b/src/common/scripting/frontend/zcc_parser.h @@ -65,6 +65,7 @@ enum ZCC_Version = 1 << 21, ZCC_Internal = 1 << 22, ZCC_Sealed = 1 << 23, + ZCC_FuncConstUnsafe = 1 << 24, }; // Function parameter modifiers From ad579a8e431dac672e0df8a698d52d05e0474548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 7 Feb 2025 19:47:30 -0300 Subject: [PATCH 017/384] add unsafe clearscope, to allow declaring clearscope fields outside of gzdoom.pk3 --- src/common/scripting/frontend/zcc-parse.lemon | 1 + src/common/scripting/frontend/zcc_compile.cpp | 4 +- src/common/scripting/frontend/zcc_parser.h | 51 ++++++++++--------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index 72dbc35fa..ab46782aa 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -1314,6 +1314,7 @@ decl_flag(X) ::= VARARG(T). { X.Int = ZCC_VarArg; X.SourceLoc = T.SourceLoc; decl_flag(X) ::= UI(T). { X.Int = ZCC_UIFlag; X.SourceLoc = T.SourceLoc; } decl_flag(X) ::= PLAY(T). { X.Int = ZCC_Play; X.SourceLoc = T.SourceLoc; } decl_flag(X) ::= CLEARSCOPE(T). { X.Int = ZCC_ClearScope; X.SourceLoc = T.SourceLoc; } +decl_flag(X) ::= UNSAFE(T) LPAREN CLEARSCOPE RPAREN. { X.Int = ZCC_UnsafeClearScope; X.SourceLoc = T.SourceLoc; } decl_flag(X) ::= VIRTUALSCOPE(T). { X.Int = ZCC_VirtualScope; X.SourceLoc = T.SourceLoc; } func_const(X) ::= . { X.Int = 0; X.SourceLoc = stat->sc->GetMessageLine(); } diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index 51591c399..8a432e8c1 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -1508,7 +1508,7 @@ bool ZCCCompiler::CompileFields(PContainerType *type, TArrayFlags & ZCC_Play) varflags = FScopeBarrier::ChangeSideInFlags(varflags, FScopeBarrier::Side_Play); - if (field->Flags & ZCC_ClearScope) + if (field->Flags & (ZCC_ClearScope | ZCC_UnsafeClearScope)) varflags = FScopeBarrier::ChangeSideInFlags(varflags, FScopeBarrier::Side_PlainData); } else @@ -2315,7 +2315,7 @@ void ZCCCompiler::SetImplicitArgs(TArray* args, TArray* argfla if (funcflags & VARF_Method) { // implied self pointer - if (args != nullptr) args->Push(NewPointer(cls, !!(funcflags & VARF_ReadOnly))); + if (args != nullptr) args->Push(NewPointer(cls, (funcflags & VARF_SafeConst))); if (argflags != nullptr) argflags->Push(VARF_Implicit | VARF_ReadOnly); if (argnames != nullptr) argnames->Push(NAME_self); } diff --git a/src/common/scripting/frontend/zcc_parser.h b/src/common/scripting/frontend/zcc_parser.h index cf2574c28..47ab2d65f 100644 --- a/src/common/scripting/frontend/zcc_parser.h +++ b/src/common/scripting/frontend/zcc_parser.h @@ -41,31 +41,32 @@ struct ZCCToken // Variable / Function / Class modifiers enum { - ZCC_Native = 1 << 0, - ZCC_Static = 1 << 1, - ZCC_Private = 1 << 2, - ZCC_Protected = 1 << 3, - ZCC_Latent = 1 << 4, - ZCC_Final = 1 << 5, - ZCC_Meta = 1 << 6, - ZCC_Action = 1 << 7, - ZCC_Deprecated = 1 << 8, - ZCC_ReadOnly = 1 << 9, - ZCC_FuncConst = 1 << 10, - ZCC_Abstract = 1 << 11, - ZCC_Extension = 1 << 12, - ZCC_Virtual = 1 << 13, - ZCC_Override = 1 << 14, - ZCC_Transient = 1 << 15, - ZCC_VarArg = 1 << 16, - ZCC_UIFlag = 1 << 17, // there's also token called ZCC_UI - ZCC_Play = 1 << 18, - ZCC_ClearScope = 1 << 19, - ZCC_VirtualScope = 1 << 20, - ZCC_Version = 1 << 21, - ZCC_Internal = 1 << 22, - ZCC_Sealed = 1 << 23, - ZCC_FuncConstUnsafe = 1 << 24, + ZCC_Native = 1 << 0, + ZCC_Static = 1 << 1, + ZCC_Private = 1 << 2, + ZCC_Protected = 1 << 3, + ZCC_Latent = 1 << 4, + ZCC_Final = 1 << 5, + ZCC_Meta = 1 << 6, + ZCC_Action = 1 << 7, + ZCC_Deprecated = 1 << 8, + ZCC_ReadOnly = 1 << 9, + ZCC_FuncConst = 1 << 10, + ZCC_Abstract = 1 << 11, + ZCC_Extension = 1 << 12, + ZCC_Virtual = 1 << 13, + ZCC_Override = 1 << 14, + ZCC_Transient = 1 << 15, + ZCC_VarArg = 1 << 16, + ZCC_UIFlag = 1 << 17, // there's also token called ZCC_UI + ZCC_Play = 1 << 18, + ZCC_ClearScope = 1 << 19, + ZCC_VirtualScope = 1 << 20, + ZCC_Version = 1 << 21, + ZCC_Internal = 1 << 22, + ZCC_Sealed = 1 << 23, + ZCC_FuncConstUnsafe = 1 << 24, + ZCC_UnsafeClearScope = 1 << 25, }; // Function parameter modifiers From f9dcda91d73217a7ac2173fb8d2ce2d7c70c4983 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 20 Feb 2025 02:27:23 -0500 Subject: [PATCH 018/384] Update ZScript version to 4.15 --- wadsrc/static/zscript.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt index 11a0e8c5c..b3565f8a7 100644 --- a/wadsrc/static/zscript.txt +++ b/wadsrc/static/zscript.txt @@ -1,4 +1,4 @@ -version "4.12" +version "4.15" // Generic engine code #include "zscript/engine/base.zs" From 7bdffa2592c0af463eecd2ad4debbf6d6c06a96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 20 Feb 2025 05:21:45 -0300 Subject: [PATCH 019/384] fix OptionMenuItemCommand::DoCommand for new 4.15 keyword --- wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs index 9fc0d66ff..39430278a 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs @@ -184,7 +184,7 @@ class OptionMenuItemCommand : OptionMenuItemSubmenu return self; } - private native static void DoCommand(String cmd, bool unsafe); // This is very intentionally limited to this menu item to prevent abuse. + private native static void DoCommand(String cmd, bool is_unsafe); // This is very intentionally limited to this menu item to prevent abuse. override bool Activate() { From 286371791ab5f46f413cb4c18c31b09496c43827 Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:07:18 +0200 Subject: [PATCH 020/384] Made MTF_NOINFIGHTING work. --- src/playsim/p_mobj.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index b140823b2..6eb98c0ba 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -5370,6 +5370,10 @@ void AActor::HandleSpawnFlags () { flags4 |= MF4_STANDSTILL; } + if (SpawnFlags & MTF_NOINFIGHTING) + { + flags5 |= MF5_NOINFIGHTING; + } if (SpawnFlags & MTF_FRIENDLY) { flags |= MF_FRIENDLY; From 645dbcfa51089b1f1b9dd00a5781b384536aec9f Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:19:44 +0200 Subject: [PATCH 021/384] Made the BFG and EXPLOSIVE weapon flags usable. - Made the BFG and EXPLOSIVE weapon flags usable again. - Marked the appropriate weapons with them for the 3 main supported games. --- src/gamedata/a_weapons.h | 2 ++ wadsrc/static/zscript/actors/doom/weaponbfg.zs | 1 + wadsrc/static/zscript/actors/doom/weaponrlaunch.zs | 1 + wadsrc/static/zscript/actors/heretic/weaponphoenix.zs | 3 +++ wadsrc/static/zscript/actors/heretic/weaponskullrod.zs | 1 + wadsrc/static/zscript/actors/hexen/clericflame.zs | 1 + wadsrc/static/zscript/actors/hexen/clericholy.zs | 1 + wadsrc/static/zscript/actors/hexen/fighterhammer.zs | 1 + wadsrc/static/zscript/actors/hexen/fighterquietus.zs | 1 + wadsrc/static/zscript/actors/hexen/magestaff.zs | 1 + wadsrc/static/zscript/actors/inventory/weapons.zs | 4 ++-- wadsrc/static/zscript/actors/strife/sigil.zs | 1 + wadsrc/static/zscript/actors/strife/weapongrenade.zs | 1 + wadsrc/static/zscript/actors/strife/weaponmauler.zs | 1 + wadsrc/static/zscript/actors/strife/weaponmissile.zs | 1 + 15 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/gamedata/a_weapons.h b/src/gamedata/a_weapons.h index 3de18cf4b..a626959d8 100644 --- a/src/gamedata/a_weapons.h +++ b/src/gamedata/a_weapons.h @@ -159,5 +159,7 @@ enum WIF_NODEATHINPUT = 0x00020000, // The weapon cannot be fired/reloaded/whatever when the player is dead WIF_CHEATNOTWEAPON = 0x00040000, // Give cheat considers this not a weapon (used by Sigil) WIF_NOAUTOSWITCHTO = 0x00080000, // cannot be switched to when autoswitching weapons. + WIF_BFG = 0x00100000, // BFG tier weapon + WIF_EXPLOSIVE = 0x00200000, // Weapon is explosive }; diff --git a/wadsrc/static/zscript/actors/doom/weaponbfg.zs b/wadsrc/static/zscript/actors/doom/weaponbfg.zs index cb9e0ae7d..4ea89bd4f 100644 --- a/wadsrc/static/zscript/actors/doom/weaponbfg.zs +++ b/wadsrc/static/zscript/actors/doom/weaponbfg.zs @@ -14,6 +14,7 @@ class BFG9000 : DoomWeapon Weapon.AmmoGive 40; Weapon.AmmoType "Cell"; +WEAPON.NOAUTOFIRE; + +WEAPON.BFG; Inventory.PickupMessage "$GOTBFG9000"; Tag "$TAG_BFG9000"; } diff --git a/wadsrc/static/zscript/actors/doom/weaponrlaunch.zs b/wadsrc/static/zscript/actors/doom/weaponrlaunch.zs index dc7cd0ff2..cd965c8ca 100644 --- a/wadsrc/static/zscript/actors/doom/weaponrlaunch.zs +++ b/wadsrc/static/zscript/actors/doom/weaponrlaunch.zs @@ -13,6 +13,7 @@ class RocketLauncher : DoomWeapon Weapon.AmmoGive 2; Weapon.AmmoType "RocketAmmo"; +WEAPON.NOAUTOFIRE + +WEAPON.EXPLOSIVE Inventory.PickupMessage "$GOTLAUNCHER"; Tag "$TAG_ROCKETLAUNCHER"; } diff --git a/wadsrc/static/zscript/actors/heretic/weaponphoenix.zs b/wadsrc/static/zscript/actors/heretic/weaponphoenix.zs index 884db86f8..4a4b6d0b1 100644 --- a/wadsrc/static/zscript/actors/heretic/weaponphoenix.zs +++ b/wadsrc/static/zscript/actors/heretic/weaponphoenix.zs @@ -5,6 +5,7 @@ class PhoenixRod : Weapon Default { +WEAPON.NOAUTOFIRE + +WEAPON.EXPLOSIVE Weapon.SelectionOrder 2600; Weapon.Kickback 150; Weapon.YAdjust 15; @@ -73,6 +74,8 @@ class PhoenixRodPowered : PhoenixRod Default { +WEAPON.POWERED_UP + -WEAPON.EXPLOSIVE + +WEAPON.BFG Weapon.SisterWeapon "PhoenixRod"; Weapon.AmmoGive 0; Tag "$TAG_PHOENIXRODP"; diff --git a/wadsrc/static/zscript/actors/heretic/weaponskullrod.zs b/wadsrc/static/zscript/actors/heretic/weaponskullrod.zs index 4a287e52f..5e68a58ac 100644 --- a/wadsrc/static/zscript/actors/heretic/weaponskullrod.zs +++ b/wadsrc/static/zscript/actors/heretic/weaponskullrod.zs @@ -69,6 +69,7 @@ class SkullRodPowered : SkullRod Default { +WEAPON.POWERED_UP + +WEAPON.EXPLOSIVE //[inkoalawetrust] It DOES have a form of AOE splash damage so I think it counts in spirit. Weapon.AmmoUse1 5; Weapon.AmmoGive1 0; Weapon.SisterWeapon "SkullRod"; diff --git a/wadsrc/static/zscript/actors/hexen/clericflame.zs b/wadsrc/static/zscript/actors/hexen/clericflame.zs index 6630713b9..435f0bd62 100644 --- a/wadsrc/static/zscript/actors/hexen/clericflame.zs +++ b/wadsrc/static/zscript/actors/hexen/clericflame.zs @@ -14,6 +14,7 @@ class CWeapFlame : ClericWeapon Weapon.AmmoType1 "Mana2"; Inventory.PickupMessage "$TXT_WEAPON_C3"; Tag "$TAG_CWEAPFLAME"; + +WEAPON.EXPLOSIVE } States diff --git a/wadsrc/static/zscript/actors/hexen/clericholy.zs b/wadsrc/static/zscript/actors/hexen/clericholy.zs index e3aee9bac..64660a39f 100644 --- a/wadsrc/static/zscript/actors/hexen/clericholy.zs +++ b/wadsrc/static/zscript/actors/hexen/clericholy.zs @@ -85,6 +85,7 @@ class CWeapWraithverge : ClericWeapon Health 3; Weapon.SelectionOrder 3000; +WEAPON.PRIMARY_USES_BOTH; + +WEAPON.BFG; +Inventory.NoAttenPickupSound Weapon.AmmoUse1 18; Weapon.AmmoUse2 18; diff --git a/wadsrc/static/zscript/actors/hexen/fighterhammer.zs b/wadsrc/static/zscript/actors/hexen/fighterhammer.zs index 5082de85d..e3d8cc745 100644 --- a/wadsrc/static/zscript/actors/hexen/fighterhammer.zs +++ b/wadsrc/static/zscript/actors/hexen/fighterhammer.zs @@ -10,6 +10,7 @@ class FWeapHammer : FighterWeapon +BLOODSPLATTER Weapon.SelectionOrder 900; +WEAPON.AMMO_OPTIONAL + +WEAPON.EXPLOSIVE Weapon.AmmoUse1 3; Weapon.AmmoGive1 25; Weapon.KickBack 150; diff --git a/wadsrc/static/zscript/actors/hexen/fighterquietus.zs b/wadsrc/static/zscript/actors/hexen/fighterquietus.zs index 24dd4aa8e..662449bfc 100644 --- a/wadsrc/static/zscript/actors/hexen/fighterquietus.zs +++ b/wadsrc/static/zscript/actors/hexen/fighterquietus.zs @@ -83,6 +83,7 @@ class FWeapQuietus : FighterWeapon Health 3; Weapon.SelectionOrder 2900; +WEAPON.PRIMARY_USES_BOTH; + +WEAPON.EXPLOSIVE +Inventory.NoAttenPickupSound Weapon.AmmoUse1 14; Weapon.AmmoUse2 14; diff --git a/wadsrc/static/zscript/actors/hexen/magestaff.zs b/wadsrc/static/zscript/actors/hexen/magestaff.zs index a0935e8c8..c51061078 100644 --- a/wadsrc/static/zscript/actors/hexen/magestaff.zs +++ b/wadsrc/static/zscript/actors/hexen/magestaff.zs @@ -93,6 +93,7 @@ class MWeapBloodscourge : MageWeapon Weapon.AmmoType1 "Mana1"; Weapon.AmmoType2 "Mana2"; +WEAPON.PRIMARY_USES_BOTH; + +WEAPON.BFG +Inventory.NoAttenPickupSound Inventory.PickupMessage "$TXT_WEAPON_M4"; Inventory.PickupSound "WeaponBuild"; diff --git a/wadsrc/static/zscript/actors/inventory/weapons.zs b/wadsrc/static/zscript/actors/inventory/weapons.zs index 66a7f8964..26c61269a 100644 --- a/wadsrc/static/zscript/actors/inventory/weapons.zs +++ b/wadsrc/static/zscript/actors/inventory/weapons.zs @@ -93,12 +93,12 @@ class Weapon : StateProvider flagdef NoDeathInput: WeaponFlags, 17; // The weapon cannot be fired/reloaded/whatever when the player is dead flagdef CheatNotWeapon: WeaponFlags, 18; // Give cheat considers this not a weapon (used by Sigil) flagdef NoAutoSwitchTo : WeaponFlags, 19; // do not auto switch to this weapon ever! + flagdef BFG: WeaponFlags, 20; // This weapon is a BFG (i.e BFG9000 and the Wraithverge) + flagdef Explosive: WeaponFlags, 21; // This weapon is explosive (i.e Doom and Strife's Rocket Launchers) // no-op flags flagdef NoLMS: none, 0; flagdef Allow_With_Respawn_Invul: none, 0; - flagdef BFG: none, 0; - flagdef Explosive: none, 0; Default { diff --git a/wadsrc/static/zscript/actors/strife/sigil.zs b/wadsrc/static/zscript/actors/strife/sigil.zs index 60919c855..5337ed1d2 100644 --- a/wadsrc/static/zscript/actors/strife/sigil.zs +++ b/wadsrc/static/zscript/actors/strife/sigil.zs @@ -13,6 +13,7 @@ class Sigil : Weapon Health 1; +FLOORCLIP +WEAPON.CHEATNOTWEAPON + +WEAPON.BFG Inventory.PickupSound "weapons/sigilcharge"; Tag "$TAG_SIGIL"; Inventory.Icon "I_SGL1"; diff --git a/wadsrc/static/zscript/actors/strife/weapongrenade.zs b/wadsrc/static/zscript/actors/strife/weapongrenade.zs index 215f04c22..9b511a745 100644 --- a/wadsrc/static/zscript/actors/strife/weapongrenade.zs +++ b/wadsrc/static/zscript/actors/strife/weapongrenade.zs @@ -13,6 +13,7 @@ class StrifeGrenadeLauncher : StrifeWeapon Inventory.Icon "GRNDA0"; Tag "$TAG_GLAUNCHER1"; Inventory.PickupMessage "$TXT_GLAUNCHER"; + +WEAPON.EXPLOSIVE } States diff --git a/wadsrc/static/zscript/actors/strife/weaponmauler.zs b/wadsrc/static/zscript/actors/strife/weaponmauler.zs index 227430a90..9f895fc1a 100644 --- a/wadsrc/static/zscript/actors/strife/weaponmauler.zs +++ b/wadsrc/static/zscript/actors/strife/weaponmauler.zs @@ -98,6 +98,7 @@ class Mauler2 : Mauler Weapon.AmmoType1 "EnergyPod"; Weapon.SisterWeapon "Mauler"; Tag "$TAG_MAULER2"; + +WEAPON.BFG; } States diff --git a/wadsrc/static/zscript/actors/strife/weaponmissile.zs b/wadsrc/static/zscript/actors/strife/weaponmissile.zs index 589f75493..051f03ac1 100644 --- a/wadsrc/static/zscript/actors/strife/weaponmissile.zs +++ b/wadsrc/static/zscript/actors/strife/weaponmissile.zs @@ -12,6 +12,7 @@ class MiniMissileLauncher : StrifeWeapon Inventory.Icon "MMSLA0"; Tag "$TAG_MMLAUNCHER"; Inventory.PickupMessage "$TXT_MMLAUNCHER"; + +WEAPON.EXPLOSIVE } States From d9c224439dc00ea288426ffbb4664b499d95dd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 21 Feb 2025 02:17:54 -0300 Subject: [PATCH 022/384] don't allow changing out-ness of parameters in virtual overrides --- src/common/objects/dobjtype.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/common/objects/dobjtype.cpp b/src/common/objects/dobjtype.cpp index 0d1bfdf9a..eac10469a 100644 --- a/src/common/objects/dobjtype.cpp +++ b/src/common/objects/dobjtype.cpp @@ -672,15 +672,36 @@ bool ShouldAllowGameSpecificVirtual(FName name, unsigned index, PType* arg, PTyp int PClass::FindVirtualIndex(FName name, PFunction::Variant *variant, PFunction *parentfunc, bool exactReturnType, bool ignorePointerReadOnly) { auto proto = variant->Proto; + auto &flags = variant->ArgFlags; for (unsigned i = 0; i < Virtuals.Size(); i++) { if (Virtuals[i]->Name == name) { auto vproto = Virtuals[i]->Proto; - if (vproto->ReturnTypes.Size() != proto->ReturnTypes.Size() || + auto &vflags = Virtuals[i]->ArgFlags; + + int n = flags.size(); + + bool flagsOk = true; + + for(int i = 0; i < n; i++) + { + int argA = i >= vflags.size() ? 0 : vflags[i]; + int argB = i >= flags.size() ? 0 : flags[i]; + + bool AisRef = argA & (VARF_Out | VARF_Ref); + bool BisRef = argB & (VARF_Out | VARF_Ref); + + if(AisRef != BisRef) + { + flagsOk = false; + break; + } + } + + if (!flagsOk || vproto->ReturnTypes.Size() != proto->ReturnTypes.Size() || vproto->ArgumentTypes.Size() < proto->ArgumentTypes.Size()) { - continue; // number of parameters does not match, so it's incompatible } bool fail = false; From cb4acf91927834e71167255a299ed2f712811938 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Sat, 22 Feb 2025 12:02:21 -0500 Subject: [PATCH 023/384] - language update --- wadsrc/static/language.0 | 40 +++++++-------- wadsrc/static/language.csv | 80 ++++++++++++++--------------- wadsrc_extra/static/language.csv | 87 +++++++++++++++++--------------- 3 files changed, 107 insertions(+), 100 deletions(-) diff --git a/wadsrc/static/language.0 b/wadsrc/static/language.0 index d1f112184..96ee85a57 100644 --- a/wadsrc/static/language.0 +++ b/wadsrc/static/language.0 @@ -281,7 +281,7 @@ No files,MNU_NOFILES,,,,Žádné soubory,Ingen filer,Keine Dateien,Καθ΄λο Off,OPTVAL_OFF,,,,Vyp.,Fra,Aus,ενεÏγοποιημένη,MalÅaltita,Desactivado,,Pois,,Isklj.,Ki,Disattivo,オフ,ë”,Uit,Av,Wyłączone,Não,,Pornit,Откл.,ИÑкљ.,Av,Kapalı,Вимк.,Изкл.,Isklj. On,OPTVAL_ON,,,,Zap.,Til,An,άπενεÏγοποιημένη,Åœaltita,Activado,,Päällä,,Uklj.,Be,Attivo,オン,켬,Aan,PÃ¥,Włączone,Sim,,Oprit,Вкл.,Укљ.,PÃ¥,Açık,Увім.,Вкл.,Uklj. Auto,OPTVAL_AUTO,,,,,Automatisk,,Αυτό,AÅ­tomata,Automático,,Automaattinen,,Automatski,Automatikus,Automatico,自動,ìžë™,,,Automatycznie,Automático,,,Ðвто,ÐутоматÑки,Automatiskt,Otomatik,Ðвто,Ðвтоматично,AutomatiÄno -Options,OPTMNU_TITLE,,,,Možnosti,Indstillinger,Optionen,Ρυθμίσεις,Agordo,Opciones,,Asetukset,,Postavke,Beállítások,Opzioni,オプション,설정,Opties,Alternativer,Opcje,Opções,,OpÈ›iuni,ÐаÑтройки,Подешавања,Alternativ,Seçenekler,ÐалаштуваннÑ,ÐаÑтройки,PodeÅ¡avanja +Options,OPTMNU_TITLE,,,,Možnosti,Indstillinger,Optionen,Ρυθμίσεις,Agordoj,Opciones,,Asetukset,,Postavke,Beállítások,Opzioni,オプション,설정,Opties,Alternativer,Opcje,Opções,,OpÈ›iuni,ÐаÑтройки,Подешавања,Alternativ,Seçenekler,ÐалаштуваннÑ,ÐаÑтройки,PodeÅ¡avanja Customize Controls,OPTMNU_CONTROLS,,,,Ovládání,Tilpas kontrolelementer,Steuerung einstellen,ΠÏοσαÏμογή χειÏιστηÏίων,Adapti regilojn,Personalizar controles,,Ohjausasetukset,Modifier les Contrôles,Prilagodi kontrole,Irányítás testreszabása,Personalizza i controlli,キーé…置変更,ì¡°ìž‘ ì‚¬ìš©ìž ì§€ì •,Instellen van de controle,Tilpass kontroller,Ustaw Klawisze,Personalizar comandos,Configurar Controlos,Personalizare Setări Control,Управление,Контроле,Anpassa kontrollerna,Kontrolleri ÖzelleÅŸtir,КеруваннÑ,Управление,Prilagodite Upravljanje Mouse Options,OPTMNU_MOUSE,,,,MyÅ¡,Indstillinger for mus,Mauseinstellungen,Ρυθμίσεις ποντικίου,Agordo de muso,Opciones de ratón,,Hiiriasetukset,Options de la Souris,Postavke miÅ¡a,Egér beállítások,Opzioni Mouse,マウス オプション,마우스 설정,Muis opties,Alternativer for mus,Opcje Myszki,Opções de mouse,Opções do rato,Setări Mouse,Мышь,Миш,Alternativ för musen,Fare Seçenekleri,Миш,Мишка,Postavke MiÅ¡a Controller Options,OPTMNU_JOYSTICK,,,,OvladaÄ,Indstillinger for controller,Joystickeinstellungen,Επιλογές ελεγκτή,Agordo de regilo,Opciones de mando,,Peliohjainasetukset,Options de la Manette,Postavke kontrolera,JátékvezérlÅ‘ beállítások,Opzioni Joystick,コントローラーオプション,ì¡°ì´ìŠ¤í‹± 설정,Controller opties,Kontrollalternativer,Opcje Kontrolera,Opções de controle,,Setări Controller,Контроллер,Контролер,Alternativ för styrenhet,Kontrolör Seçenekleri,Контролер,Контролер,Postavke Džojstika @@ -882,27 +882,27 @@ Coronas,GLPREFMNU_CORONAS,,,,Záře,Coronas,,,Lum-ampoloj,Focos de luz,,Coronat, Appearance,DSPLYMNU_APPEARANCE,,,,Vzhled,Udseende,Spieldarstellung,,Aspekto,Apariencia,,Ulkonäkö,Apparence,Izgled,Megjelenés,Aspetto,アピアランス,외형,Uiterlijk,Utseende,WyglÄ…d,Aparência,,Aspect,ВнешноÑть,Изглед,Utseende,Görünüş,Зовнішній виглÑд,,Izgled Advanced Display Options,DSPLYMNU_ADVANCED,,,,Grafika (pokroÄilé),Avancerede visningsindstillinger,Erweiterte Anzeigeoptionen,,Altnivelaj ekran-agordoj,Opciones avanzadas de visualización,,Näytön lisäasetukset,Options d'affichage avancées,Napredne postavke zaslona,Speciális megjelenítési beállítások,Opzioni di visualizzazione avanzate,高度ãªãƒ‡ã‚£ã‚¹ãƒ—レイオプション,고급 ë””ìŠ¤í”Œë ˆì´ ì˜µì…˜,Geavanceerde Weergave Opties,Avanserte visningsalternativer,Zaawansowane Opcje WyÅ›wietlania,Opções de vídeo avançadas,,OpÈ›iuni avansate de afiÈ™are,РаÑширенные наÑтройки Ñкрана,Ðапредне опције приказа,Avancerade visningsalternativ,GeliÅŸmiÅŸ Görüntüleme Seçenekleri,Додаткові параметри відображеннÑ,,Napredne postavke zaslona ,,IWAD/Game picker,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Select which game file to run.,PICKER_SELECT,,,,"Vyberte, jaký herní soubor spustit.","Vælg, hvilket spil du vil spille",Bitte wähle ein Spiel aus.,,Elektu kiun ludodosieron ruli.,,,Valitse suoritettava pelitiedosto.,Sélectionner le jeu à jouer,Odaberi koju igru želiÅ¡ pokrenuti.,"Válassza ki, hogy melyik játékfájlt futtassa.",Selezionare il file di gioco da eseguire.,実行ã™ã‚‹ã‚²ãƒ¼ãƒ ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¾ã™ã€‚,실행할 게임 파ì¼ì„ ì„ íƒí•©ë‹ˆë‹¤.,Selecteer welk spel je wilt spelen,Velg hvilket spill du vil spille,"Wybierz, który plik gry uruchomić.",Selecione qual arquivo de jogo rodar.,,SelectaÈ›i ce fiÈ™ier de joc să rulaÈ›i.,Выбор файла игры Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка.,Изаберите коју датотеку игре желите да покренете.,Välj vilket spel du vill spela,Hangi oyunu oynayacağınızı seçin,Виберіть файл гри Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку.,,Odaberite koju igru želiÅ¡ pokrenuti. -Play Game,PICKER_PLAY,,,,Hrát hru,Start spil,Spielen,,Ludi ludon,,,Pelin pelaaminen,Démarrer le jeu,Zaigraj igru,Játék lejátszása,Esegui gioco,プレイ,게임 플레ì´,Spel starten,Start spill,Graj,Jogar,,Joacă jocul,Играть,Играј игру,Starta spel,Oyunu BaÅŸlat,ЗапуÑтити гру,,Zaigrajte igru -Exit,PICKER_EXIT,,,,Odejít,Afslut,Verlassen,,Eliri,,,Poistu,Quitter,Izlaz,Kilépés,Esci,終了,종료,Verlaten,Avslutt,Wyjdź,Sair,,IeÈ™ire,Выход,Изађи,Avsluta,Çıkış,Вихід,,Izlaz -General,PICKER_GENERAL,,,,Obecné,Generelt,Allgemein,,Äœenerala,,,Yleistä,Général,Općenito,Ãltalános,Generale,一般,ì¼ë°˜,Algemeen,Generelt,Ogólne,Geral,,,Общее,Генерал,Allmänt,Genel,Загальні,,Općenito -Extra Graphics,PICKER_EXTRA,,,,Grafické doplňky,Ekstra grafik,Extragrafiken,,Ekstra grafiko,,,Extra Graphics,Graphiques supplémentaires,Dodatna grafika,Extra grafika,Grafica extra,追加グラフィックス,추가 그래픽,Extra afbeeldingen,Ekstra grafikk,Ekstra grafiki,Gráficos extras,,Grafică suplimentară,Доп. графика,Ектра ГрапхицÑ,Extra grafik,Ekstra Grafikler,Додаткова графіка,,Dodatne grafike -Fullscreen,PICKER_FULLSCREEN,,,,PÅ™es celou obrazovku,Fuld skærm,Vollbild,,Plena ekrano,,,Koko näyttö,Plein écran,Puni zaslon,Teljes képernyÅ‘,Schermo intero,フルスクリーン,ì „ì²´ 화면,Volledig scherm,Fullskjerm,PeÅ‚ny ekran,Tela cheia,,Ecran complet,Полный Ñкран,Цео екран,Fullskärm,Tam Ekran,Повноекранний режим,,Puni zaslon -Disable autoload,PICKER_NOAUTOLOAD,,,,Zakázat autoload,Deaktiver autoload,Autoload deaktivieren,,Malvalidigi aÅ­toÅargon,,,Poista automaattinen lataus käytöstä,Désactiver le chargement automatique,Onemogući automatsko uÄitavanje,Automatikus betöltés kikapcsolása,Disabilita il caricamento automatico,オートロード無効,ìžë™ 로드 비활성화,Autoload uitschakelen,Deaktiver autolading,Wyłącz auto-Å‚adowanie,Desativar autocarregam.,,DezactivaÈ›i încărcarea automată,Откл. автозагрузку,Онемогући аутоматÑко учитавање,Inaktivera autoload,Otomatik yükleme yok,Вимкнути автозавантаженнÑ,,Onemogućite automatsko uÄitavanje -Don't ask me again,PICKER_DONTASK,,,,Již se neptat,Spørg mig ikke igen,Nicht nochmal fragen,,Ne demandu min denove,,,Älä kysy uudestaan,Ne me demandez plus rien,ViÅ¡e me ne pitaj,Ne kérdezz újra,Non chiedermelo più,å†åº¦èžã‹ãªã„,다시 묻지 마세요,Vraag me niet opnieuw,Ikke spør meg igjen,Nie pytaj ponownie,Não perguntar de novo,,Nu mă mai întrebaÈ›i din nou,Ðе Ñпрашивать Ñнова,Ðе питај ме поново,FrÃ¥ga mig inte igen,Bir daha sorma.,Ðе запитуйте мене більше,,ViÅ¡e me ne pitajte -Lights,PICKER_LIGHTS,,,,SvÄ›tla,Lys,Lichtdefinitionen,,Lumoj,,,Valot,Lumières,Svjetla,Fények,Luci,ライト,조명,Verlichting,Lysdefinisjoner,OÅ›wietlenie,Luzes,,Lumini,ОÑвещение,Светла,Definitioner av ljus,Işık tanımları,ОÑвітленнÑ,,Svjetla -Brightmaps,PICKER_BRIGHTMAPS,,,,,,,,Helomapoj,,,Brightmaps,Cartes lumineuses,Svijetle karte,Brightmaps,Mappe luminose,ブライトマップ,브ë¼ì´íŠ¸ë§µ,Heldermaps,Lyskart,Mapowanie Å›wiateÅ‚,,,,Карты оÑвещениÑ,БригхтмапÑ,Ljuskartor,Brightmaps,ЯÑкраві карти,,Svijetiokartice -Widescreen,PICKER_WIDESCREEN,,,,,,Breitbildunterstützung,,LarÄekrana,,,Laajakuva,Écran large,Å iroki zaslon,SzélesképernyÅ‘,Widescreen,ワイドスクリーン,와ì´ë“œìФí¬ë¦°,Breedbeeld,Bredskjerm,Szeroki ekran,,,,Широкий Ñкран,Широки екран,Bredbildsskärm,GeniÅŸ Ekran,Широкоформатний,,Å iroki zaslon -Additional Parameters:,PICKER_ADDPARM,,,,DodateÄné parametry:,Yderligere parametre:,Zusätzliche Parameter,,Aldonaj parametroj,,,Lisäparametrit:,Paramètres supplémentaires :,Dodatni parametri:,További paraméterek:,Parametri aggiuntivi:,追加パラメータ,환ì˜í•©ë‹ˆë‹¤: %s!,Extra parameters:,Ytterligere parametere:,Dodatkowe parametry:,Parâmetros adicionais:,,Parametrii suplimentari:,Доп. параметры:,Додатни параметри:,Ytterligare parametrar:,Ek Parametreler:,Додаткові параметри:,,Dodatni parametri: -Welcome to %s!,PICKER_WELCOME,,,,Vítejte v enginu %s!,Velkommen til %s!,Willkommen bei %s!,,Bonvenon en %s!,,,Tervetuloa %s!,Bienvenue à %s !,DobrodoÅ¡ao/la u %s!,Üdvözöljük a %s!,Benvenuti a %s!,よã†ã“ã: %s!,ì— ì˜¤ì‹  ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤!,Welkom bij %s!,Velkommen til %s!,Witaj w %s!,Bem-vindo(a) ao %s!,,Bine aÈ›i venit la %s!,Добро пожаловать в %s!,Добродошли у %s!,Välkommen till %s!,S'ye hoÅŸ geldiniz!,ЛаÑкаво проÑимо до %s!,,DobrodoÅ¡ao/la u %s! -Version %s,PICKER_VERSION,,,,Verze %s,,,,Versio %s,,,Versio %s,,Verzija %s,Verzió %s,Versione %s,ãƒãƒ¼ã‚¸ãƒ§ãƒ³ %s,버전 %s,Versie %s,Versjon %s,Wersja %s,Versão %s,,Versiunea %s,ВерÑÐ¸Ñ %s,Верзија %s,,Sürüm %s,ВерÑÑ–Ñ %s,,Verzija %s +Select which game file to run.,PICKER_SELECT,,,,"Vyberte, jaký herní soubor spustit.","Vælg, hvilket spil du vil spille",Bitte wähle ein Spiel aus.,,Elektu lanĉotan ludon.,Elige un juego para ejecutar.,,Valitse suoritettava pelitiedosto.,Sélectionner le jeu à jouer,Odaberi koju igru želiÅ¡ pokrenuti.,"Válassza ki, hogy melyik játékfájlt futtassa.",Selezionare il file di gioco da eseguire.,実行ã™ã‚‹ã‚²ãƒ¼ãƒ ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¾ã™ã€‚,실행할 게임 파ì¼ì„ ì„ íƒí•©ë‹ˆë‹¤.,Selecteer welk spel je wilt spelen,Velg hvilket spill du vil spille,"Wybierz, który plik gry uruchomić.",Selecione qual arquivo de jogo rodar.,,SelectaÈ›i ce fiÈ™ier de joc să rulaÈ›i.,Выбор файла игры Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка.,Изаберите коју датотеку игре желите да покренете.,Välj vilket spel du vill spela,Hangi oyunu oynayacağınızı seçin,Виберіть файл гри Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку.,,Odaberite koju igru želiÅ¡ pokrenuti. +Play Game,PICKER_PLAY,,,,Hrát hru,Start spil,Spielen,,Ekludi,Jugar,,Pelin pelaaminen,Démarrer le jeu,Zaigraj igru,Játék lejátszása,Esegui gioco,プレイ,게임 플레ì´,Spel starten,Start spill,Graj,Jogar,,Joacă jocul,Играть,Играј игру,Starta spel,Oyunu BaÅŸlat,ЗапуÑтити гру,,Zaigrajte igru +Exit,PICKER_EXIT,,,,Odejít,Afslut,Verlassen,,Mallanĉi,Salir,,Poistu,Quitter,Izlaz,Kilépés,Esci,終了,종료,Verlaten,Avslutt,Wyjdź,Sair,,IeÈ™ire,Выход,Изађи,Avsluta,Çıkış,Вихід,,Izlaz +General,PICKER_GENERAL,,,,Obecné,Generelt,Allgemein,,Äœeneralaj,Generales,,Yleistä,Général,Općenito,Ãltalános,Generale,一般,ì¼ë°˜,Algemeen,Generelt,Ogólne,Geral,,,Общее,Генерал,Allmänt,Genel,Загальні,,Općenito +Extra Graphics,PICKER_EXTRA,,,,Grafické doplňky,Ekstra grafik,Extragrafiken,,Ekstra grafiko,Gráficos extras,,Extra Graphics,Graphiques supplémentaires,Dodatna grafika,Extra grafika,Grafica extra,追加グラフィックス,추가 그래픽,Extra afbeeldingen,Ekstra grafikk,Ekstra grafiki,Gráficos extras,,Grafică suplimentară,Доп. графика,Ектра ГрапхицÑ,Extra grafik,Ekstra Grafikler,Додаткова графіка,,Dodatne grafike +Fullscreen,PICKER_FULLSCREEN,,,,PÅ™es celou obrazovku,Fuld skærm,Vollbild,,Plena ekrano,Pantalla completa,,Koko näyttö,Plein écran,Puni zaslon,Teljes képernyÅ‘,Schermo intero,フルスクリーン,ì „ì²´ 화면,Volledig scherm,Fullskjerm,PeÅ‚ny ekran,Tela cheia,,Ecran complet,Полный Ñкран,Цео екран,Fullskärm,Tam Ekran,Повноекранний режим,,Puni zaslon +Disable autoload,PICKER_NOAUTOLOAD,,,,Zakázat autoload,Deaktiver autoload,Autoload deaktivieren,,MalÅalti aÅ­tomatan Åargon,Desactivar autocargado,,Poista automaattinen lataus käytöstä,Désactiver le chargement automatique,Onemogući automatsko uÄitavanje,Automatikus betöltés kikapcsolása,Disabilita il caricamento automatico,オートロード無効,ìžë™ 로드 비활성화,Autoload uitschakelen,Deaktiver autolading,Wyłącz auto-Å‚adowanie,Desativar autocarregam.,,DezactivaÈ›i încărcarea automată,Откл. автозагрузку,Онемогући аутоматÑко учитавање,Inaktivera autoload,Otomatik yükleme yok,Вимкнути автозавантаженнÑ,,Onemogućite automatsko uÄitavanje +Don't ask me again,PICKER_DONTASK,,,,Již se neptat,Spørg mig ikke igen,Nicht nochmal fragen,,Ne demandi denove,No preguntar de nuevo,,Älä kysy uudestaan,Ne me demandez plus rien,ViÅ¡e me ne pitaj,Ne kérdezz újra,Non chiedermelo più,å†åº¦èžã‹ãªã„,다시 묻지 마세요,Vraag me niet opnieuw,Ikke spør meg igjen,Nie pytaj ponownie,Não perguntar de novo,,Nu mă mai întrebaÈ›i din nou,Ðе Ñпрашивать Ñнова,Ðе питај ме поново,FrÃ¥ga mig inte igen,Bir daha sorma.,Ðе запитуйте мене більше,,ViÅ¡e me ne pitajte +Lights,PICKER_LIGHTS,,,,SvÄ›tla,Lys,Lichtdefinitionen,,Lumoj,Luces,,Valot,Lumières,Svjetla,Fények,Luci,ライト,조명,Verlichting,Lysdefinisjoner,OÅ›wietlenie,Luzes,,Lumini,ОÑвещение,Светла,Definitioner av ljus,Işık tanımları,ОÑвітленнÑ,,Svjetla +Brightmaps,PICKER_BRIGHTMAPS,,,,,,,,Helomapoj,Mapas luminosos,,Brightmaps,Cartes lumineuses,Svijetle karte,Brightmaps,Mappe luminose,ブライトマップ,브ë¼ì´íŠ¸ë§µ,Heldermaps,Lyskart,Mapowanie Å›wiateÅ‚,,,,Карты оÑвещениÑ,БригхтмапÑ,Ljuskartor,Brightmaps,ЯÑкраві карти,,Svijetiokartice +Widescreen,PICKER_WIDESCREEN,,,,,,Breitbildunterstützung,,LarÄa ekrano,Pantalla ancha,,Laajakuva,Écran large,Å iroki zaslon,SzélesképernyÅ‘,Widescreen,ワイドスクリーン,와ì´ë“œìФí¬ë¦°,Breedbeeld,Bredskjerm,Szeroki ekran,,,,Широкий Ñкран,Широки екран,Bredbildsskärm,GeniÅŸ Ekran,Широкоформатний,,Å iroki zaslon +Additional Parameters:,PICKER_ADDPARM,,,,DodateÄné parametry:,Yderligere parametre:,Zusätzliche Parameter,,Aldonaj parametroj,Parámetros adicionales:,,Lisäparametrit:,Paramètres supplémentaires :,Dodatni parametri:,További paraméterek:,Parametri aggiuntivi:,追加パラメータ,환ì˜í•©ë‹ˆë‹¤: %s!,Extra parameters:,Ytterligere parametere:,Dodatkowe parametry:,Parâmetros adicionais:,,Parametrii suplimentari:,Доп. параметры:,Додатни параметри:,Ytterligare parametrar:,Ek Parametreler:,Додаткові параметри:,,Dodatni parametri: +Welcome to %s!,PICKER_WELCOME,,,,Vítejte v enginu %s!,Velkommen til %s!,Willkommen bei %s!,,Bonvenon en %s!,¡Bienvenidos a %s!,,Tervetuloa %s!,Bienvenue à %s !,DobrodoÅ¡ao/la u %s!,Üdvözöljük a %s!,Benvenuti a %s!,よã†ã“ã: %s!,ì— ì˜¤ì‹  ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤!,Welkom bij %s!,Velkommen til %s!,Witaj w %s!,Bem-vindo(a) ao %s!,,Bine aÈ›i venit la %s!,Добро пожаловать в %s!,Добродошли у %s!,Välkommen till %s!,S'ye hoÅŸ geldiniz!,ЛаÑкаво проÑимо до %s!,,DobrodoÅ¡ao/la u %s! +Version %s,PICKER_VERSION,,,,Verze %s,,,,Versio %s,Versión %s,,Versio %s,,Verzija %s,Verzió %s,Versione %s,ãƒãƒ¼ã‚¸ãƒ§ãƒ³ %s,버전 %s,Versie %s,Versjon %s,Wersja %s,Versão %s,,Versiunea %s,ВерÑÐ¸Ñ %s,Верзија %s,,Sürüm %s,ВерÑÑ–Ñ %s,,Verzija %s Rendering API,PICKER_PREFERBACKEND,,,,Renderovací API,,Render API,,API de bildigado,API de renderizado,,Renderöinti API,API de rendu,Api za renderiranje,RenderelÅ‘ API,API di rendering,優先レンダリングAPI,기본ì ì¸ API ëžœë”ë§,,,API renderowania,API de renderização,,API Video Preferat,API обработки,ÐПИ приказивања,API för rendering,,API Ð´Ð»Ñ Ð²Ñ–Ð·ÑƒÐ°Ð»Ñ–Ð·Ð°Ñ†Ñ–Ñ—,,API za iscrtivanje Game,PICKER_TAB_PLAY,,,,Hra,Spil,Spiel,,Ludo,Juego,,Peli,Jeu,Igra,Játék,Gioco,ゲーム,게임,Spel,Spill,Gra,Jogo,,Joc,Игра,Игра,Spel,Oyun,Гра,,Igra -Enable this controller,JOYMNU_JOYENABLE,Option to enable or disable individual controllers/joysticks when configuring it in the Controller Options menu,,,Povolit tento ovladaÄ,Aktivér denne controller,Diesen Controller aktivieren,,Åœalti ĉi tiun ludregilon,Habilitar este controlador,,Aktivoi tämä ohjain,Activer ce contrôleur,Omogući ovaj kontroler,Engedélyezze ezt a vezérlÅ‘t,Attiva questo controller,ã“ã®ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ©ãƒ¼ã‚’有効ã«ã™ã‚‹,ì´ ì»¨íŠ¸ë¡¤ëŸ¬ 활성화,Deze controller inschakelen,Aktiver denne kontrolleren,Włącz ten kontroler,Habilitar este controle,,ActivaÈ›i acest controler,Включить Ñтот контроллер,Омогућите овај контролер,Aktivera denna styrenhet,Bu denetleyiciyi etkinleÅŸtirin,Увімкніть цей контролер,,Omogućite ovaj džojstik +Enable this controller,JOYMNU_JOYENABLE,Option to enable or disable individual controllers/joysticks when configuring it in the Controller Options menu,,,Povolit tento ovladaÄ,Aktivér denne controller,Diesen Controller aktivieren,,Åœalti ĉi tiun ludregilon,Activar este controlador,,Aktivoi tämä ohjain,Activer ce contrôleur,Omogući ovaj kontroler,Engedélyezze ezt a vezérlÅ‘t,Attiva questo controller,ã“ã®ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ©ãƒ¼ã‚’有効ã«ã™ã‚‹,ì´ ì»¨íŠ¸ë¡¤ëŸ¬ 활성화,Deze controller inschakelen,Aktiver denne kontrolleren,Włącz ten kontroler,Habilitar este controle,,ActivaÈ›i acest controler,Включить Ñтот контроллер,Омогућите овај контролер,Aktivera denna styrenhet,Bu denetleyiciyi etkinleÅŸtirin,Увімкніть цей контролер,,Omogućite ovaj džojstik Open Main Menu,CNTRLMNU_OPEN_MAIN,,,,Otevřít hlavní menu,Ã…bn hovedmenuen,Hauptmenü öffnen,,Malfermi ĉefan menuon,Abrir el menú principal,,Avaa päävalikko,Ouvrir le menu principal,Otvori glavni izbornik,FÅ‘menü megnyitása,Apri il menu principale,メインメニューを開ã,ë©”ì¸ ë©”ë‰´ 열기,Hoofdmenu openen,Ã…pne hovedmenyen,Otwórz meny główne,Abrir menu principal,,DeschideÈ›i meniul principal,Открыть главное меню,Отворите главни мени,Öppna huvudmenyn,Ana Menüyü Aç,Відкрити головне меню,,Otvorite glavni izbornik ,,New content,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Module replayer,MODMNU_REPLAYER,,,,Nastavení pÅ™ehrávaÄe modulů,Modulafspiller,Modul-Spieler,,Legilo de moduloj,Reproductor de módulos,,,Lecteur de module,PonavljaÄ modula,,Module replayer,モジュール リプレイヤー,,Module replayer,Modulavspiller,,Reprodutor de módulos,,Redare a modulelor,Модуль-повторитель,,ModulÃ¥terspelare,,Module replayer,,Modula za snimanje razigravanja -Allow Background Controller Input,JOYMNU_JOYENABLEINBACKGROUND,"This is enabled per-controller, it means to enable ""this"" controller in the background when GZDoom is not in focus.",,,,,Controller im Hintergrund aktivieren,,Permesi enigon de ludregilo fone,,,,,Dopusti unos kontrolera u pozadini,,,,,,,,Permitir controle em segundo plano,,,Разрешить контроллер на фоне,,,,,, -Save Game Sorting Order,MISCMNU_SAVEORDER,Order to sort the list of saved games in the save and load game menu. This used to only be alphabetical.,,,,,Gespeicherte Spiele sortieren nach,,Ordiga ordo de konservitaj ludoj,,,,,Redoslijed sortiranja spremljenih igara,,,,,,,,Ordem de jogos salvos,,,ПорÑдок Ñортировки Ñохранений,,,,,, -Alphabetical,OPTVAL_ALPHABETICAL,,,,,,Alphabetisch,,Alfabeta,,,,,Abecedni,,,,,,,,Alfabética,,,По алфавиту,,,,,, -Last Saved Time,OPTVAL_LASTSAVEDTIME,,,,,,Zuletzt gespeichert,,Laste konservita tempo,,,,,Posljednje spremljeno vrijeme,,,,,,,,Mais recente,,,Ð’Ñ€ÐµÐ¼Ñ Ð¿Ð¾Ñледнего ÑохранениÑ,,,,,, \ No newline at end of file +Allow Background Controller Input,JOYMNU_JOYENABLEINBACKGROUND,"This is enabled per-controller, it means to enable ""this"" controller in the background when GZDoom is not in focus.",,,,,Controller im Hintergrund aktivieren,,Permesi enigon de ludregilo fone,Permitir entrada del controlador en segundo plano,,,,Dopusti unos kontrolera u pozadini,,,,,,,,Permitir controle em segundo plano,,,Разрешить контроллер на фоне,,,,,, +Save Game Sorting Order,MISCMNU_SAVEORDER,Order to sort the list of saved games in the save and load game menu. This used to only be alphabetical.,,,,,Gespeicherte Spiele sortieren nach,,Ordo de konservitaj ludadoj,Orden de las partidas guardadas,,,,Redoslijed sortiranja spremljenih igara,,,,,,,,Ordem de jogos salvos,,,ПорÑдок Ñортировки Ñохранений,,,,,, +Alphabetical,OPTVAL_ALPHABETICAL,,,,,,Alphabetisch,,Alfabeta,Alfabético,,,,Abecedni,,,,,,,,Alfabética,,,По алфавиту,,,,,, +Last Saved Time,OPTVAL_LASTSAVEDTIME,,,,,,Zuletzt gespeichert,,Laste konservita tempo,Guardado más reciente,,,,Posljednje spremljeno vrijeme,,,,,,,,Mais recente,,,Ð’Ñ€ÐµÐ¼Ñ Ð¿Ð¾Ñледнего ÑохранениÑ,,,,,, \ No newline at end of file diff --git a/wadsrc/static/language.csv b/wadsrc/static/language.csv index 9871811ce..2e9cb0746 100644 --- a/wadsrc/static/language.csv +++ b/wadsrc/static/language.csv @@ -1232,7 +1232,7 @@ Shotgun Guy,CC_SHOTGUN,a.k.a. (former human) sergeant,,,,Seržant,Sergent,Schüt Heavy Weapon Dude,CC_HEAVY,a.k.a. former commando,,,,KulometÄík,,MG-Schütze,ΤÏπος με βαÏί όπλο,Pezarmilulo,Ametrallador,,Raskasaseäijä,Type au Gros Flingue,,Kommandós,Zombie Commando,ヘビーウェãƒãƒ³ã‚¾ãƒ³ãƒ“,헤비 ì›¨í° ë“€ë“œ,Zwarewapenskerel,Maskingeværskyt,Ciężkozbrojny Zombie,Comando Possuído,Gajo das Armas Pesadas,Mitralior,Зомби-пулемётчик,Митраљезац,Tunga vapen kille,Ağır Silah Dostum,Кулеметник Imp,CC_IMP,,,,,ÄŒert,,Kobold,Διάβολος,Diableto,,,Piru,Diablotin,,Tűzkobold,,インプ,임프,,,Diablik,Diabrete,,Drac,Имп,Враг,,İmp,Імп Demon,CC_DEMON,,,,,Démon,Dæmon,Dämon,Δαίμονας,Demono,Demonio,,Demoni,Démon,,Démon,Pinky,デーモン,ë°ëª¬,Demoon,,,Demônio,,,Демон,Демон,,İblis,Демон -Lost Soul,CC_LOST,,,,,Ztracená duÅ¡e,Fortabt sjæl,Verlorene Seele,Χαμένη Ξυχή,Perdita Animo,Alma Errante,,Kadonnut sielu,Ame Perdue,,Kóbor lélek,Anima Errante,ロストソウル,로스트 소울,Verloren ziel,Fortapt sjel,Zaginiona Dusza,Alma Perdida,Alma Penada,Suflet Pierdut,ПотерÑÐ½Ð½Ð°Ñ Ð´ÑƒÑˆÐ°,Изгубљена душа,Förlorad själ,Kayıp Ruh,Пропаща душа +Lost Soul,CC_LOST,,,,,Ztracená duÅ¡e,Fortabt sjæl,Verlorene Seele,Χαμένη Ξυχή,Vaga animo,Alma Errante,,Kadonnut sielu,Ame Perdue,,Kóbor lélek,Anima Errante,ロストソウル,로스트 소울,Verloren ziel,Fortapt sjel,Zaginiona Dusza,Alma Perdida,Alma Penada,Suflet Pierdut,ПотерÑÐ½Ð½Ð°Ñ Ð´ÑƒÑˆÐ°,Изгубљена душа,Förlorad själ,Kayıp Ruh,Пропаща душа Cacodemon,CC_CACO,,,,,Kakodémon,Cacodæmon,Cacodämon,Κακοδαίμονας,Kakodemono,Cacodemonio,,Kakodemoni,Cacodémon,,Kakodémon,Cacodemone,カコデーモン,ì¹´ì½”ë°ëª¬,Cacodemoon,Kacodemon,Kakodemon,Cacodemônio,,Cacodemon,Какодемон,Какодемон,Cacodemon,Cacodemon,Какодемон Hell Knight,CC_HELL,,,,,Pekelný rytíř,Helvedesridder,Höllenritter,Ιππότης της Κόλασης,Inferkavaliro,Caballero del Infierno,,Hornanritari,Chevalier Infernal,,Pokollovag,Cavaliere Infernale,ヘルナイト,í—¬ 나ì´íЏ,Helleridder,Helvetesridder,Rycerz PiekÅ‚a,Cavaleiro Infernal,Cavaleiro Infernal,Cavaler al Infernului,Рыцарь ада,Витез пакла,Helvete riddare,Cehennem Şövalyesi,Лицар Пекла Baron of Hell,CC_BARON,,,,,Baron pekel,Helvedesbaron,Höllenbaron,ΒαÏώνος της Κολασης,Barono de Infero,Barón del Infierno,,Hornanparoni,Baron des Enfers,,Pokolbáró,Barone Infernale,ãƒãƒ­ãƒ³ã‚ªãƒ–ヘル,바론 오브 í—¬,Baron des Hels,Helvetesbaron,Baron PiekÅ‚a,Barão do Inferno,,Baron al Infernului,Барон ада,Барон пакла,Helvetets baron,Cehennem Baronu,Барон Пекла @@ -1291,7 +1291,7 @@ Energy Cells,AMMO_CELLS,,,,Energetske ćelije,Energetické Älánky,Energiceller %o couldn't evade a revenant's fireball.,OB_UNDEAD,,,,,%o se nevyhnul@[ao_cs] umrlÄí ohnivé kouli.,%o kunne ikke undvige en ildkugle fra en hævnen.,%o konnte dem Feuerball des Wiederauferstandenen nicht ausweichen.,@[art_gr] %o δέν μποÏοÏσε να αποφείγει τον Ï€ÏÏαυλο ενός Εκδικητή.,%o ne povis eviti fajrobulon de renaskito.,A %o l@[ao_esp] ha hecho reventar un Revenant.,A %o l@[ao_esp] hizo reventar un Revenant.,%o ei kyennyt väistämään henkiinpalanneen tulipalloa.,%o n'a pas réussi à esquiver un missile de revenant.,,%o nem tudta kivédeni egy kísértet tűzgolyóját.,%o non ha potuto evitare il missile di un revenant.,%o ã¯ãƒ¬ãƒãƒŠãƒ³ãƒˆã®ãƒ•ァイアボールを回é¿ã§ããªã‹ã£ãŸã€‚,%o ì€(는) ë ˆë²„ë„ŒíŠ¸ì˜ í™”ì—¼êµ¬ë¥¼ 피하지 못했다.,%o kon de vuurbal van een wraakzuchtige niet ontwijken.,%o klarte ikke Ã¥ unngÃ¥ en gjengangers ildkule.,%o nie udaje siÄ™ uniknąć pocisku od zjawy.,%o não conseguiu desviar do míssil do insepulto.,%o não conseguiu desviar do míssil do renascido.,%o n-a putut scăpa de mingea de foc a nemortului.,Игрок %o не уÑпел увернутьÑÑ Ð¾Ñ‚ ÑнарÑда ревенанта.,%o није мог@[ao_2_sr] да избегне повратникову пламену куглу.,%o kunde inte undvika en Ã¥terfödds eldklot.,%o bir iskeletin ateÅŸ topundan kaçamadı.,СнарÑд ревенанта діÑтав %o. %o was squashed by a mancubus.,OB_FATSO,,,,,%o byl@[ao_cs] rozmaÄkán@[ao_cs] mankubem.,%o blev knust af en mankubus.,%o wurde von einem Mancubus plattgemacht.,@[art_gr] %o λιόθηκε απο έναν ΧοντÏÏŒ.,%o estis dispremita de mankubo.,A %o l@[ao_esp] ha pulverizado un Mancubus.,A %o l@[ao_esp] pulverizó un Mancubus.,%o joutui mankubuksen liiskaamaksi.,%o s'est fait@[e_fr] aplatir par un Mancube.,,%o-t szétlapította egy mankubusz.,%o è stato disintegrato da un mancubus.,%o ã¯ãƒžãƒ³ã‚­ãƒ¥ãƒã‚¹ã«æ½°ã•れãŸã€‚,%o ì€(는) 맨í버스ì—게 ì§“ì´ê²¨ì¡Œë‹¤.,%o werd verpletterd door een mancubus.,%o ble knust av en mancubus.,%o zostaÅ‚@[ao_pl] zmiażdżon@[adj_pl] przez antropokuba.,Um mancubus esmagou %o.,,%o a fost strivit de un mancubus.,Игрока %o раздавил манкубуÑ.,Играча %o је изгњечио манкубуÑ.,%o blev krossad av en mancubus.,%o bir mancubus tarafından ezildi.,%o розчавлено манкубуÑом. %o was perforated by a chaingunner.,OB_CHAINGUY,,,,,%o byl@[ao_cs] prostÅ™elen@[ao_cs] kulometÄíkem.,%o blev perforeret af en chaingunner.,%o wurde von einem MG-Schützen perforiert.,@[art_gr] %o εκτελέστικε απο ένα Ζόμπι με ένα πολυβόλο.,%o estis truigadita de maÅinpafilisto.,A %o l@[ao_esp] ha perforado un Ametrallador.,A %o l@[ao_esp] perforó un Ametrallador.,%o joutui konekiväärimiehen rei'ittämäksi.,%o a été perforé@[e_fr] par un mitrailleur.,,%o-t szitává lÅ‘tte egy kommandós.,%o è stato perforato da uno zombie commando.,%o ã¯ãƒã‚§ã‚¤ãƒ³ã‚¬ãƒ³ãƒŠãƒ¼ã«èœ‚ã®å·£ã«ã•れãŸã€‚,%o ì€(는) ì²´ì¸ê±°ë„ˆì— ì˜í•´ ë²Œì§‘ì´ ë다.,%o werd geperforeerd door een zware wapens kerel.,%o ble perforert av en chaingunner.,%o zostaÅ‚@[ao_pl] przedziurawion@[adj_pl] przez ciężkozbrojnego zombie.,Um comando possuído perfurou %o.,%o foi perfurad@[ao_ptb] por um metrelhador.,%o a fost perforat de un mitralior.,Игрока %o продырÑвил пулемётчик.,Играча %o је изрешетао митраљезац.,%o blev perforerad av en tunga vapen kille.,%o bir ağır silah tarafından delindi.,%o отрима@[adj_1_ua] чиÑленні отвори від куль кулеметника. -%o was spooked by a lost soul.,OB_SKULL,,,,,%o se lekl@[ao_cs] ztracené duÅ¡e.,%o blev skræmt af en fortabt sjæl.,%o wurde von einer Verlorenen Seele heimgesucht.,Μια Χαμένη Ξυχή φόβησε @[pro_gr] %o.,%o estis timigita de perdita animo.,%o se ha muerto de miedo con un Alma Errante.,%o se murió de miedo con un Alma Errante.,%o joutui kadonneen sielun säikäyttämäksi.,%o s'est fait@[e_fr] surprendre par une âme perdue.,,%o-t halálra ijesztette egy kóbor lélek.,%o è stato spaventato a morte da un'Anima Errante.,%o ã¯ãƒ­ã‚¹ãƒˆã‚½ã‚¦ãƒ«ã«ãƒ“ビらã•れãŸã€‚,%o ì€(는) 로스트 ì†Œìš¸ì„ ë³´ê³  ê¹œì§ ë†€ëžë‹¤.,%o werd bang gemaakt door een verloren ziel.,%o ble skremt av en fortapt sjel.,%o zostaÅ‚@[ao_pl] przestraszon@[adj_pl] przez zaginionÄ… duszÄ™.,Uma alma penada assustou %o.,%o se assustou com uma alma penada.,%o a fost speriat de un suflet pierdut.,Игрок %o запуган до Ñмерти потерÑнной душой.,%o је уплашен@[adj_1_sr] од изгубљене душе.,%o blev skrämd av en förlorad själ.,%o kayıp bir ruh tarafından korkutuldu.,Заблудла душа переÑтрашила %o. +%o was spooked by a lost soul.,OB_SKULL,,,,,%o se lekl@[ao_cs] ztracené duÅ¡e.,%o blev skræmt af en fortabt sjæl.,%o wurde von einer Verlorenen Seele heimgesucht.,Μια Χαμένη Ξυχή φόβησε @[pro_gr] %o.,%o estis timigita de vaga animo.,%o se ha muerto de miedo con un Alma Errante.,%o se murió de miedo con un Alma Errante.,%o joutui kadonneen sielun säikäyttämäksi.,%o s'est fait@[e_fr] surprendre par une âme perdue.,,%o-t halálra ijesztette egy kóbor lélek.,%o è stato spaventato a morte da un'Anima Errante.,%o ã¯ãƒ­ã‚¹ãƒˆã‚½ã‚¦ãƒ«ã«ãƒ“ビらã•れãŸã€‚,%o ì€(는) 로스트 ì†Œìš¸ì„ ë³´ê³  ê¹œì§ ë†€ëžë‹¤.,%o werd bang gemaakt door een verloren ziel.,%o ble skremt av en fortapt sjel.,%o zostaÅ‚@[ao_pl] przestraszon@[adj_pl] przez zaginionÄ… duszÄ™.,Uma alma penada assustou %o.,%o se assustou com uma alma penada.,%o a fost speriat de un suflet pierdut.,Игрок %o запуган до Ñмерти потерÑнной душой.,%o је уплашен@[adj_1_sr] од изгубљене душе.,%o blev skrämd av en förlorad själ.,%o kayıp bir ruh tarafından korkutuldu.,Заблудла душа переÑтрашила %o. %o was burned by an imp.,OB_IMP,,,,,%o byl@[ao_cs] spálen@[ao_cs] Äertem.,%o blev brændt af en spøgelse.,%o wurde von einem Kobold verbrannt.,@[art_gr] %o κάικε απο έναν Διάβολο.,%o estis bruligita de diableto.,A %o l@[ao_esp] ha calcinado un Imp.,A %o l@[ao_esp] calcinó un Imp.,%o joutui pirun polttamaksi.,%o brûlé@[e_fr] par un diablotin.,,%o-t szénné égette egy tűzkobold.,%o è stato bruciato da un imp.,%o ã¯ã‚¤ãƒ³ãƒ—ã«ç„¼ã‹ã‚ŒãŸã€‚,%o ì€(는) 임프ì—게 불태워졌다.,%o werd verbrand door een imp.,%o ble brent av en imp.,%o zostaÅ‚@[ao_pl] spalon@[adj_pl] przez diablika.,Um diabrete queimou %o.,,%o a fost ars de un demon.,Игрока %o Ñжёг имп.,%o је изгорен@[adj_1_sr] од Ñтране врага.,%o blev bränd av en imp.,%o bir imp tarafından yakıldı.,%o Ñпалено імпом. %o was smitten by a cacodemon.,OB_CACO,,,,,%o byl@[ao_cs] udeÅ™en@[ao_cs] kakodémonem.,%o blev ramt af en cacodæmon.,%o wurde von einem Cacodämonen gequält.,@[art_gr] %o καίκε απο έναν Κακοδαίμονα.,%o estis venkobatita de kakodemono.,A %o l@[ao_esp] ha cascado un Cacodemonio.,A %o l@[ao_esp] cascó un Cacodemonio.,%o joutui kakodemonin iskemäksi.,%o a été terrassé@[e_fr] par un Cacodémon.,,%o-ra lesújtott egy kakodémon.,%o è stato abbattuto da un cacodemone.,%o ã¯ã‚«ã‚³ãƒ‡ãƒ¼ãƒ¢ãƒ³ã«è£ãã‚’å—ã‘ãŸã€‚,%o ì€(는) ì¹´ì½”ë°ëª¬ì—게 엄습 당했다.,%o werd geslagen door een cacodemon.,%o ble slÃ¥tt av en kakodemon.,%o zostaÅ‚@[ao_pl] porażon@[adj_pl] przez kakodemona.,Um cacodemônio golpeou %o.,,%o a fost lovit de un cacodemon.,Игрока %o поразил какодемон.,Играча %o је ударио какодемон.,%o blev förkrossad av en cacodemon.,%o bir caco iblisi tarafından vuruldu.,%o уражено какодемоном. %o was bruised by a Baron of Hell.,OB_BARON,,,,,%o byl@[ao_cs] pomaÄkán@[ao_cs] baronem pekel.,%o blev sÃ¥ret af en helvedesbaron.,%o wurde von einem Höllenbaron geröstet.,@[art_gr] %o γÏατζουνήθηκε απο ένα ΒαÏώνο της Κόλασης.,%o estis kontuzita de Barono de Infero.,A %o l@[ao_esp] ha magullado un Barón del Infierno.,A %o l@[ao_esp] magulló un Barón del Infierno.,%o joutui hornanparonin ruhjomaksi.,%o a été démoli@[e_fr] par un Baron des Enfers.,,%o-t összezúzta egy pokolbáró.,%o è stato scorticato da un Barone Infernale.,%o ã¯ãƒãƒ­ãƒ³ã‚ªãƒ–ヘルã«ç—›ã‚ã¤ã‘られãŸã€‚,%o ì€(는) 바론 오브 í—¬ì—게 ìƒì²˜ë°›ì•˜ë‹¤.,%o werd gekneusd door een baron van de hel.,%o ble forslÃ¥tt av en helvetesbaron.,%o zostaÅ‚@[ao_pl] stÅ‚uczon@[adj_pl] Barona PiekÅ‚a.,Um Barão do Inferno feriu %o.,,%o a fost contuzionat de un Baron al Infernului.,Игрок %o зашиблен бароном ада.,%o је мод@[adj_3_sr] због барона пакла.,%o blev skadad av en helvetesbaron.,%o bir Cehennem Baronu tarafından yaralandı.,%o потовчено бароном Пекла. @@ -1316,16 +1316,16 @@ Energy Cells,AMMO_CELLS,,,,Energetske ćelije,Energetické Älánky,Energiceller %o was burned by %k's BFG.,OB_MPBFG_MBF,,,,,%o shoÅ™el@[ao_cs] BFGÄkem hráÄe %k.,%o blev brændt af %ks BFG.,%o wurde von %ks BFG verbrannt.,@[art_gr] %o κάικε απο το BFG του/της %k.,%o estis bruligita de la BFG de %k.,A %o l@[ao_esp] ha calcinado la BFG de %k.,A %o l@[ao_esp] calcinó la BFG de %k.,%k korvensi %o paran BFG:llään.,%o a été irradié@[e_fr] par le BFG de %k.,,%k hamuvá égette %o testét BFG-vel.,%o è stato bruciato dal BFG di %k.,%o 㯠%k ã®BFGã§ç„¼ã殺ã•れãŸã€‚,%o ì€(는) %k ì˜ BFGì— ì˜í•´ 타올ëžë‹¤.,%o werd verbrand door %k's BFG.,%o ble brent av %ks BFG.,%o zostaÅ‚@[ao_pl] spalon@[adj_pl] przez BFG %k.,%k cozinhou %o com sua BFG.,,%o a fost ars de BFG-ul lui %k.,Игрока %o Ñжёг из BFG игрок %k.,%o је изгоре@[ao_1_sr] од ВЈП играча %k.,%o blev bränd av %ks BFG.,%o %k tarafından yakıldı,%o Ñпалило БФГ %k. ,,Heretic,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,Pickup ,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Blue Key,TXT_GOTBLUEKEY,,,,,Modrý klíÄ,BlÃ¥ nøgle,Blauer Schlüssel,Μπλέ Κλειδί,Blua Åœlosilo,Llave Azul,,Sininen avain,Clé Bleue,,Kék kulcs,Chiave blu,é’ã®éµ,청색 열쇠,Blauwe sleutel,BlÃ¥ nøkkel,Niebieski klucz,Chave Azul,,Cheie Albastră,Синий ключ,Плави кључ,BlÃ¥ nyckel,Mavi Anahtar,Синій ключ -Yellow Key,TXT_GOTYELLOWKEY,,,,,Žlutý klíÄ,Gul nøgle,Gelber Schlüssel,ΚÏÏ„Ïινο Κλειδί,Flava Åœlosilo,Llave Amarilla,,Keltainen avain,Clé Jaune,,Sárga kulcs,Chiave gialla,黄ã®éµ,황색 열쇠,Gele sleutel,Gul nøkkel,Żółty klucz,Chave Amarela,,Cheie Galbenă,Жёлтый ключ,Жути кључ,Gul nyckel,Sarı Anahtar,Жовтий ключ -Green Key,TXT_GOTGREENKEY,,,,,Zelený klíÄ,Grøn nøgle,Grüner Schlüssel,ΠÏάσινο Κλειδί,Verda Åœlosilo,Llave Verde,,Vihreä avain,Clé Verte,,Zöld kulcs,Chiave verde,ç·‘ã®éµ,녹색 열쇠,Groene sleutel,Grønn nøkkel,Zielony klucz,Chave Verde,,Cheie Verde,Зелёный ключ,Зелени кључ,Grön nyckel,YeÅŸil Anahtar,Зелений ключ -Quartz Flask,TXT_ARTIHEALTH,,,,,BlyÅ¡tivá baňka,Kvartsflaske,Quarzflasche,Φλάσκα Χαλαζίας,Kvarca Flakono,Frasco de Cuarzo,,Kvartsipullo,Flasque en Quartz,,Kvarc flaska,Ampolla di quarzo,石英フラスコ,ì„ì˜ í”Œë¼ìФí¬,Kwartskolf,Kvartskolbe,Kwarcowa Butelka,Frasco de Quartzo,,Flacon de Quartz,Кварцевый флакон,Кварцна боца,Kvartsflaska,Kuvars ÅžiÅŸesi,Кварцова плÑшечка -Wings of Wrath,TXT_ARTIFLY,,,,,Křídla hnÄ›vu,Vredens vinger,Flügel des Zorns,ΦτεÏά της ΟÏγής,Flugiloj de Kolero,Alas de Ira,,Kiihtymyksen siivet,Ailes du Courroux,,A Harag Szárnyai,Ali iraconde,レイスã®ç¿¼,ë¶„ë…¸ì˜ ë‚ ê°œ,Vleugels der Toorn,Vredens vinger,SkrzydÅ‚a Gniewu,Asas da Ira,,Aripile Furiei,ÐšÑ€Ñ‹Ð»ÑŒÑ Ð³Ð½ÐµÐ²Ð°,Крила гнева,Vridsvingar,Gazap Kanatları,Крила гніву -Ring of Invincibility,TXT_ARTIINVULNERABILITY,,,,,Prsten nesmrtelnosti,Ring af uovervindelighed,Ring der Unverwundbarkeit,Δαχτυλίδι της Αθανασίας,Ringo de Nevenkebleco,Anillo de Invencibilidad,,Näkymättömyyden sormus,Anneau d'Invincibilité,,A Sérthetetlenség Gyűrűje,Anello dell'invincibilità,䏿­»ã®æŒ‡è¼ª,ë¶ˆë©¸ì˜ ë°˜ì§€,Ring van Onoverwinnelijkheid,Uovervinnelighetens ring,PierÅ›cieÅ„ NiewrażliwoÅ›ci,Anel da Invencibilidade,,Inelul Invincibilității,Кольцо неуÑзвимоÑти,ПрÑтен непобедивоÑти,Ring av oövervinnlighet,Yenilmezlik Yüzüğü,Кільце ÐепереможноÑті -Tome of Power,TXT_ARTITOMEOFPOWER,,,,,Kniha moci,Bog om magt,Buch der Macht,Τομός της ΔÏναμης,Librego de Forto,Tomo de Poder,,Väkevyyden kirja,Livre du Pouvoir,,Az ErÅ‘ Kódexe,Tomo del potere,力ã®è¡“書,íž˜ì˜ ì„œ,Boek van de Macht,Maktens bok,Tom Mocy,Livro do Poder,Livro do Poder,Cartea Puterii,Том могущеÑтва,Том моћи,Maktens tome,Güç Kitabı,Том Ñили -Shadowsphere,TXT_ARTIINVISIBILITY,,,,,Å erosféra,Skyggesfære,Schattensphäre,ΣκιόσφαιÏα,Ombrosfero,Esfera de Sombra,,Varjokehrä,Sphère des Ombres,,Ãrnygömb,Sfera dell'ombra,é—‡ã®çƒä½“,ê·¸ë¦¼ìž êµ¬ì²´,Schaduwbol,Skyggesfære,Sfera Cieni,Esfera das Sombras,,Sfera Umbrei,Ð¢ÐµÐ½ÐµÐ²Ð°Ñ Ñфера,Сфера Ñенки,Skuggsfär,Gölge Küre,Сфера тіні -Morph Ovum,TXT_ARTIEGG,,,,,MÄ›nivejce,,Transformations-Ei,Μετασχηματίζοντικο ΩάÏιο,Ovo de Transformado,Huevo de Transformación,,Muodonmuutoksen muna,Ovule de Métamorphose,,Alakváltó tojás,Uovo della metamorfosi,変貌ã®åµå­,변신 알,Morfose-Ei,,Jajko MorfujÄ…ce,Ovo da Metamorfose,,Oul Metamorfozei,Яйцо превращениÑ,Преображавајуће јајашце,,Dönüşümlü Yumurta,Яйце транÑформації -Mystic Urn,TXT_ARTISUPERHEALTH,,,,,Tajemná urna,Mystisk urne,Mystische Urne,Μυστικό Δοχείο,Mistika Urno,Urna Mística,,Mystinen uurna,Urne Mystique,,Misztikus urna,Urna mistica,神秘ã®éª¨å£·,신비한 항아리,Mystieke Urn,Mystisk urne,Mistyczna Urna,Urna Mística,,Urnă Mistică,МиÑтичеÑÐºÐ°Ñ ÑƒÑ€Ð½Ð°,МиÑтериозна урна,Mystisk urna,Mistik Urn,МіÑтична урна +Blue Key,TXT_GOTBLUEKEY,,,,,Modrý klíÄ,BlÃ¥ nøgle,Blauer Schlüssel,Μπλέ Κλειδί,Blua Ålosilo,Llave azul,,Sininen avain,Clé Bleue,,Kék kulcs,Chiave blu,é’ã®éµ,청색 열쇠,Blauwe sleutel,BlÃ¥ nøkkel,Niebieski klucz,Chave Azul,,Cheie Albastră,Синий ключ,Плави кључ,BlÃ¥ nyckel,Mavi Anahtar,Синій ключ +Yellow Key,TXT_GOTYELLOWKEY,,,,,Žlutý klíÄ,Gul nøgle,Gelber Schlüssel,ΚÏÏ„Ïινο Κλειδί,Flava Ålosilo,Llave amarilla,,Keltainen avain,Clé Jaune,,Sárga kulcs,Chiave gialla,黄ã®éµ,황색 열쇠,Gele sleutel,Gul nøkkel,Żółty klucz,Chave Amarela,,Cheie Galbenă,Жёлтый ключ,Жути кључ,Gul nyckel,Sarı Anahtar,Жовтий ключ +Green Key,TXT_GOTGREENKEY,,,,,Zelený klíÄ,Grøn nøgle,Grüner Schlüssel,ΠÏάσινο Κλειδί,Verda Ålosilo,Llave verde,,Vihreä avain,Clé Verte,,Zöld kulcs,Chiave verde,ç·‘ã®éµ,녹색 열쇠,Groene sleutel,Grønn nøkkel,Zielony klucz,Chave Verde,,Cheie Verde,Зелёный ключ,Зелени кључ,Grön nyckel,YeÅŸil Anahtar,Зелений ключ +Quartz Flask,TXT_ARTIHEALTH,,,,,BlyÅ¡tivá baňka,Kvartsflaske,Quarzflasche,Φλάσκα Χαλαζίας,Kvarca flakono,Frasco de cuarzo,,Kvartsipullo,Flasque en Quartz,,Kvarc flaska,Ampolla di quarzo,石英フラスコ,ì„ì˜ í”Œë¼ìФí¬,Kwartskolf,Kvartskolbe,Kwarcowa Butelka,Frasco de Quartzo,,Flacon de Quartz,Кварцевый флакон,Кварцна боца,Kvartsflaska,Kuvars ÅžiÅŸesi,Кварцова плÑшечка +Wings of Wrath,TXT_ARTIFLY,,,,,Křídla hnÄ›vu,Vredens vinger,Flügel des Zorns,ΦτεÏά της ΟÏγής,Flugiloj de kolero,Alas de ira,,Kiihtymyksen siivet,Ailes du Courroux,,A Harag Szárnyai,Ali iraconde,レイスã®ç¿¼,ë¶„ë…¸ì˜ ë‚ ê°œ,Vleugels der Toorn,Vredens vinger,SkrzydÅ‚a Gniewu,Asas da Ira,,Aripile Furiei,ÐšÑ€Ñ‹Ð»ÑŒÑ Ð³Ð½ÐµÐ²Ð°,Крила гнева,Vridsvingar,Gazap Kanatları,Крила гніву +Ring of Invincibility,TXT_ARTIINVULNERABILITY,,,,,Prsten nesmrtelnosti,Ring af uovervindelighed,Ring der Unverwundbarkeit,Δαχτυλίδι της Αθανασίας,Ringo de nevenkebleco,Anillo de invencibilidad,,Näkymättömyyden sormus,Anneau d'Invincibilité,,A Sérthetetlenség Gyűrűje,Anello dell'invincibilità,䏿­»ã®æŒ‡è¼ª,ë¶ˆë©¸ì˜ ë°˜ì§€,Ring van Onoverwinnelijkheid,Uovervinnelighetens ring,PierÅ›cieÅ„ NiewrażliwoÅ›ci,Anel da Invencibilidade,,Inelul Invincibilității,Кольцо неуÑзвимоÑти,ПрÑтен непобедивоÑти,Ring av oövervinnlighet,Yenilmezlik Yüzüğü,Кільце ÐепереможноÑті +Tome of Power,TXT_ARTITOMEOFPOWER,,,,,Kniha moci,Bog om magt,Buch der Macht,Τομός της ΔÏναμης,Librego de forto,Tomo de poder,,Väkevyyden kirja,Livre du Pouvoir,,Az ErÅ‘ Kódexe,Tomo del potere,力ã®è¡“書,íž˜ì˜ ì„œ,Boek van de Macht,Maktens bok,Tom Mocy,Livro do Poder,Livro do Poder,Cartea Puterii,Том могущеÑтва,Том моћи,Maktens tome,Güç Kitabı,Том Ñили +Shadowsphere,TXT_ARTIINVISIBILITY,,,,,Å erosféra,Skyggesfære,Schattensphäre,ΣκιόσφαιÏα,Ombrosfero,Esfera de sombra,,Varjokehrä,Sphère des Ombres,,Ãrnygömb,Sfera dell'ombra,é—‡ã®çƒä½“,ê·¸ë¦¼ìž êµ¬ì²´,Schaduwbol,Skyggesfære,Sfera Cieni,Esfera das Sombras,,Sfera Umbrei,Ð¢ÐµÐ½ÐµÐ²Ð°Ñ Ñфера,Сфера Ñенки,Skuggsfär,Gölge Küre,Сфера тіні +Morph Ovum,TXT_ARTIEGG,,,,,MÄ›nivejce,,Transformations-Ei,Μετασχηματίζοντικο ΩάÏιο,Ovo de transformado,Huevo de transformación,,Muodonmuutoksen muna,Ovule de Métamorphose,,Alakváltó tojás,Uovo della metamorfosi,変貌ã®åµå­,변신 알,Morfose-Ei,,Jajko MorfujÄ…ce,Ovo da Metamorfose,,Oul Metamorfozei,Яйцо превращениÑ,Преображавајуће јајашце,,Dönüşümlü Yumurta,Яйце транÑформації +Mystic Urn,TXT_ARTISUPERHEALTH,,,,,Tajemná urna,Mystisk urne,Mystische Urne,Μυστικό Δοχείο,Mistika urno,Urna mística,,Mystinen uurna,Urne Mystique,,Misztikus urna,Urna mistica,神秘ã®éª¨å£·,신비한 항아리,Mystieke Urn,Mystisk urne,Mistyczna Urna,Urna Mística,,Urnă Mistică,МиÑтичеÑÐºÐ°Ñ ÑƒÑ€Ð½Ð°,МиÑтериозна урна,Mystisk urna,Mistik Urn,МіÑтична урна Torch,TXT_ARTITORCH,,,,,Pochodeň,Fakkel,Fackel,ΠυÏσός,Torĉo,Antorcha,,Soihtu,Torche,,Fáklya,Torcia,æ¾æ˜Ž,횃불,Fakkel,Fakkel,Pochodnia,Tocha,,Torță,Факел,Бакља,Fackel,MeÅŸale,СмолоÑкип Time Bomb of the Ancients,TXT_ARTIFIREBOMB,,,,,ÄŒasovaná bomba starovÄ›ku,De ældres tidsbombe,Zeitbombe der Alten,ΧÏονοβόμβα τον ΑÏχαίων,Tempobombo de la Antikvuloj,Bomba de tiempo de los Ancestros,,Vanhain aikapommi,Bombe a Retardement des Anciens,,Az Åsök IdÅ‘bombája,Bomba a tempo degli antichi,å¤ä»£ã®æ™‚é™çˆ†è–¬,ê³ ëŒ€ì˜ ì‹œí•œí­íƒ„,Tijdbom der Tijden,De eldgamles tidsinnstilte bombe,Starożytna Bomba Czasu,Bomba-relógio dos Anciãos,,Bomba cu Ceas a Anticilor,ЧаÑÐ¾Ð²Ð°Ñ Ð±Ð¾Ð¼Ð±Ð° древних,ВременÑка бомба древних,De gamlas tidsbomb,Kadimlerin Saatli Bombası,ЧаÑова бомба древніх Chaos Device,TXT_ARTITELEPORT,,,,,Zmatkostroj,Chaos-enhed,Chaosgerät,Συσκευή του Χάος,Ĥaos-Aparato,Dispositivo del Caos,,Kaaoskoje,Outil du Chaos,,Káoszszerkezet,"Dispositivo del Caos @@ -3074,9 +3074,9 @@ Earthquake shake intensity,DSPLYMNU_QUAKEINTENSITY,,,,,Intenzita zemÄ›tÅ™esení, Interpolate monster movement,DSPLYMNU_NOMONSTERINTERPOLATION,,,,,Interpolovat pohyb příšer,Interpolere monsterbevægelse,Interpoliere Monsterbewegung,,Interpoli monstro-movadon,Interpolar movimiento de los monstruos,,Interpoloi hirviöiden liike,Interpolation des monstres,,Szörny mozgás simítása,Interpola il movimento dei mostri,モンスターã®ç§»å‹•補間,개체 ì´ë™ ë³´ì •,Interpoleer monsterbewegingen,Interpolere monsterbevegelse,Interpolacja ruchu potworów,Interpolar movimento de monstros,,Interpolare miÈ™care monÈ™tri (efect vizual),Сглаживание Ð´Ð²Ð¸Ð¶ÐµÐ½Ð¸Ñ Ð¼Ð¾Ð½Ñтров,Интерполирај кретање чудовишта,Interpolera monsterrörelse,Canavar hareketini enterpole et, Menu dim,DSPLYMNU_MENUDIM,,,,,Síla barvy pozadí v menu,Menu dæmpes,Menüabdunklung,,Menuo-malheleco,Atenuación del menú,,Valikon himmennys,Assombrissement menu,,Menü homályosítása,Offuscamento menu,メニュー背景,메뉴 배경색 ê°•ë„,Donkere menuachtergrond,Demp meny,MgÅ‚a w menu,Atenuação do menu,,ÃŽntunecare meniu,Затемнение фона меню,Пригушивање менија,Dimma menyn,Menü loÅŸ, Dim color,DSPLYMNU_DIMCOLOR,,,Dim colour,,Barva pozadí v menu,Dim farve,Abdunklungsfarbe,,Malheleca koloro,Color de la atenuación,,Himmennysväri,Couleur de l'assombrissement,,Homályosítás színe,Colore offuscamento,背景色,배경색 설정,Donkere kleur,Demp farge,Kolor mgÅ‚y,Cor da atenuação,,Culoare întunecare,Цвет затемнениÑ,Боја пригушивања,Dimma färgen,LoÅŸ renk, -View bob amount while moving,DSPLYMNU_MOVEBOB,,,,,Pohupování pohledu pÅ™i pohybu,Vis bob-beløb under bevægelse,Waffenpendeln beim Bewegen,,Vidi kvanton de kapo-balanciÄo dum movado,Cantidad de balanceo al moverse,,Aseen heilumisvoimakkuus liikkeessä,Chaloupage arme en movement,,Fegyver mozgása lépés közben,Ammontare di bob di movimento,視点æºã‚Œã™ã‚‹ç§»å‹•値,ì´ë™ 시 화면 í”들림 ê°•ë„,Bekijk bob bedrag terwijl je beweegt,Vis bob-beløp mens du beveger deg,Dygaj kiedy siÄ™ ruszasz,Quantidade de balanço durante movimento,,MiÈ™care cameră în timpul deplasării,Покачивание камеры при движении,ТреÑење камере током кретања,Visa bob-mängden under förflyttning,Hareket halindeyken bob miktarını görüntüleme, -View bob amount while not moving,DSPLYMNU_STILLBOB,,,,,Pohupování pohledu v klidu,"Vis bob-beløbet, mens du ikke bevæger dig",Waffenpendeln bei Nichtbewegen,,Vidi kvanton de kapo-balanciÄo dum ne movado,Cantidad de balanceo al no moverse,,Aseen heilumisvoimakkuus levossa,Chaloupage arme statique,,Fegyver mozgása egy helyben,Ammontare di bob di movimento da fermo,視点æºã‚Œã—ãªã„移動値,ì •ì§€ 시 화면 움ì§ìž„ ê°•ë„,Bekijk bob bedrag terwijl je niet beweegt,Vis bobmengde mens du ikke beveger deg,Dygaj kiedy siÄ™ nie ruszasz,Quantidade de balanço parado,,MiÈ™care cameră în timpul staÈ›ionării,Покачивание камеры при бездейÑтвии,ТреÑење камере током неактивноÑти,Visa bob-mängden när den inte rör sig,Hareket etmiyorken bob miktarını görüntüleme, -Weapon bob speed,DSPLYMNU_BOBSPEED,,,,,Rychlost pohupování zbranÄ›,VÃ¥ben bob hastighed,Waffenpendelgeschwindigkeit,,Rapido de armilo-balanciÄo,Velocidad de balanceo de Arma,,Aseen heilumisnopeus,Vitesse du chaloupage,,Fegyver mozgás sebesség,Velocità di bob dell'arma,武器æºã‚Œé€Ÿåº¦,무기 í”들림 ì†ë„,Snelheid wapenzwaaieffect,VÃ¥pen bob hastighet,Szybkość ruchu broni,Velocidade de balanço de arma,,Viteză miÈ™care arme,СкороÑть Ð¿Ð¾ÐºÐ°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ð¾Ñ€ÑƒÐ¶Ð¸Ñ,Брзина трÑења оружја,Vapens hastighet,Silah bob hızı, +View bob amount while moving,DSPLYMNU_MOVEBOB,,,,,Pohupování pohledu pÅ™i pohybu,Vis bob-beløb under bevægelse,Waffenpendeln beim Bewegen,,Ekrana balanciÄo dum moviÄo,La vista tambalea al moverse,,Aseen heilumisvoimakkuus liikkeessä,Chaloupage arme en movement,,Fegyver mozgása lépés közben,Ammontare di bob di movimento,視点æºã‚Œã™ã‚‹ç§»å‹•値,ì´ë™ 시 화면 í”들림 ê°•ë„,Bekijk bob bedrag terwijl je beweegt,Vis bob-beløp mens du beveger deg,Dygaj kiedy siÄ™ ruszasz,Quantidade de balanço durante movimento,,MiÈ™care cameră în timpul deplasării,Покачивание камеры при движении,ТреÑење камере током кретања,Visa bob-mängden under förflyttning,Hareket halindeyken bob miktarını görüntüleme, +View bob amount while not moving,DSPLYMNU_STILLBOB,,,,,Pohupování pohledu v klidu,"Vis bob-beløbet, mens du ikke bevæger dig",Waffenpendeln bei Nichtbewegen,,Ekrana balanciÄo dum halto,La vista tambalea al no moverse,,Aseen heilumisvoimakkuus levossa,Chaloupage arme statique,,Fegyver mozgása egy helyben,Ammontare di bob di movimento da fermo,視点æºã‚Œã—ãªã„移動値,ì •ì§€ 시 화면 움ì§ìž„ ê°•ë„,Bekijk bob bedrag terwijl je niet beweegt,Vis bobmengde mens du ikke beveger deg,Dygaj kiedy siÄ™ nie ruszasz,Quantidade de balanço parado,,MiÈ™care cameră în timpul staÈ›ionării,Покачивание камеры при бездейÑтвии,ТреÑење камере током неактивноÑти,Visa bob-mängden när den inte rör sig,Hareket etmiyorken bob miktarını görüntüleme, +Weapon bob speed,DSPLYMNU_BOBSPEED,,,,,Rychlost pohupování zbranÄ›,VÃ¥ben bob hastighed,Waffenpendelgeschwindigkeit,,Rapido de armila balanciÄo,Velocidad de tambaleo del arma,,Aseen heilumisnopeus,Vitesse du chaloupage,,Fegyver mozgás sebesség,Velocità di bob dell'arma,武器æºã‚Œé€Ÿåº¦,무기 í”들림 ì†ë„,Snelheid wapenzwaaieffect,VÃ¥pen bob hastighet,Szybkość ruchu broni,Velocidade de balanço de arma,,Viteză miÈ™care arme,СкороÑть Ð¿Ð¾ÐºÐ°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ð¾Ñ€ÑƒÐ¶Ð¸Ñ,Брзина трÑења оружја,Vapens hastighet,Silah bob hızı, ,,Scaling,,,,,,,,,,,,,,,,,,,,,,,,,,,, Scaling Options,SCALEMNU_TITLE,,,,,Nastavení Å¡kálování rozhraní,Indstillinger for skalering,Skalierungsoptionen,,Skalo-Agordoj,Opciones de escalado,,Skaalausasetukset,Option de mise à l'échelle,,Méretezési Beállítások,Opzioni di scala,スケーリング オプション,배율 설정,Schaalopties,Alternativer for skalering,Opcje skalowania,Opções de escala,,Setări Dimensiuni,ÐаÑтройки маÑштабированиÑ,Подешавања размере,Skalningsalternativ,Ölçeklendirme Seçenekleri, Overrides for above setting,SCALEMNU_OVERRIDE,,,,,PotlaÄení nastavení výše,Overrides for ovenstÃ¥ende indstilling,Spezialisierungen,,Transpasoj por ĉi-supre agordo,Anular ajuste anterior,,Ohittaa asetuksen ylhäällä,Annule les paramètres au dessus,,Beállítások felülírása,Sovrascritture per i settaggi sopra,設定ã®ä¸Šæ›¸ã,위 ì„¤ì •ì„ ìœ„í•´ 무시,Overschrijdingen voor bovenstaande instelling,Overstyrer for innstillingen ovenfor,Nadpisuje dla powyższego ustawienia,Anular configurações acima,,Suprascriere pentru setările de mai sus,Задать Ñпециальные наÑтройки,Специјална подешавања,Överordnar ovanstÃ¥ende inställningar,Yukarıdaki ayar için geçersiz kılmalar, @@ -3130,7 +3130,7 @@ Precise,OPTVAL_PRECISE,,,,,PÅ™esný,Præcist,Genau,,Preciza,Preciso,,Tarkka,Pré Capped,OPTVAL_CAPPED,,,,,UzavÅ™ený,Begrænset,Limitiert,,Limitigita,Limitado,,Rajoitettu,Limité,,Lekorlátozva,Limitato,上é™,색ìƒì— 맞게,Afgeschermd,Begrenset,Ograniczony,Limitado,,Limitat,Ограниченный,Ограничено,Begränsad,Sınırlı, Particles,OPTVAL_PARTICLES,,,,,Částice,Partikler,Partikel,,Partikloj,Partículas,,Hiukkasina,Particules,,Részecskék,Particelle,パーティクル,ìž…ìžë§Œ,Deeltjes,Partikler,CzÄ…stki,Partículas,,Particule,ЧаÑтицы,ЧеÑтице,Partiklar,Parçacıklar, Sprites,OPTVAL_SPRITES,,,,,Sprity,,,,Mov-rastrumoj,,,Spriteinä,,,Sprite-ok,Sprite,スプライト,스프ë¼ì´íŠ¸ë§Œ,,,Sprite'y,,,Sprite-uri,Спрайты,Спрајтови,,Sprite'lar, -Sprites & Particles,OPTVAL_SPRITESPARTICLES,,,,,Sprity a Äástice,Sprites og partikler,Sprites und Partikel,,Mov-rastrumoj kaj partikloj,Sprites y partículas,,Spriteinä ja hiukkasina,Sprites & Particules,,Sprite-ok és részecskék,Sprite e particelle,スプライト & パーティクル,스프ë¼ì´íЏ+ìž…ìž,Sprites & Deeltjes,Sprites og partikler,Sprite'y i czÄ…stki,Sprites & Partículas,Sprites e Partículas,Sprite-uri & Particule,Спрайты и чаÑтицы,Спрајтови и чеÑтице,Sprites och partiklar,Sprite'lar ve Parçacıklar, +Sprites & Particles,OPTVAL_SPRITESPARTICLES,,,,,Sprity a Äástice,Sprites og partikler,Sprites und Partikel,,Mov-rastrumoj kaj partikloj,«Sprites» y partículas,,Spriteinä ja hiukkasina,Sprites & Particules,,Sprite-ok és részecskék,Sprite e particelle,スプライト & パーティクル,스프ë¼ì´íЏ+ìž…ìž,Sprites & Deeltjes,Sprites og partikler,Sprite'y i czÄ…stki,Sprites & Partículas,Sprites e Partículas,Sprite-uri & Particule,Спрайты и чаÑтицы,Спрајтови и чеÑтице,Sprites och partiklar,Sprite'lar ve Parçacıklar, Melt,OPTVAL_MELT,,,,,RozpuÅ¡tÄ›ní,Smelt,Verlauf,,Fandi,Derretir,,Sulaminen,Fondu,,Olvadás,Scioglimento,メルト,녹는 효과,Smelten,Smelte,Stopienie,Derreter,,Topire,ТаÑние,Топљење,Smält,Eriyik, Burn,OPTVAL_BURN,,,,,Oheň,Brænde,Einbrennen,,Bruli,Quemar,,Palaminen,Brûlure,,Égés,Bruciamento,ãƒãƒ¼ãƒ³,타는 효과,Verbranden,Brenne,Wypalenie,Queimar,,Ardere,Жжение,Спаљивање,Bränn,Yanmak, Crossfade,OPTVAL_CROSSFADE,,,,,ProlínaÄka,Crossfade,Überblenden,,Transvelki,Entrelazar,,Ristihäivytys,Fondu en croix,,Ãtvezetés,Dissolvenza a croce,クロスフェード,화면 êµì°¨,Crossfade,Overblending,Przenikanie,Dissolver,,Ofilire,УвÑдание,Вењење,Crossfade,Crossfade, @@ -3270,11 +3270,11 @@ Weapon light strength,GLPREFMNU_WPNLIGHTSTR,,,,,Intenzita svÄ›tel zbraní,VÃ¥ben Environment map on mirrors,GLPREFMNU_ENVIRONMENTMAPMIRROR,,,,,Mapa prostÅ™edí na zrcadlech,Miljøkort pÃ¥ spejle,Lichteffekt auf Spiegeln,,Medimapo sur speguloj,Mapa de entorno en espejos,,Ympäristökartta peileissä,Mappage environment sur les miroirs,,Környezeti mappolás a tükrökön,Ambiente mappa sui riflessi,マップオンミラーã®ç”»åƒè¡¨ç¤º,거울 주변ì˜ì§€ë„ 활성,Milieukaart op spiegels,Omgivelseskart pÃ¥ speil,Mapa Å›rodowiska w lustrach,Mapa de ambiente em espelhos,,ReflecÈ›ii pe oglinzi,Карта Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð½Ð° зеркалах,ОколинÑка мапа на прозорима,Miljökarta pÃ¥ speglar,Aynalar üzerinde çevre haritası, Enhanced night vision mode,GLPREFMNU_ENV,,,,,VylepÅ¡ený režim noÄního vidÄ›ní,Forbedret nattesynstilstand,Verbesserter Nachtsichtmodus,,Plibonigita reÄimo de noktvido,Modo de visión nocturna mejorado,,Paranneltu pimeänäkötila,Mode de vision nocture amélioré,,FelerÅ‘sített éjjellátó mód,Modalità visione notturna migliorata,ãƒã‚¤ã‚¶ãƒ¼ã‚’暗視装置調ã«ã™ã‚‹,야시경 효과 í–¥ìƒ,Verbeterde nachtzicht modus,Forbedret nattsynsmodus,Ulepszony tryb widzenia w ciemnoÅ›ci,Modo de visão noturna avançada,,Vedere infraroÈ™ie avansată,Улучшенный режим ночного видениÑ,Побољшана ноћна визија мод,Förbättrat läge för mörkerseende,GeliÅŸtirilmiÅŸ gece görüş modu, ENV shows stealth monsters,GLPREFMNU_ENVSTEALTH,,,,,VylepÅ¡ené noÄní vidÄ›ní ukazuje skryté příšery,ENV viser stealth-monstre,Nachtsichtmodus zeigt Stealth-Monster,,ENV montras kaÅiÄitajn monstrojn,ENV muestra enemigos sigilosos,,PPN näyttää näkymättömät hirviöt,VNA affiche monstres invisibles,,ENV felfedi a lopakodó szörnyeket,La VNM mostra i mostri stealth ,暗視ãƒã‚¤ã‚¶ãƒ¼ãŒé€æ˜Žã‚’見破る,스텔스 ì  ê°œì²´ë¥¼ ê°ì§€í•˜ëŠ” ENV,ENV toont stealth monsters,ENV viser stealth monstre,Ulepszony noktowizor pokazuje ukrywajÄ…cych siÄ™ przeciwników,Visão noturna mostra monstros invisíveis,,PNV afiÈ™ează monÈ™tri ascunÈ™i,ПÐÐ’ показывает Ñкрытых монÑтров,ПÐÐ’ показује Ñкривена чудовишта,ENV visar smygande monster,ENV gizli canavarları gösteriyor, -Adjust sprite clipping,GLPREFMNU_SPRCLIP,,,,,VyladÄ›ní clippingu spritů,Juster sprite clipping,Sprite Clipping,,Agordi trairadon de mov-rastrumoj,Ajustar recortado de sprites,,Spritejen asettelu,Adjusted le clipping des sprites,,Sprite átlógás igazítása,Aggiusta il clipping degli sprite,スプライトã®ãšã‚‰ã—を調整ã™ã‚‹,스프ë¼ì´íЏ í´ë¦¬í•‘ ì¡°ì •,Sprite clipping,Juster sprite klipping,Ustaw wciÄ™cie sprite'ów,Ajustar clipping de sprite,,Ajustare poziÈ›ii sprite-uri,Режим обрезки Ñпрайтов,ПодеÑи Ñпрајт клипинг,Justera sprite clipping,Sprite kırpmayı ayarlama, -Smooth sprite edges,GLPREFMNU_SPRBLEND,,,,,Vyhladit okraje spritů,Udglatte sprite-kanter,Glätte Spritekanten,,Glatigi randojn de mov-rastrumoj,Suavizar bordes de sprites,,Pehmeät spritejen reunat,Adoucir bords des sprites,,Sprite szélek simítása,Smussa gli angoli degli sprite,スプライトã®è§’を丸ã‚ã‚‹,부드러운 스프ë¼ì´íЏ 모서리,Gladde sprite randen,Glatte sprite kanter,GÅ‚adkie krawÄ™dzie sprite'ów,Suavizar bordas de sprites,,Netezire margini sprite-uri,Размытие краёв Ñпрайтов,Углачати ивице Ñпрајта,Slätar ut sprite-kanter,Düzgün sprite kenarları, +Adjust sprite clipping,GLPREFMNU_SPRCLIP,,,,,VyladÄ›ní clippingu spritů,Juster sprite clipping,Sprite Clipping,,Agordi trairadon de mov-rastrumoj,Ajustar recortado de «sprites»,,Spritejen asettelu,Adjusted le clipping des sprites,,Sprite átlógás igazítása,Aggiusta il clipping degli sprite,スプライトã®ãšã‚‰ã—を調整ã™ã‚‹,스프ë¼ì´íЏ í´ë¦¬í•‘ ì¡°ì •,Sprite clipping,Juster sprite klipping,Ustaw wciÄ™cie sprite'ów,Ajustar clipping de sprite,,Ajustare poziÈ›ii sprite-uri,Режим обрезки Ñпрайтов,ПодеÑи Ñпрајт клипинг,Justera sprite clipping,Sprite kırpmayı ayarlama, +Smooth sprite edges,GLPREFMNU_SPRBLEND,,,,,Vyhladit okraje spritů,Udglatte sprite-kanter,Glätte Spritekanten,,Glatigi randojn de mov-rastrumoj,Suavizar bordes de «sprites»,,Pehmeät spritejen reunat,Adoucir bords des sprites,,Sprite szélek simítása,Smussa gli angoli degli sprite,スプライトã®è§’を丸ã‚ã‚‹,부드러운 스프ë¼ì´íЏ 모서리,Gladde sprite randen,Glatte sprite kanter,GÅ‚adkie krawÄ™dzie sprite'ów,Suavizar bordas de sprites,,Netezire margini sprite-uri,Размытие краёв Ñпрайтов,Углачати ивице Ñпрајта,Slätar ut sprite-kanter,Düzgün sprite kenarları, Fuzz Style,GLPREFMNU_FUZZSTYLE,,,,,Styl Å¡umu,Fuzz-stil,Fuzz Stil,,Stilo de Lenugo,Estilo de difuminación,,Sumennustyyli,Style de bruit blanc,,Homályosítás stílusa,Stile fuzz,ファズスタイル,í¼ì¦ˆ 스타ì¼,Fuzz stijl,Fuzz-stil,Styl Szumu,Estilo de difusão,,Stil sclipire,Тип шума,Фаз Ñтајл,Fuzz stil,Fuzz Tarzı, -Sprite billboard,GLPREFMNU_SPRBILLBOARD,,,,,Orientace spritů,,Spriteausrichtung,,Liniigi mov-rastrumojn,Alineado de sprites,,Spritetaulu,Etalage des sprites,,Sprite alapú reklámtábla,,スプライト ビルボード,스프ë¼ì´íЏ 샘플ë§,,,Wyrównanie Sprite'ów,Alinhamento de sprite,,Rotire sprite-uri,ПривÑзка по оÑÑм Ñпрайтов,Спрајт билборд,,Sprite reklam panosu, -Sprites face camera,GLPREFMNU_SPRBILLFACECAMERA,,,,,Sprity Äelí kameÅ™e,Sprites vender mod kameraet,Sprites zur Kamera ausrichten,,Mov-rastrumoj ĉiam rigardas vian kameraon,Los sprites miran a la cámara,,Spritet suuntaavat kameraan,Sprites font face à la caméra,,Sprite alapú kamera,Gli sprite sono rivolti alla telecamera.,スプライトã®ãƒ•ェイスカメラ,ì¹´ë©”ë¼ë¥¼ 향한 스프ë¼ì´íЏ,Sprites gezicht camera,Sprites vender mot kameraet,Sprite'y skierowane w kamerÄ™,Sprites de frente para a câmera,,Sprite-urile privesc înspre cameră,Спрайты направлены к камере,Спрајт камера фаце,Sprites vänder sig mot kameran,Sprite'lar kamerayla yüzleÅŸir, +Sprite billboard,GLPREFMNU_SPRBILLBOARD,,,,,Orientace spritů,,Spriteausrichtung,,Liniigi mov-rastrumojn,Alineado de «sprites»,,Spritetaulu,Etalage des sprites,,Sprite alapú reklámtábla,,スプライト ビルボード,스프ë¼ì´íЏ 샘플ë§,,,Wyrównanie Sprite'ów,Alinhamento de sprite,,Rotire sprite-uri,ПривÑзка по оÑÑм Ñпрайтов,Спрајт билборд,,Sprite reklam panosu, +Sprites face camera,GLPREFMNU_SPRBILLFACECAMERA,,,,,Sprity Äelí kameÅ™e,Sprites vender mod kameraet,Sprites zur Kamera ausrichten,,Mov-rastrumoj ĉiam rigardas vian kameraon,Los «sprites» miran a la cámara,,Spritet suuntaavat kameraan,Sprites font face à la caméra,,Sprite alapú kamera,Gli sprite sono rivolti alla telecamera.,スプライトã®ãƒ•ェイスカメラ,ì¹´ë©”ë¼ë¥¼ 향한 스프ë¼ì´íЏ,Sprites gezicht camera,Sprites vender mot kameraet,Sprite'y skierowane w kamerÄ™,Sprites de frente para a câmera,,Sprite-urile privesc înspre cameră,Спрайты направлены к камере,Спрајт камера фаце,Sprites vänder sig mot kameran,Sprite'lar kamerayla yüzleÅŸir, Particle style,GLPREFMNU_PARTICLESTYLE,,,,,Styl Äástic,Partikelstil,Partikelstil,,Partikla stilo,Estilo de partículas,,Hiukkastyyli,Style de particules,,Részecske stílusa,Stile particelle,パーティクル スタイル,ìž…ìž ìŠ¤íƒ€ì¼,Deeltjes stijl,Partikkel-stil,Styl CzÄ…steczek,Tipo de partícula,,Stil particule,Тип чаÑтиц,ЧеÑтице Ñтајл,Partikelstil,Parçacık stili, Rendering quality,GLPREFMNU_RENDERQUALITY,,,,,Kvalita vykreslování,Renderingskvalitet,Renderqualität,,Kvalito de bildigado,Calidad de Renderizado,,Hahmonnuslaatu,Qualité du rendu,,Renderelés minÅ‘sége,Qualità resa grafica,レンダリングå“質,ë Œë”ë§ í’ˆì§ˆ,Het teruggeven van kwaliteit,Renderingskvalitet,Jakość Renderowania,Qualidade de renderização,,Calitate video,КачеÑтво обработки,Квалитет рендовања,Renderingskvalitet,Render kalitesi, Stereo 3D VR,GLPREFMNU_VRMODE,,,,,,,,,Duvida 3D VR,Modo Stereo 3D VR,,,3D VR Stéréo,,,,ステレオ3Dã®VR,VR ìž…ì²´ 스테레오 사용,,,,3D Estéreo (VR),,Mod VR,Стерео 3D VR-режим,Стереотоно 3D VR,,, @@ -3386,7 +3386,7 @@ Custom Inverse Map,DSPLYMNU_CUSTOMINVERTMAP,,,,,Vlastní inverzní barvení,Brug ", Custom Inverse Color Light,DSPLYMNU_CUSTOMINVERTC2,,,Custom Inverse Colour Light,,Vlastní inverzní svÄ›tlá barva,Brugerdefineret omvendt farve lys,Helle Farbe für Invertierung,ΠÏοσαÏμοσμένος Ανάποδο ΧÏώμα Φωτεινό,Propra Inversa Koloro Hela,Color Claro de Filtro Invertido,,"Mukautettu käänteinen vaalea väri ",Couleur claire pour l'inversion,,Egyéni inverz világos szín,Colore inverso personalizzato chiaro,å転色カスタム 明,ì‚¬ìš©ìž ì§€ì • ë°ì€ 반전 색ìƒ,Aangepaste lichte inversiekleur,Egendefinert invers farge lys,Jasny kolor do inwersji,Personalizar Cores Invertidas - Cor clara,,Personalizare Culoare Inverse Map Deschisă,Светлый цвет,Прилагођено инверзно Ñветло у боји,Anpassad omvänd färg ljus,Özel Ters Renkli Işık, -Weapon bob amount when firing,DSPLYMNU_BOBFIRE,,,,,Pohupování zbranÄ› pÅ™i stÅ™elbÄ›,VÃ¥ben bob-beløb ved affyring,Waffenpendeln beim Schießen,,Vidi kvanton de kapo-balanciÄo dum pafado,Balanceo de pantalla al disparar,,Aseen heilumisvoimakkuus ammuttaessa,Intensité du chaloupage lors des tirs,,Fegyvermozgás lövésnél,Movimento dell'arma quando si spara,å°„æ’ƒæ™‚ã®æ­¦å™¨ã‚¹ã‚¤ãƒ³ã‚°,사격 시 무기 스윙,Wapenzwaai bij schieten,VÃ¥ben bob-beløp ved avfyring,CzÄ™stotliwość bujania broni przy strzelaniu,Quantidade de balanço ao atirar,,Intensitate miÈ™care arme în timpul deplasării,Кол-во трÑÑки Ñкрана при Ñтрельбе,,Vapen bob-värde vid skottlossning,AteÅŸleme sırasında bob miktarını görüntüleme, +Weapon bob amount when firing,DSPLYMNU_BOBFIRE,,,,,Pohupování zbranÄ› pÅ™i stÅ™elbÄ›,VÃ¥ben bob-beløb ved affyring,Waffenpendeln beim Schießen,,Armila balanciÄo dum pafado,El arma tambalea al disparar,,Aseen heilumisvoimakkuus ammuttaessa,Intensité du chaloupage lors des tirs,,Fegyvermozgás lövésnél,Movimento dell'arma quando si spara,å°„æ’ƒæ™‚ã®æ­¦å™¨ã‚¹ã‚¤ãƒ³ã‚°,사격 시 무기 스윙,Wapenzwaai bij schieten,VÃ¥ben bob-beløp ved avfyring,CzÄ™stotliwość bujania broni przy strzelaniu,Quantidade de balanço ao atirar,,Intensitate miÈ™care arme în timpul deplasării,Кол-во трÑÑки Ñкрана при Ñтрельбе,,Vapen bob-värde vid skottlossning,AteÅŸleme sırasında bob miktarını görüntüleme, Intermission and Screen Border Classic Scaling,SCALEMNU_SCALECLASSIC,HUD->Scaling Options,,,,Klasické Å¡kálování text. obrazovek a okrajů obrazovky,Pause og skærmkant Klassisk skalering,Klassische Skalierung von Texthintergrund und Rahmen,,Klasika Skalado de Intermito kaj Ekranbordero,Escalado Clásico de Intermisión y Bordes de Pantalla,,Perinteinen väliruutujen ja ruudunreunusten skaalaus,Mise à l'échelle classique pour bordure écran et intermission,,Megszakító és Képszél klasszikus arányosítása,Ridimensionamento classico dell'intermissione e del bordo dello schermo,æ—§å¼ã‚¹ã‚±ãƒ¼ãƒ«ã®ã‚¯ãƒªã‚¢çµæžœã¨ç”»é¢ãƒœãƒ¼ãƒ€ãƒ¼,ì¸í„°ë¯¸ì…˜ ë° í™”ë©´ í…Œë‘리 í´ëž˜ì‹ 스케ì¼ë§,Intermissie en schermrand schalen,Klassisk skalering av pause og skjermkant,Klasyczne skalowanie przerywników i ramek,Escala clássica para tela de intervalo e borda de tela,,Scară bordură antract È™i ecran,КлаÑÑичеÑкие размеры Ñкрана между миÑÑиÑми и границ Ñкрана,КлаÑично Ñкалирање паузе и ивице екрана,Klassisk skalning för paus och skärmkant,Ara ve Ekran Kenarlığı Klasik Ölçeklendirme, Intermission background,SCALEMNU_INTER,,,,,Pozadí textových obrazovek,Intermission baggrund,Texthintergrund,,Fono de intermito,Fondo de intermisión,,Väliruutujen tausta,Fond d'écran intermission,,Megszakító háttér,Sottofondi delle intermissioni,ã‚¯ãƒªã‚¢çµæžœã®èƒŒæ™¯,ì¸í„°ë¯¸ì…˜ ë°°ê²½,Intermissieachtergrond,Pause bakgrunn,TÅ‚o przerywników,Fundo da tela de intervalo,,Fundal antract,Фон Ñкрана между миÑÑиÑми,ИнтермиÑÑион бацкгроунд,Bakgrund för mellanslag,Devre arası arka planı, Screen border,SCALEMNU_BORDER,,,,,Okraje obrazovky,Skærmbord,Rahmen,,Ekranbordero,Bordes de pantalla,,Ruudunreunus,Bordure d'écran,,Kép széle,Bordo dello schermo,ç”»é¢ãƒœãƒ¼ãƒ€ãƒ¼,화면 í…Œë‘리,Schermrand,Skjermkant,Ramka,Borda de tela,,Bordură ecran,Границы Ñкрана,Граница екрана,Skärmgräns,Ekran kenarlığı, @@ -3427,30 +3427,30 @@ Disable MBF21 features,CMPTMNU_NOMBF21,,,,,Zakázat funkce MBF21,Deaktivere MBF2 Show time widget,ALTHUDMNU_SHOWTIMESTAT,,,,,Zobrazit widget s Äasem,Vis tid widget,Zeige Zeit-Widget,,Montri tempo-fenestraĵon,Mostrar widget de tiempo,,Näytä aikatyökalu,Afficher le widget horloge,,Óra mutatása,Mostra orario,ウィジットタイマーã®è¡¨ç¤º,시간 표시 위젯,Tijdwidget tonen,Vis tid widget,Pokaż widżet czasu,Mostrar widget de tempo,,AfiÈ™are dispozitiv pt. timp,Показать виджет времени,Прикажи виџет времена,Visa tidswidget,Zaman widget'ını göster, MBF21,OPTVAL_MBF21,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Crushed monsters get resurrected as ghosts,CMPTMNU_VILEGHOSTS,,,,,Oživovat rozdrcené příšery jako duchy,Knuste monstre genopstÃ¥r som spøgelser,Zermalmte Monster werden als Geister wiedererweckt,Συνθλιμένα τέÏατα αναστήνοντε ως φαντάσματα,Dispremitaj monstroj reviviÄas kiel fantomojn,Los monstruos aplastados son resucitados como fantasmas,,Murskatut hirviöt heräävät henkiin aaveina...,Les monstres écrasés ressuscitent en fantômes,,Összenyomott szörnyek szellemként születnek újra,Mostri schiacciati vengono resuscitati come fantasmi,圧死ã—ãŸæ•µãŒè˜‡ç”Ÿã§ã‚´ãƒ¼ã‚¹ãƒˆåŒ–,ë¶„ì‡„ëœ ëª¬ìŠ¤í„°ëŠ” 유령으로 부활합니다.,Verpletterde monsters worden herrezen als geesten,Knuste monstre gjenoppstÃ¥r som spøkelser,Zmiażdżone potwory wskrzeszone jako duchy,Monstros esmagados ressucitam como fantasmas,,MonÈ™trii striviÈ›i sunt readuÈ™i la viață ca fantome.,Раздавленные монÑтры воÑкреÑают в виде призраков,Сломљена чудовишта ваÑкрÑавају као духови,Krossade monster Ã¥teruppstÃ¥r som spöken.,EzilmiÅŸ canavarlar hayalet olarak dirilir, -View bobbing while flying,MISCMNU_FVIEWBOB,,,,,Pohupování pohledu pÅ™i létání,Vis bobbing mens du flyver,Waffenpendeln beim Fliegen,,Vidi kapo-balanciÄon dum flugado,La vista tambalea al volar,,,Chaloupage en vol,,Nézet repüléskori billegése,,飛行中ã®è¦–点æºã‚Œ,비행 중 í”들림 보기,Bekijk bob bedrag terwijl je vliegt,Se bobbing mens du flyr,Pokaż bujanie podczas lotu,Balanço na visão durante voo,,MiÈ™care cameră în timpul zborului,Покачивание камеры при полёте,Погледајте Ñкакање током лета,Visa att det guppar när du flyger,Uçarken sallanmayı görüntüle, +View bobbing while flying,MISCMNU_FVIEWBOB,,,,,Pohupování pohledu pÅ™i létání,Vis bobbing mens du flyver,Waffenpendeln beim Fliegen,,Ekrana balanciÄo dum flugado,La vista tambalea al volar,,,Chaloupage en vol,,Nézet repüléskori billegése,,飛行中ã®è¦–点æºã‚Œ,비행 중 í”들림 보기,Bekijk bob bedrag terwijl je vliegt,Se bobbing mens du flyr,Pokaż bujanie podczas lotu,Balanço na visão durante voo,,MiÈ™care cameră în timpul zborului,Покачивание камеры при полёте,Погледајте Ñкакање током лета,Visa att det guppar när du flyger,Uçarken sallanmayı görüntüle, %o was maimed by a Serpent.,OB_DEMON1HIT,,,,,%o byl@[ao_cs] zmrzaÄen@[ao_cs] zlohadem.,%o blev lemlæstet af en slange.,%o wurde von einer Schlange zerfleischt.,,%o vundegiÄis de Serpento.,,,,%o a été mutilé@[e_fr] par un Serpent.,,,%o è stato mutilato da un serpente.,%o ã¯ã‚µãƒ¼ãƒšãƒ³ãƒˆã«æ–¬ã‚Šæ¨ã¦ã‚‰ã‚ŒãŸã€‚,,%o werd verminkt door een slang.,%o ble lemlestet av en slange.,Wąż skaleczyÅ‚ gracza %o,%o foi mutilad@[ao_ptb] por uma Serpente.,,,Игрок %o иÑкалечен змеем.,,%o blev lemlästad av en orm.,"%o, bir yılan tarafından sakat bırakıldı.", Antialias Lines,AUTOMAPMNU_LINEANTIALIASING,,,,,Vyhlazovat Äáry,Antialias-linjer,Linien glätten,,Glatigaj linioj,,,,Lignes Antialias,,,,Linesã®ã‚¢ãƒ³ãƒã‚¨ã‚¤ãƒªã‚¢ã‚¹,,Antialijnen,Antialias-linjer,Linie Antyaliasowe,Linhas com antiserrilhamento,,,Линии ÑглаживаниÑ,,Antialias Linjer,Antialias Hatları, Move and shoot through other players,GMPLYMNU_NOPLAYERCLIP,,,,,Procházet a střílet skrz ostatní hráÄe,Bevæg dig og skyd gennem andre spillere,Keine Kollisionen mit anderen Spielern,,Movi kaj pafi tra aliajn ludantojn,,,,Déplacez-vous et tirez à travers les autres joueurs,,,Muoversi e sparare attraverso gli altri giocatori,ç§»å‹•ã¨æ”»æ’ƒãŒä»–プレイヤーを通éŽ,,Beweeg en schiet door andere spelers,Beveg deg og skyt gjennom andre spillere,Brak kolizji z innymi graczami,Mover e atirar através de outros jogadores,,,Перемещение и Ñтрельба через игроков,,Förflytta dig och skjut genom andra spelare,DiÄŸer oyuncuların arasından geçin ve ateÅŸ edin, -Share keys with all players,GMPLYMNU_SHAREKEYS,,,,,Sdílet klíÄe se vÅ¡emi hráÄi,Del nøgler med alle spillere,Schlüssel mit allen Spielern teilen,,Kunhavigi Ålosilojn kun ĉiuj ludantoj,,,,Partager les clés avec tous les joueurs,,,Condividere i tasti con tutti i giocatori,全プレイヤーã«ã‚­ãƒ¼ã‚’共有,,Deel toetsen met alle spelers,Del nøkler med alle spillere,Współdziel klucze z innymi graczami,Compartilhar chaves com todos os jogadores,,,Общие ключи игроков,,Dela nycklar med alla spelare,Anahtarları tüm oyuncularla paylaşın, +Share keys with all players,GMPLYMNU_SHAREKEYS,,,,,Sdílet klíÄe se vÅ¡emi hráÄi,Del nøgler med alle spillere,Schlüssel mit allen Spielern teilen,,Kunhavigi Ålosilojn kun ĉiuj ludantoj,Compartir llaves con los demás jugadores,,,Partager les clés avec tous les joueurs,,,Condividere i tasti con tutti i giocatori,全プレイヤーã«ã‚­ãƒ¼ã‚’共有,,Deel toetsen met alle spelers,Del nøkler med alle spillere,Współdziel klucze z innymi graczami,Compartilhar chaves com todos os jogadores,,,Общие ключи игроков,,Dela nycklar med alla spelare,Anahtarları tüm oyuncularla paylaşın, Disallow override of camera facing mode,GLPREFMNU_SPRBILLFACECAMERAFORCE,,,,,,Tillad ikke tilsidesættelse af kameravendt tilstand,Änderung des Kameramodus nicht erlauben,,Malebligi transpason de kamerao-rigardado-reÄimo,,,,Interdiction de modifier le mode d'orientation de la caméra,,,Disconoscimento della modalità telecamera,Facing modeã®ã‚«ãƒ¡ãƒ©ã‚’オーãƒãƒ¼ãƒ©ã‚¤ãƒ‰ã—ãªã„,,Overschrijven van camerastand verbieden,Tillat ikke overstyring av kameramodus,ZabroÅ„ nadpisywania trybu widoku,Forçar modo de alinhamento do sprite à câmera,,,Запретить переопределение режима поворота камеры,,TillÃ¥t inte Ã¥sidosättande av kamerans vändläge,Kameraya bakma modunun geçersiz kılınmasına izin verme, Swap health and armor,ALTHUDMNU_SWAPHEALTHARMOR,,,,,Prohodit zdraví a brnÄ›ní,Byt om pÃ¥ helbred og rustning,Anzeige für Gesundheit und Panzerung tauschen,,Permuti sanon kaj kirason,,,,Échanger la santé et l'armure,,,Scambio di salute e armatura,体力ã¨ã‚¢ãƒ¼ãƒžãƒ¼ã‚’スワップ,,Verwissel gezondheid en pantser,Bytt helse og rustning,ZamieÅ„ zdrowie i pancerz,Trocar saúde e armadura,,,ПоменÑть отображение Ð·Ð´Ð¾Ñ€Ð¾Ð²ÑŒÑ Ð¸ брони меÑтами,,Byt hälsa och rustning,SaÄŸlık ve zırhı deÄŸiÅŸtir, -Use classic HUD scaling,SCALEMNU_HUD_OLDSCALE,hud_oldscale,,,,Pro HUD použít klasické Å¡kálování,Brug klassisk HUD-skalering,Klassische HUD-Skalierung,,Uzi klasikan skaladon de HUD,,,,Utiliser l'échelle classique du HUD,,,Utilizzare la scalatura classica dell'HUD,æ—§å¼HUDスケーリングを使ã†,,Gebruik klassieke HUD-schaling,Bruk klassisk HUD-skalering,Użyj klasycznego skalowania HUD-a,Usar escala clássica do HUD,,,КлаÑÑичеÑкое маÑштабирование интерфейÑа,,Använd klassisk HUD-skalning,Klasik HUD ölçeklendirmesini kullanın, +Use classic HUD scaling,SCALEMNU_HUD_OLDSCALE,hud_oldscale,,,,Pro HUD použít klasické Å¡kálování,Brug klassisk HUD-skalering,Klassische HUD-Skalierung,,Uzi klasikan skaladon de HUD,Usar escalado clásico del HUD,,,Utiliser l'échelle classique du HUD,,,Utilizzare la scalatura classica dell'HUD,æ—§å¼HUDスケーリングを使ã†,,Gebruik klassieke HUD-schaling,Bruk klassisk HUD-skalering,Użyj klasycznego skalowania HUD-a,Usar escala clássica do HUD,,,КлаÑÑичеÑкое маÑштабирование интерфейÑа,,Använd klassisk HUD-skalning,Klasik HUD ölçeklendirmesini kullanın, HUD scale factor,SCALEMNU_HUD_SCALEFACTOR,hud_scalefactor,,,,Faktor HUD Å¡kálování,HUD-skaleringsfaktor,HUD-Skalierungsfaktor,,Skalfaktoro de HUD,,,,Facteur d'échelle du HUD,,,Fattore di scala dell'HUD,HUDスケールå€çއ,,HUD-schaalfactor,HUD-skaleringsfaktor,Współczynnik skalowania HUD-a,Fator de escala do HUD,,,Значение маÑÑˆÑ‚Ð°Ð±Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñа,,Skalningsfaktor för HUD,HUD ölçek faktörü, -Spawn co-op only things in multiplayer,GMPLYMNU_COOPTHINGS,Any Actor that only appears in co-op is disabled,,,,Co-op vÄ›ci pouze v multiplayeru,Skab kun co-op-objekter i multiplayer,Erzeuge Nur-Co-op Dinge in Multiplayer,,Ekaperigi nurajn kooperajn aĵojn en plurludanta reÄimo,,,,Faire apparaitre les acteurs Co-Op Seulement en Multijoueur,,,Creare oggetti solo per la cooperativa in multigiocatore,マルãƒã§å”力モードé™å®šthingsを出ã™,,Alleen co-op-voorwerpen maken in multiplayer,Opprett co-op-objekter i flerspillermodus,Wyłącz aktorów trybu kooperacji,Objetos exclusivos do modo cooperativo surgem somente no modo multijogador,,,ПоÑвление вещей из ÑовмеÑтной игры в Ñетевой игре,,Skapa co-op-objekt i flerspelarläget,Çok oyunculu modda ortak nesneler oluÅŸturun, -Spawn co-op only pickup items in multiplayer,GMPLYMNU_COOPITEMS,Items that only appear in co-op are disabled,,,,Co-op pÅ™edmÄ›ty pouze v multiplayeru,Skab kun co-op-genstande i multiplayer,Erzeuge Nur-Co-op Gegenstände in Multiplayer,,Ekaperigi nurajn kooperajn objektojn en plurludanta reÄimo,,,,Faire apparaitre les objets Co-Op Seulement en Multijoueur,,,Creare pickup solo per la cooperativa in multigiocatore,マルãƒã§å”力モードé™å®šã‚¢ã‚¤ãƒ†ãƒ ã‚’出ã™,,Alleen co-op pickups maken in multiplayer,Opprett co-op-pickuper i flerspillermodus,Wyłącz przedmioty trybu kooperacji,Itens exclusivos do modo cooperativo surgem somente no modo multijogador,,,ПоÑвление подбираемых предметов из ÑовмеÑтной игры в Ñетевой игре,,Skapa co-op pickups i flerspelarläget,Çok oyunculu modda ortak pikaplar oluÅŸturun, -Players pick up their own copy of items in multiplayer,GMPLYMNU_LOCALITEMS,Items are picked up client-side rather than fully taken by the client who picked it up,,,,V multiplayeru hráÄi sbírají své vlastní kopie pÅ™edmÄ›tů,Spillere samler deres egen kopi af genstande op i multiplayer,Spieler nehmen ihre eigene Kopie von Gegenständen im Mehrspielermodus auf,,Ludantoj prenas sian propran kopion de objektoj en plurludanta reÄimo,,,,Chaque joueur obtient sa copie de chaque objet en Multijoueur,,,I giocatori raccolgono la propria copia di oggetti in multigiocatore,マルãƒã§ãƒ—レイヤーé”ã¯è¤‡è£½ã‚¢ã‚¤ãƒ†ãƒ ã‚’拾ã†,,Spelers pikken hun eigen exemplaar van voorwerpen op in multiplayer,Spillere plukker opp sin egen kopi av gjenstander i flerspiller,Gracze podnoszÄ… swoje przedmioty w trybie wieloosobowym,Jogadores podem pegar suas próprias cópias do item no modo multijogador,,,Игроки подбирают ÑобÑтвенные копии предметов в Ñетевой игре,,Spelare plockar upp sin egen kopia av föremÃ¥l i flerspelarläget,Oyuncular çok oyunculu modda eÅŸyaların kendi kopyalarını alırlar, +Spawn co-op only things in multiplayer,GMPLYMNU_COOPTHINGS,Any Actor that only appears in co-op is disabled,,,,Co-op vÄ›ci pouze v multiplayeru,Skab kun co-op-objekter i multiplayer,Erzeuge Nur-Co-op Dinge in Multiplayer,,Nur-kunlabor-reÄimaj agemaj objektoj aperas en plurludula reÄimo,Los obj. interactivos del modo cooperativo salen en el multijugador,,,Faire apparaitre les acteurs Co-Op Seulement en Multijoueur,,,Creare oggetti solo per la cooperativa in multigiocatore,マルãƒã§å”力モードé™å®šthingsを出ã™,,Alleen co-op-voorwerpen maken in multiplayer,Opprett co-op-objekter i flerspillermodus,Wyłącz aktorów trybu kooperacji,Objetos exclusivos do modo cooperativo surgem somente no modo multijogador,,,ПоÑвление вещей из ÑовмеÑтной игры в Ñетевой игре,,Skapa co-op-objekt i flerspelarläget,Çok oyunculu modda ortak nesneler oluÅŸturun, +Spawn co-op only pickup items in multiplayer,GMPLYMNU_COOPITEMS,Items that only appear in co-op are disabled,,,,Co-op pÅ™edmÄ›ty pouze v multiplayeru,Skab kun co-op-genstande i multiplayer,Erzeuge Nur-Co-op Gegenstände in Multiplayer,,Nur-kunlabor-reÄimaj prenotaĵoj aperas en plurludula reÄimo,Los obj. cogibles del modo cooperativo salen en el multijugador,Los obj. agarrables del modo cooperativo salen en el multijugador,,Faire apparaitre les objets Co-Op Seulement en Multijoueur,,,Creare pickup solo per la cooperativa in multigiocatore,マルãƒã§å”力モードé™å®šã‚¢ã‚¤ãƒ†ãƒ ã‚’出ã™,,Alleen co-op pickups maken in multiplayer,Opprett co-op-pickuper i flerspillermodus,Wyłącz przedmioty trybu kooperacji,Itens exclusivos do modo cooperativo surgem somente no modo multijogador,,,ПоÑвление подбираемых предметов из ÑовмеÑтной игры в Ñетевой игре,,Skapa co-op pickups i flerspelarläget,Çok oyunculu modda ortak pikaplar oluÅŸturun, +Players pick up their own copy of items in multiplayer,GMPLYMNU_LOCALITEMS,Items are picked up client-side rather than fully taken by the client who picked it up,,,,V multiplayeru hráÄi sbírají své vlastní kopie pÅ™edmÄ›tů,Spillere samler deres egen kopi af genstande op i multiplayer,Spieler nehmen ihre eigene Kopie von Gegenständen im Mehrspielermodus auf,,Objektoj prenitaj de alia ludanto ne malaperas en plurludula reÄimo,Los obj. cogidos por otro jugador no desaparecen en el multijugador,Los obj. agarrados por otro jugador no desaparecen en el multijugador,,Chaque joueur obtient sa copie de chaque objet en Multijoueur,,,I giocatori raccolgono la propria copia di oggetti in multigiocatore,マルãƒã§ãƒ—レイヤーé”ã¯è¤‡è£½ã‚¢ã‚¤ãƒ†ãƒ ã‚’拾ã†,,Spelers pikken hun eigen exemplaar van voorwerpen op in multiplayer,Spillere plukker opp sin egen kopi av gjenstander i flerspiller,Gracze podnoszÄ… swoje przedmioty w trybie wieloosobowym,Jogadores podem pegar suas próprias cópias do item no modo multijogador,,,Игроки подбирают ÑобÑтвенные копии предметов в Ñетевой игре,,Spelare plockar upp sin egen kopia av föremÃ¥l i flerspelarläget,Oyuncular çok oyunculu modda eÅŸyaların kendi kopyalarını alırlar, Disable client-side pick ups on dropped items,GMPLYMNU_NOLOCALDROP,Drops from Actors aren't picked up locally,,,,Zakázat sbírání odhozených pÅ™edmÄ›tů na stranÄ› klientů,Deaktiver opsamling pÃ¥ klientsiden af tabte genstande,Client-seitiges Aufnehmen von fallengelassenen Gegenständen deaktivieren,,Malvalidigi klientflankajn prenadojn de faligitaj objektoj,,,,Désactiver le ramassage des objets lâchés en clientside,,,Disabilita il ritiro degli oggetti caduti dal lato client,クライアントå´ã§è½ã¨ã—ãŸã‚¢ã‚¤ãƒ†ãƒ ã‚’拾ã‚ã›ãªã„,,Client-side pick ups van gedropte items uitschakelen,Deaktiver opphenting pÃ¥ klientsiden av gjenstander som droppes,Wyłącz współdzielenie wypadajÄ…cych przedmiotów,Jogadores não podem pegar itens largados,,,Отключить подбирание выпавших предметов на Ñтороне клиента,,Inaktivera upphämtning av tappade föremÃ¥l pÃ¥ klientsidan,Düşen öğeleri istemci tarafında almayı devre dışı bırakma, -Restart level on Death,MISCMNU_RESTARTONDEATH,Disables autoloading save on death,,,,Restart po umÅ™ení,Genstart niveau ved død,Level bei Tod neu starten,,Rekomenci nivelon post morto,,,,Recharger après mort,,,Riavvio del livello in caso di morte,ãƒªã‚¹ã‚¿ãƒ¼ãƒˆæ™‚ã«æ­»äº¡ã™ã‚‹,,Level herstarten bij dood,Start nivÃ¥et pÃ¥ nytt ved død,Restartuj poziom po Å›mierci,Reiniciar fase após morte,,,ПерезапуÑк ÑƒÑ€Ð¾Ð²Ð½Ñ Ð¿Ð¾Ñле Ñмерти,,Starta om nivÃ¥n vid död,Ölüm halinde seviyeyi yeniden baÅŸlat, -Pistol Start,GMPLYMNU_PISTOLSTART,Resets inventory on every map,,,,ZaÄít jen s pistolí,Start med pistol,Pistolenstart,,Komenci kun pistolo,,,,Démarrage du pistolet,,,Avvio pistola,ピストルスタート,,Start met pistool,Start med pistol,Start z pistoletem,Iniciar com a pistola,,,ПиÑтолет в начале,,Starta med pistol,Tabanca ile baÅŸlayın, -Allow creation of zombie players,CMPTMNU_VOODOOZOMBIES,,,,,Povolit zombie hráÄe,,Erlaube Zombieplayer,,Permesi kreadon de zombiaj ludantoj,,,,,,,,ゾンビプレイヤーã®ç”Ÿæˆè¨±å¯,,,,Pozwól na tworzenie graczy-zombie,Permitir criação de jogadores zumbi,,,Разрешить Ñоздание игроков-зомби,,,, -ignore floor z when teleporting,CMPTMNU_FDTELEPORT,,,,,PÅ™i teleportu ignorovat výšku podlahy,,Ignoriere Fußbodenhöhe bem Teleportieren,,Ignori altecon de planko dum teleportado,,,,,,,,テレãƒãƒ¼ãƒˆæ™‚ã«floor z値を無視ã™ã‚‹,,,,Ignoruj wysokość podÅ‚ogi podczas teleportacji,Ignorar altura do chão ao teletransportar,,,игнорирование диагонали пола (Z) при телепортации,,,, -Forced-Perspective,OPTVAL_ANAMORPHIC,"Hardware Sprite Clipping mode that gives the illusion of a sprite being in a different position than it really is, allowing it to be rendered under the floor despite clipping",,,,Vynucená perspektiva,,Erzwungene Perspektive,,Devigita perspektivo,,,,,,,,,,,,,Perspectiva forçada,,,Принудительно перÑпективный,,,, +Restart level on Death,MISCMNU_RESTARTONDEATH,Disables autoloading save on death,,,,Restart po umÅ™ení,Genstart niveau ved død,Level bei Tod neu starten,,Rekomenci nivelon post morto,Reiniciar nivel al morir,,,Recharger après mort,,,Riavvio del livello in caso di morte,ãƒªã‚¹ã‚¿ãƒ¼ãƒˆæ™‚ã«æ­»äº¡ã™ã‚‹,,Level herstarten bij dood,Start nivÃ¥et pÃ¥ nytt ved død,Restartuj poziom po Å›mierci,Reiniciar fase após morte,,,ПерезапуÑк ÑƒÑ€Ð¾Ð²Ð½Ñ Ð¿Ð¾Ñле Ñмерти,,Starta om nivÃ¥n vid död,Ölüm halinde seviyeyi yeniden baÅŸlat, +Pistol Start,GMPLYMNU_PISTOLSTART,Resets inventory on every map,,,,ZaÄít jen s pistolí,Start med pistol,Pistolenstart,,Komenci nivelojn nur kun pistolo,Empezar niveles solo con la pistola,,,Démarrage du pistolet,,,Avvio pistola,ピストルスタート,,Start met pistool,Start med pistol,Start z pistoletem,Iniciar com a pistola,,,ПиÑтолет в начале,,Starta med pistol,Tabanca ile baÅŸlayın, +Allow creation of zombie players,CMPTMNU_VOODOOZOMBIES,,,,,Povolit zombie hráÄe,,Erlaube Zombieplayer,,Permesi kreon de zombiaj ludantoj,Permitir crear jugadores zombificados,,,,,,,ゾンビプレイヤーã®ç”Ÿæˆè¨±å¯,,,,Pozwól na tworzenie graczy-zombie,Permitir criação de jogadores zumbi,,,Разрешить Ñоздание игроков-зомби,,,, +ignore floor z when teleporting,CMPTMNU_FDTELEPORT,,,,,PÅ™i teleportu ignorovat výšku podlahy,,Ignoriere Fußbodenhöhe bem Teleportieren,,Ignori altecon de planko dum teleportado,Ignorar altura del suelo al teletransportarse,,,,,,,テレãƒãƒ¼ãƒˆæ™‚ã«floor z値を無視ã™ã‚‹,,,,Ignoruj wysokość podÅ‚ogi podczas teleportacji,Ignorar altura do chão ao teletransportar,,,игнорирование диагонали пола (Z) при телепортации,,,, +Forced-Perspective,OPTVAL_ANAMORPHIC,"Hardware Sprite Clipping mode that gives the illusion of a sprite being in a different position than it really is, allowing it to be rendered under the floor despite clipping",,,,Vynucená perspektiva,,Erzwungene Perspektive,,Trompa perspektivo de mov-rastrumoj,Forzar perspectiva de los «sprites»,,,,,,,,,,,,Perspectiva forçada,,,Принудительно перÑпективный,,,, Footstep Volume,SNDMENU_FOOTSTEPVOLUME,,,,,Hlasitost kroků,,Schrittlautstärke,,LaÅ­teco de paÅo,,,,,,,,,,,,,Volume dos passos,,,ГромкоÑть шагов,,,, -You got the incinerator!,ID24_GOTINCINERATOR,,,,,,,Du hast den Verbrenner!,,,,,,,,,,,,,,,,,,Получен «ИÑпепелитель»!,,,, -You got the calamity blade! Hot damn!,ID24_GOTCALAMITYBLADE,,,,,,,Du hast das Unheilsschwert! Verdammt!,,,,,,,,,,,,,,,,,,У Ð²Ð°Ñ ÐšÐ»Ð¸Ð½Ð¾Ðº бед! Ð¡ÐµÐ¹Ñ‡Ð°Ñ Ñ‡ÐµÑ€Ñ‚Ð¸ поплÑшут!,,,, -Picked up a fuel can.,ID24_GOTFUELCAN,,,,,,,Benzinkanister genommen.,,,,,,,,,,,,,,,,,,Получена Ñ‚Ð¾Ð¿Ð»Ð¸Ð²Ð½Ð°Ñ ÐºÐ°Ð½Ð¸Ñтра.,,,, -Picked up a fuel tank.,ID24_GOTFUELTANK,,,,,,,Benzintank genommen.,,,,,,,,,,,,,,,,,,Получен топливный бак.,,,, -Incinerator,TAG_ID24INCINERATOR,,,,,,,Verbrenner,,,,,,,,,,,,,,,,,,«ИÑпепелитель»,,,, -Calamity Blade,TAG_ID24CALAMITYBLADE,,,,,,,Unheilsschwert,,,,,,,,,,,,,,,,,,Клинок бед,,,, -Fuel Can,AMMO_ID24FUEL,,,,,,,Benzinkanister,,,,,,,,,,,,,,,,,,Ð¢Ð¾Ð¿Ð»Ð¸Ð²Ð½Ð°Ñ ÐºÐ°Ð½Ð¸Ñтра,,,, -Fuel Tank,AMMO_ID24FUELTANK,,,,,,,Benzintank,,,,,,,,,,,,,,,,,,Топливный бак,,,, \ No newline at end of file +You got the incinerator!,ID24_GOTINCINERATOR,,,,,,,Du hast den Verbrenner!,,,,,,,,,,,,,,,Pegou o incinerador!,,,Получен «ИÑпепелитель»!,,,, +You got the calamity blade! Hot damn!,ID24_GOTCALAMITYBLADE,,,,,,,Du hast das Unheilsschwert! Verdammt!,,,,,,,,,,,,,,,Pegou a lâmina da calamidade! Caramba!,,,У Ð²Ð°Ñ ÐšÐ»Ð¸Ð½Ð¾Ðº бед! Ð¡ÐµÐ¹Ñ‡Ð°Ñ Ñ‡ÐµÑ€Ñ‚Ð¸ поплÑшут!,,,, +Picked up a fuel can.,ID24_GOTFUELCAN,,,,,,,Benzinkanister genommen.,,,,,,,,,,,,,,,Pegou uma lata de combustível.,,,Получена Ñ‚Ð¾Ð¿Ð»Ð¸Ð²Ð½Ð°Ñ ÐºÐ°Ð½Ð¸Ñтра.,,,, +Picked up a fuel tank.,ID24_GOTFUELTANK,,,,,,,Benzintank genommen.,,,,,,,,,,,,,,,Pegou um tanque de combustível.,,,Получен топливный бак.,,,, +Incinerator,TAG_ID24INCINERATOR,,,,,,,Verbrenner,,,,,,,,,,,,,,,Incinerador,,,«ИÑпепелитель»,,,, +Calamity Blade,TAG_ID24CALAMITYBLADE,,,,,,,Unheilsschwert,,,,,,,,,,,,,,,Lâmina da Calamidade,,,Клинок бед,,,, +Fuel Can,AMMO_ID24FUEL,,,,,,,Benzinkanister,,,,,,,,,,,,,,,Lata de Combustível,,,Ð¢Ð¾Ð¿Ð»Ð¸Ð²Ð½Ð°Ñ ÐºÐ°Ð½Ð¸Ñтра,,,, +Fuel Tank,AMMO_ID24FUELTANK,,,,,,,Benzintank,,,,,,,,,,,,,,,Tanque de Combustível,,,Топливный бак,,,, \ No newline at end of file diff --git a/wadsrc_extra/static/language.csv b/wadsrc_extra/static/language.csv index 28b9b9bdc..332fae18a 100644 --- a/wadsrc_extra/static/language.csv +++ b/wadsrc_extra/static/language.csv @@ -10285,24 +10285,28 @@ The way is open,TXT_ACS_MAP60_3_THEWA,,,,Cesta je otevÅ™ena.,Vejen er Ã¥ben,Der è£åˆ‡ã‚‰ã‚Œã¦ç§ãŒãƒ—ログラマーã«å°‹å•ã•れるã“ã¨ã« ãªã£ã¦ã—ã¾ã£ãŸã‚“ã ã€‚","난 문제를 ë” ì¼ìœ¼í‚¤ê³  ì‹¶ì§€ 않아, 나ì—게서 떨어져! 해리스 ê·¸ ìžì‹ì´ 나를 ì†ì˜€ì–´. ë…€ì„ì´ ëˆì„ 준다고 해서 ë„와줬ë”니 ë„ë§ì¹˜ê³  없었고 ê²°êµ­ì—” 붙잡혀서 프로그래머ì—게 ì‹¬ë¬¸ì„ ë°›ì„ ì²˜ì§€ì— ë†“ì˜€ë‹¤ê³ !","Ik wil geen problemen, blijf uit mijn buurt. Ik heb genoeg problemen gehad met wat die klootzak Harris me heeft aangedaan. Hij beloofde me geld, maar in plaats daarvan kan ik me verheugen op een Ondervraging door de programmeur.","Jeg vil ikke ha trøbbel. Hold deg unna meg. Jeg har hatt nok trøbbel med det den jævelen Harris gjorde mot meg. Han lovet meg penger, i stedet kan jeg se frem til Ã¥ bli avhørt av programmereren.","Nie chcÄ™ żadnych kÅ‚opotów, zostaw mnie w spokoju. MiaÅ‚em już wystarczajÄ…co dużo problemów z tym co ten draÅ„ Harris mi zrobiÅ‚. ObiecaÅ‚ mi pieniÄ…dze, ale zamiast tego muszÄ™ czekać na to, by być PrzesÅ‚uchanym przez ProgramistÄ™.","Não quero me meter em problemas, fique longe de mim. Já tive problemas demais com o que aquele desgraçado do Harris fez comigo. Ele me prometeu dinheiro, mas ao invés disso vou ser Questionado pelo Programador.",,"Nu vreau să am probleme, stai departe de mine. Am avut suficiente probleme din cauza a ceea ce mi-a făcut ticălosul de Harris. Mi-a promis niÈ™te bani, dar în schimb, acum aÈ™tept cu nerăbdare să fiu interogat de către Programator.","Ðе подходи ко мне — мне не нужны неприÑтноÑти! У Ð¼ÐµÐ½Ñ Ð¸Ñ… и так доÑтаточно из-за Ñтого мерзавца ХарриÑа. Он обещал мне деньги, а вмеÑто Ñтого Ð¼ÐµÐ½Ñ Ð¾Ð¶Ð¸Ð´Ð°ÐµÑ‚ «допроÑ» у ПрограммиÑта.",,"Jag vill inte ha nÃ¥gra problem, hÃ¥ll dig borta frÃ¥n mig. Jag har haft tillräckligt med problem med vad den jäveln Harris gjorde mot mig. Han lovade mig pengar, istället fÃ¥r jag se fram emot att bli förhörd av Programmeraren.","Bela istemiyorum, benden uzak dur. O piç Harris'in bana yaptıkları yüzünden yeterince sorun yaÅŸadım. Bana para sözü verdi, onun yerine Programcı tarafından sorgulanmayı dört gözle bekliyorum." -I'll help you if you help me. Five pieces of gold and I'll tell all I know.,TXT_DLG_SCRIPT02_D0_ILLHE,MAP02: Guy next to Norwall Prison.,,,"Pomůžu ti, když pomůžeÅ¡ ty mÄ›. PÄ›t zlaťáků a Å™eknu ti vÅ¡e, co vím.","Jeg vil hjælpe dig, hvis du hjælper mig. Fem guldmønter, og jeg fortæller alt, hvad jeg ved.",Ich helfe dir wenn du mir hilfst. Fünf Goldstücke und ich erzähle alles was ich weiß.,,"Mi helpos vin se vi helpos min. Donu kvin da oro kaj mi rakontos ĉion, kion mi scias.",Te ayudo si me ayudas. Cinco monedas de oro y te cuento todo lo que sé.,,"Autan sinua, jos autat minua. Viidestä kultakolikosta kerron kaiken, minkä tiedän.","Tu m'aide, je t'aide. Cinq pièces et je te dis ce que je sais.",,"Segítek, ha segítesz. 5 arany és mindent elmondok, amit tudok.",Ti aiuterò se tu aiuti me. Cinque pezzi d'oro e ti dirò tutto quello che so.,"助ã‘ã¦ãれるãªã‚‰æ´åŠ©ã™ã‚‹ã€‚ +I'll help you if you help me. Five pieces of gold and I'll tell all I know.,TXT_DLG_SCRIPT02_D0_ILLHE,MAP02: Guy next to Norwall Prison.,,,"Pomůžu ti, když pomůžeÅ¡ ty mÄ›. PÄ›t zlaťáků a Å™eknu ti vÅ¡e, co vím.","Jeg vil hjælpe dig, hvis du hjælper mig. Fem guldmønter, og jeg fortæller alt, hvad jeg ved.",Ich helfe dir wenn du mir hilfst. Fünf Goldstücke und ich erzähle alles was ich weiß.,,"Mi helpos vin se vi helpos min. Donu kvin da oro kaj mi rakontos tion, kion mi scias.",Te ayudo si me ayudas. Cinco monedas de oro y te cuento lo que sé.,,"Autan sinua, jos autat minua. Viidestä kultakolikosta kerron kaiken, minkä tiedän.","Tu m'aide, je t'aide. Cinq pièces et je te dis ce que je sais.",,"Segítek, ha segítesz. 5 arany és mindent elmondok, amit tudok.",Ti aiuterò se tu aiuti me. Cinque pezzi d'oro e ti dirò tutto quello che so.,"助ã‘ã¦ãれるãªã‚‰æ´åŠ©ã™ã‚‹ã€‚ 5ゴールドã§çŸ¥ã£ã¦ã„ã‚‹æƒ…å ±ã‚’å…¨ã¦æ•™ãˆã‚ˆã†",저를 ë„와준다면 ë‹¹ì‹ ì„ ë•겠습니다. 5 골드를 ì¤Œìœ¼ë¡œì¨ ë§ì´ì£ .,Ik zal je helpen als je me helpt. Vijf goudstukken en ik vertel alles wat ik weet.,"Jeg hjelper deg hvis du hjelper meg. Fem gullstykker, sÃ¥ forteller jeg alt jeg vet.",PomogÄ™ ci jeÅ›li ty mi pomożesz. Pięć sztuk zÅ‚ota i powiem ci wszytsko co wiem.,Eu te ajudo se você me ajudar. Cinco moedas de ouro e te digo tudo que eu sei.,,"O să te ajut dacă È™i tu mă ajuÈ›i. Cinci bucăți de aur È™i-È›i voi spune tot ce È™tiu.","Я помогу тебе, еÑли ты поможешь мне. ПÑть золотых, и Ñ Ñ€Ð°ÑÑкажу вÑÑ‘, что знаю.",,Jag hjälper dig om du hjälper mig. Fem guldmynt och jag berättar allt jag vet.,Bana yardım edersen ben de sana yardım ederim. BeÅŸ altın ve tüm bildiklerimi anlatacağım. Here's the gold.,TXT_RPLY0_SCRIPT02_D0_HERES,〃,,,Tady jsou.,Her er guldet.,Hier ist dein Gold.,,Jen la oro.,Aquí está el oro.,,Tässä on kulta.,Voilà l'argent.,,Itt az arany.,Ecco l'oro.,金ã¯ã“れã ã€‚,여기 골드입니다.,Hier is het goud.,Her er gullet.,Oto zÅ‚oto.,Aqui está o ouro.,,PoftiÈ›i aurul.,Вот золото.,,Här är guldet.,İşte altın. -"Be stealthy when you kill, you won't set off alarms.",TXT_RYES0_SCRIPT02_D0_BESTE,〃,,,"Když zabíjíš, dÄ›lej to potichu - nespustíš tak poplach.","Vær snigende nÃ¥r du dræber, sÃ¥ du ikke udløser alarmer.","Sei leise, wenn du jemanden tötest, damit du keinen Alarm auslöst.",,"Mortigu silente por +"Be stealthy when you kill, you won't set off alarms.",TXT_RYES0_SCRIPT02_D0_BESTE,"〃 +(This text is only four seconds on screen)",,,"Když zabíjíš, dÄ›lej to potichu - nespustíš tak poplach.","Vær snigende nÃ¥r du dræber, sÃ¥ du ikke udløser alarmer.","Sei leise, wenn du jemanden tötest, damit du keinen Alarm auslöst.",,"Mortigu silente por ne ekagigi alarmojn.","Sé sigiloso al matar para no activar las alarmas.",,"Jos tapat vihollisesi salaa, vältyt hälytyksiltä.",Tuez discrètement et vous n'aurez pas de problème avec les alarmes.,,"Lopakodva, halkan gyilkolj, kézzel, így nem indul be a riasztó.","Sii furtivo quando uccidi, non farai scattare l'allarme.",始末ã™ã‚‹éš›ã¯éš å¯†ã«ã€è­¦å ±ã‚’鳴らã•ãªã„よã†ã«ã€‚,"조용히 ì€ì‹ í•´ì„œ 암살만 한다면, 경보를 울리지 ì•Šì„ ê²ë‹ˆë‹¤.","Wees sluipend als je doodt, je zult geen alarmbellen laten afgaan.","Vær snikende nÃ¥r du dreper, sÃ¥ du ikke utløser alarmer.","BÄ…dź cicho kiedy zabijasz, nie włączysz wtedy alarmu.",Seja cauteloso quando for matar e não ativará os alarmes.,,"Fii discret când ucizi, nu vei declanÈ™a alarma astfel.","Убивай беÑшумно, чтобы не поднÑть тревогу.",,"Var smygande när du dödar, du kommer inte att utlösa larm.","Öldürürken gizli ol, alarmları çalıştırmazsın." -"Well, I won't be telling you anything for free!",TXT_RNO0_SCRIPT02_D0_WELLI,〃,,,Zadarmo ti nic říkat nebudu!,Jeg vil ikke fortælle dig noget gratis!,"Na ja, für umsonst werde ich dir nichts erzählen!",,"Nu, mi ne parolos senpage!","Bueno, ¡no te diré +"Well, I won't be telling you anything for free!",TXT_RNO0_SCRIPT02_D0_WELLI,"〃 +(Four seconds)",,,Zadarmo ti nic říkat nebudu!,Jeg vil ikke fortælle dig noget gratis!,"Na ja, für umsonst werde ich dir nichts erzählen!",,"Nu, mi ne parolos senpage!","Bueno, ¡no te diré nada gratis!","Bueno, ¡no voy a decir nada gratis!","No, en aio kertoa mitään ilmaiseksi!",Je ne vous dirai rien si vous ne me donnez rien!,,"Figyelj, ingyen semmit sem mondok el neked.","Beh, io non ti dirò un bel niente senza compenso!",ã„ã‚„ã€ã‚¿ãƒ€ã§æƒ…å ±ã¯æ¸¡ã›ãªã„!,세ìƒì— 공짜가 어딨습니까? 먼저 ëˆì„ 주세요!,"Nou, ik zal je niets gratis vertellen!","Vel, jeg vil ikke fortelle deg noe gratis!","Cóż, nie bÄ™dÄ™ ci mówiÅ‚ niczego za darmo!","Bom, não vou te dar informação de graça!",,"Păi, n-o să-È›i zic nimic pe gratis!","Что ж, Ñ Ð½Ð¸Ñ‡ÐµÐ³Ð¾ не Ñкажу даром!",,Jag kommer inte att berätta nÃ¥got gratis!,Sana bedavaya hiçbir ÅŸey anlatmayacağım! Have you by any chance got another 5 gold on you?,TXT_DLG_SCRIPT02_D1516_HAVEY,〃,,,NemÄ›l bys u sebe náhodou dalších pÄ›t zlatek?,Har du tilfældigvis fem guldstykker mere pÃ¥ dig?,Hast du vielleicht 5 weitere Goldmünzen dabei?,,Ĉu eble vi havas aliajn kvin da oro?,"¿No tendrás otros cinco de oro, por casualidad?",,Sattuisiko sinulla olemaan toiset viisi kolikkoa?,T'aurais pas 5 pièces en plus sur toi?,,Nincs véletlenül nálad még 5 arany?,Hai per caso altri 5 pezzi d'oro con te?,ã‚‚ã—ã‹ã—ã¦5ゴールド稼ã„ã ã®ã‹?,5 골드 ë” ìžˆìŠµë‹ˆê¹Œ? 그러면 아주 ì¢‹ì„ í…ë°.,Heb je toevallig nog 5 goud bij je?,Har du tilfeldigvis fem gull til pÃ¥ deg?,Czy przypadkiem masz może kolejne 5 monet przy sobie?,Por acaso teria mais 5 de ouro aí?,,"Ai cumva, din întâmplare, 5 monezi de aur?",У Ñ‚ÐµÐ±Ñ Ñлучайно не найдётÑÑ ÐµÑ‰Ñ‘ 5 монеток?,,Har du av en slump 5 guldpengar till pÃ¥ dig?,Yanında 5 altın daha var mı acaba? 5 gold.,TXT_RPLY0_SCRIPT02_D1516_5GOLD,〃,,,PÄ›t zlatých.,5 guld.,5 Gold.,,Kvin da oro.,Cinco de oro.,,5 kolikkoa.,5 pièces.,,5 arany.,5 pezzi d'oro.,5ゴールドã ã€‚,5 골드.,5 goud.,5 gull.,5 monet.,5 de ouro.,,5 monezi de aur.,5 золотых.,,Fem guld.,5 altın. -"Well, poison bolts can kill the guards instantly and won't set off the alarms.",TXT_RYES0_SCRIPT02_D1516_WELLP,〃,,,"No, otrávené šípy okamžitÄ› zabijí stráže a nikdo nevyhlásí poplach.","NÃ¥, giftpile kan dræbe vagterne med det samme og vil ikke udløse alarmen.",Also Giftpfeile können die Wachen sofort töten und lösen keinen Alarm aus.,,"Mortigi per venenaj sagoj +"Well, poison bolts can kill the guards instantly and won't set off the alarms.",TXT_RYES0_SCRIPT02_D1516_WELLP,"〃 +(Four seconds)",,,"No, otrávené šípy okamžitÄ› zabijí stráže a nikdo nevyhlásí poplach.","NÃ¥, giftpile kan dræbe vagterne med det samme og vil ikke udløse alarmen.",Also Giftpfeile können die Wachen sofort töten und lösen keinen Alarm aus.,,"Mortigi per venenaj sagoj estas tuje kaj silente.","Matar con flechas envenenadas es instantáneo y sin alarmas.",,No niin; myrkkynuolilla voi tappaa vartijat välittömästi hälytystä laukaisematta.,Les carreaux empoisonnés tuent les gardes instantanément et discrètement.,,"Hát, a mérgezÅ‘ nyilak azonnal a túlvilágra küldik az Å‘röket, és a riasztó sem kapcsol be.",Le frecce avvelenate possono uccidere le guardie istantaneamente e non faranno scattare l'allarme.,"ãã†ã ãªã€ãƒã‚¤ã‚ºãƒ³ãƒœãƒ«ãƒˆã¯è­¦å‚™å“¡ã‚’ç°¡å˜ã«æ®ºã›ã‚‹ã—〠警報を作動ã•ã›ã‚‹ã“ã¨ã‚‚ãªã„。",아시나요? ë§¹ë… ë³¼íŠ¸ë§Œ 있으면 경보를 울리지 않고 ê²½ë¹„ë³‘ì„ ì£½ì¼ ìˆ˜ 있다는거?,"Nou, gifbouten kunnen de bewakers onmiddellijk doden en zullen het alarm niet laten afgaan.","Vel, giftbolter kan drepe vaktene øyeblikkelig og vil ikke utløse alarmen.","WiÄ™c, zatrute beÅ‚ty mogÄ… od razu zabić strażników i nie włączÄ… alarmu.","Bem, setas venenosas podem matar guardas na hora e não disparam alarmes.",,"Păi, bolÈ›urile otrăvite pot omorâ gardienii instant È™i nu vor declanÈ™a alarma.","Что ж, отравленными болтами можно убивать охранников быÑтро и беÑшумно.",,"Tja, giftbultar kan döda vakterna omedelbart och kommer inte att utlösa larmen.",Zehirli oklar muhafızları anında öldürebilir ve alarmı çalıştırmaz. -"No sir, I won't be telling you anything for free!",TXT_RNO0_SCRIPT02_D1516_NOSIR,〃,,,"Ne, ne, zadarmo ti nic říkat nebudu!","Nej, sir, jeg vil ikke fortælle dig noget gratis!","Nein, ich werde dir nichts für umsonst erzählen!",,"Ne, mi diros nenion senpage!","¡No, no diré nada gratis!","¡No, no voy a decir nada gratis!","Ehei, en kerro mitään ilmaiseksi!","Non, monsieur, je ne vous dirai rien si je ne me fais pas payer!",,"Nem uram, nem árulok el semmit ingyen!","No signore, non ti dirò nulla senza avere qualcosa in cambio!",ã„ã‚„ã„ã‚„ã€ã‚¿ãƒ€ã§ã¯è©±ã›ãªã„ãª!,"죄송합니다만, ì „ ì•„ë¬´ê²ƒë„ ë¬´ë£Œë¡œ ë§í•´ì£¼ì§€ ì•Šì„ ê²ë‹ˆë‹¤!","Nee meneer, ik zal u niets gratis vertellen!","Nei, sir, jeg vil ikke fortelle deg noe gratis!","Nie panie, nie bÄ™dÄ™ ci mówiÅ‚ niczego za darmo!","Nada disso, senhor, não vou te dizer nada de graça!",,"Nu domnule, n-o să-È›i zic nimic pe gratis!","Ðет, товарищ! Даром — только за амбаром!",,"Nej, sir, jag tänker inte berätta nÃ¥got gratis för dig!","Hayır efendim, size bedava bir ÅŸey söylemeyeceÄŸim!" +"No sir, I won't be telling you anything for free!",TXT_RNO0_SCRIPT02_D1516_NOSIR,"〃 +(Four seconds)",,,"Ne, ne, zadarmo ti nic říkat nebudu!","Nej, sir, jeg vil ikke fortælle dig noget gratis!","Nein, ich werde dir nichts für umsonst erzählen!",,"Ne, mi diros nenion senpage!","¡No, no diré nada gratis!","¡No, no voy a decir nada gratis!","Ehei, en kerro mitään ilmaiseksi!","Non, monsieur, je ne vous dirai rien si je ne me fais pas payer!",,"Nem uram, nem árulok el semmit ingyen!","No signore, non ti dirò nulla senza avere qualcosa in cambio!",ã„ã‚„ã„ã‚„ã€ã‚¿ãƒ€ã§ã¯è©±ã›ãªã„ãª!,"죄송합니다만, ì „ ì•„ë¬´ê²ƒë„ ë¬´ë£Œë¡œ ë§í•´ì£¼ì§€ ì•Šì„ ê²ë‹ˆë‹¤!","Nee meneer, ik zal u niets gratis vertellen!","Nei, sir, jeg vil ikke fortelle deg noe gratis!","Nie panie, nie bÄ™dÄ™ ci mówiÅ‚ niczego za darmo!","Nada disso, senhor, não vou te dizer nada de graça!",,"Nu domnule, n-o să-È›i zic nimic pe gratis!","Ðет, товарищ! Даром — только за амбаром!",,"Nej, sir, jag tänker inte berätta nÃ¥got gratis för dig!","Hayır efendim, size bedava bir ÅŸey söylemeyeceÄŸim!" You've wrung the last bit of gossip out of me already!,TXT_DLG_SCRIPT02_D3032_YOUVE,〃,,,"Už jsi ze mÄ› vyždímal vÅ¡echny klepy, co jsem mÄ›l!",Du har allerede vredet den sidste smule sladder ud af mig!,Du hast bereits das letzte bisschen Klatsch aus mir herausgequetscht!,,Vi jam eltiris de mi eĉ la lastan klaĉon.,Ya me has sacado hasta el último cotilleo.,Ya me sacaste hasta el último chisme.,Olet jo puristanut minusta kaikki juorut!,Vous m'avez tiré tous les vers du nez!,,Már a legutolsó pletykát is kiszedted belÅ‘lem.,Mi hai già spremuto fino all'ultimo pettegolezzo! ,æ—¢ã«ã‚¤ã‚­ã®ã„ã„噂ã¯èžã‹ã›ãŸãž!,가십거리를 ì´ë¯¸ 다 털어놓았습니다. ë” ë­˜ ë°”ëžë‹ˆê¹Œ?,Je hebt de laatste roddels al uit me uitgewrongen!,Du har allerede vrengt det siste sladderet ut av meg!,WyciÄ…gnÄ…Å‚eÅ› już ze mnie ostatniÄ… plotkÄ™!,Você já ouviu todos os boatos que posso te contar!,,Ai stors ultimele bârfe din mine deja!,"Ты уже вытÑнул из Ð¼ÐµÐ½Ñ Ð²ÑÑ‘, что только можно!",,Du har redan pressat ut det sista skvallret ur mig!,Benden son dedikodumu da aldınız zaten! What can I get for you?,TXT_DLG_SCRIPT02_D4548_WHATC,MAP02: Irale.,,,Co si budeÅ¡ přát?,Hvad kan jeg fÃ¥ for dig?,Was kann ich für dich tun?,,Kion mi donu al vi?,¿Qué puedo ofrecerte?,,Mitä saisi olla?,Qu'est ce que je peux pour vous?,,Mit tehetnék érted?,Cosa posso offrirti?,何ã‹å¿…è¦ã‹?,ë¬´ì—‡ì„ êµ¬ë§¤í•˜ê³  ì‹¶ì–´?,Wat kan ik voor u halen?,Hva vil du ha?,Co mogÄ™ dla ciebie zrobić?,Como posso te ajudar?,,Ce pot să-È›i aduc?,Что Ñ Ð¼Ð¾Ð³Ñƒ тебе предложить?,,Vad kan jag fÃ¥ för dig?,Sizin için ne alabilirim? Assault gun,TXT_RPLY0_SCRIPT02_D4548_ASSAU,〃,,,ÚtoÄnou puÅ¡ku,En pistol til angreb,Sturmgewehr,,Sturmofusilon,Fusil de asalto,,Rynnäkkökivääri,Fusil d'Assaut,,Gépfegyver,Fucile d'assalto,アサルトガン,ëŒê²©ì†Œì´,Aanvalswapen,En automatpistol.,Karabin Szturmowy,Fuzil de assalto,,PuÈ™că de asalt,Штурмовую винтовку,,En attackpistol,Saldırı silahı. @@ -10491,12 +10495,14 @@ para comprar curitas?",Olisiko teillä ehkä varaa sidekääreisiin?,"Eh bien, p ã™ã‚‹ãªã‚ˆã€‚オーダーã¨ã„ã†è‡ªè­¦å›£ãŒã™ã㫠駆ã‘ã¤ã‘ã¦ãã‚‹ãžã€‚","안녕하신가 ì´ë°©ì¸ì´ì—¬, ìžë„¤ëŠ” ì´ê³³ì—서 ì²˜ìŒ ë³´ëŠ” 얼굴ì´êµ°. ë‚´ê°€ ìžë„¤ì—게 공짜 ì¡°ì–¸ì„ í•´ì£¼ì§€. ë‚´ê°€ ìžë„¤ë¼ë©´ 조심할 거야. 오ë”는 ìžìœ ì˜ì§€ë¥¼ 용납하지 않아. 그리고 ê·¸ë“¤ì˜ ì •ì˜ëŠ” ì‹ ì†í•˜ì§€.","Hallo vreemdeling, ik heb je hier nog niet eerder gezien. Laat me je een stukje gratis advies geven. Ik zou voorzichtig zijn als ik jou was. De Orde tolereert geen vrije wil en hun gerechtigheid is snel.","Hei, fremmede, jeg har ikke sett deg her før. La meg gi deg et gratis rÃ¥d. Jeg ville vært forsiktig hvis jeg var deg. Ordenen tolererer ikke fri vilje, og deres rettferdighet er rask.","Witaj nieznajomy, nigdy cie tu wczeÅ›niej nie widziaÅ‚em. Dam ci darmowÄ… radÄ™. Na twoim miejscu bym uważaÅ‚. Zakon nie toleruje wolnej woli, a ich prawo dziaÅ‚a prÄ™dko.","Olá, forasteiro. Nunca vi você por aqui antes. Deixa eu te dar um conselho de graça. Eu tomaria cuidado se eu fosse você. A Ordem não tolera livre arbítrio e a justiça deles é rápida.",,"Salutări străine, nu te-am mai văzut pe-aici. Permite-mi să-È›i dau un sfat. AÈ™ fi grijuliu în locul tău. Ordinul nu tolerează voinÈ›a liberă, iar justiÈ›ia lor e rapidă.","Привет, незнакомец. Что-то Ñ Ð½Ðµ видел Ñ‚ÐµÐ±Ñ Ñ‚ÑƒÑ‚ раньше. Позволь дать тебе беÑплатный Ñовет: Ñ Ð½Ð° твоём меÑте был бы пооÑторожнее. Орден не терпит неповиновениÑ, и они Ñкоры на Ñуд.",,"Hej främling, jag har inte sett dig här förut. LÃ¥t mig ge dig ett gratis rÃ¥d. Jag skulle vara försiktig om jag var du. Orden tolererar inte fri vilja och deras rättvisa är snabb.","Merhaba yabancı, seni daha önce buralarda görmemiÅŸtim. Sana bedava bir tavsiye vereyim. Yerinde olsam dikkatli olurdum. Tarikat'ın özgür iradeye tahammülü yoktur ve adaletleri çok hızlıdır." What's the word?,TXT_RPLY0_SCRIPT02_D25772_WHATS,〃,,,NÄ›jaké zvÄ›sti?,Hvad er ordet?,Was erzählt man sich so?,,Kion oni diras ĉi tie?,¿Qué se dice por aquí?,,Mikä on päivän sana?,C'est quoi la rumeur?,,Mi a hír?,Che si dice in giro?,ä»–ã«ã¯?,무슨 뜻ì´ì£ ?,Wat is het woord?,Hva heter det?,JakieÅ› wieÅ›ci?,Quais são as últimas?,,Ai vreo veste?,ЕÑть новоÑти?,,Vad heter det?,Neydi o kelime? -The word is... The sewers hold more than just rats and robots.,TXT_RYES0_SCRIPT02_D25772_THEWO,〃,,,Kolují zvÄ›sti... že stoky ukrývají víc než jen krysy a roboty.,Ordet er... Kloakkerne rummer mere end bare rotter og robotter.,"Man erzählt sich, dass die Kanalisation mehr als Ratten und Roboter zu bieten hat.",,"Oni diras... ke en la kloako +The word is... The sewers hold more than just rats and robots.,TXT_RYES0_SCRIPT02_D25772_THEWO,"〃 +(This text is only four seconds on screen)",,,Kolují zvÄ›sti... že stoky ukrývají víc než jen krysy a roboty.,Ordet er... Kloakkerne rummer mere end bare rotter og robotter.,"Man erzählt sich, dass die Kanalisation mehr als Ratten und Roboter zu bieten hat.",,"Oni diras... ke en la kloako ne estas nur ratoj kaj robotoj.","Se dice... que en las alcantarillas hay más que solo ratas y robots.",,Sana on... että viemärit pitävät sisällään muutakin kuin vain rottia ja robotteja.,La rumeur est.. Que les égouts cachent quelque chose de plus gros que des rats et des robots.,,Az a hír...hogy a kanális nem csak patkányokat és robotokat rejteget.,In giro si dice che... Le fogne contengono molto di più che ratti e robot.,ä»–ã¯...下水é“ã«ã¯é¼ ã‚„ãƒ­ãƒœãŒæ²¢å±±ã ã€‚,"무슨 ë§ì´ëƒ 하면, í•˜ìˆ˜ë„ ì•ˆì—는 시ê¶ì¥ë§Œ 있는 게 ì•„ë‹ˆë¼ ë¡œë´‡ë“¤ë„ ìˆ¨ì–´ 있다는 ê±°ì§€.",Het woord is.... De riolen bevatten meer dan alleen ratten en robots.,Ordet er... Kloakken rommer mer enn bare rotter og roboter.,"WieÅ›ci sÄ… takie, że... kanaÅ‚y skrywajÄ… coÅ› wiÄ™cej niż tylko szczury i roboty.",As últimas são... Que os esgotos abrigam mais do que apenas ratos e robôs.,,Vestea e că... Canalele ascund mai mult decât roboÈ›i È™i È™oareci.,"ГоворÑÑ‚, в канализации водÑÑ‚ÑÑ Ð½Ðµ только крыÑÑ‹ и роботы.",,Ordet är... Kloakerna innehÃ¥ller mer än bara rÃ¥ttor och robotar.,Kelime ÅŸu. Lağımlarda fareler ve robotlardan daha fazlası var. What can I do for you now?,TXT_DLG_SCRIPT02_D27288_WHATC,〃,,,Co pro vás můžu udÄ›lat teÄ?,Hvad kan jeg gøre for dig nu?,Was kann ich jetzt für dich tun?,,Kion mi faru por vi?,¿Qué puedo hacer por ti?,,"No niin, miten voin olla teille avuksi?",Que puis-je faire pour vous?,,Mit tehetek ma Önért?,E ora cosa posso fare per te?,何ã‹ç”¨ã‹?,ì´ì œ ìžë„¤ì—게 ë¬´ì—‡ì„ í•´ë“œë¦´ê¹Œ?,Wat kan ik nu voor je doen?,Hva kan jeg gjøre for deg nÃ¥?,Co mogÄ™ dla ciebie zrobić?,O que posso fazer por você agora?,,Cu ce te mai pot ajuta?,Тебе что-нибудь нужно?,,Vad kan jag göra för dig nu?,Åžimdi senin için ne yapabilirim? More info.,TXT_RPLY0_SCRIPT02_D27288_MOREI,〃,,,Další informace,Mere info.,Mehr Informationen.,,Pliaj informoj,Más información,,Muuta tietoa.,Plus d'infos.,,Több információt.,Altre informazioni.,話ã®ãƒã‚¿ã€‚,ë” ë§Žì€ ì •ë³´ë¥¼ 주세요.,Meer info.,Mer informasjon.,WiÄ™cej informacji.,Mais informações.,,Mai multe informaÈ›ii.,РаÑÑкажи больше,,Mer information.,Daha fazla bilgi. -The Governor is a simple reminder to us that we aren't free people anymore.,TXT_RYES0_SCRIPT02_D27288_THEGO,〃,,,"Náš guvernér je hezkou přípomínkou toho, že už nejsme svobodní.","Guvernøren er en simpel pÃ¥mindelse til os om, at vi ikke længere er frie mennesker.","Der Gouverneur ist eine einfache Erinnerung daran, das wir keine freien Menschen mehr sind.",,"La registo ekzistas +The Governor is a simple reminder to us that we aren't free people anymore.,TXT_RYES0_SCRIPT02_D27288_THEGO,"〃 +(Four seconds)",,,"Náš guvernér je hezkou přípomínkou toho, že už nejsme svobodní.","Guvernøren er en simpel pÃ¥mindelse til os om, at vi ikke længere er frie mennesker.","Der Gouverneur ist eine einfache Erinnerung daran, das wir keine freien Menschen mehr sind.",,"La registo ekzistas nur por memorigi, ke ne plu estas libereco.","El gobernador solo es un recordatorio de @@ -10508,7 +10514,8 @@ tengas suficiente oro.",,"Palatkaa takaisin, kun teillä on kultaa.",Revenez qua èžã回ã£ã¦ã‚‹æ§˜ã ãŒã€‚悪ã„ã“ã¨ã¯è¨€ã‚ã­ãˆã€ ã‚ã‚“ã¾ã—æ­»ã«æ€¥ãã‚“ã˜ã‚ƒã­ãˆãžã€‚",죽지 않으려고 발버둥 ì¹˜ë“¯ì´ ì§ˆë¬¸ì„ ë§Žì´ í•˜ëŠ”êµ¬ë¨¼. 무장한 ê³³ì— ë¬´ëª¨í•˜ê²Œ 가서 죽지나 ë§ê²Œ.,"Nou, je stelt veel vragen voor iemand die niet probeert te sterven. Zorg ervoor dat je niet gaat en jezelf niet dood laat gaan, of erger nog.",Du stiller mange spørsmÃ¥l til Ã¥ være en som ikke prøver Ã¥ dø. Pass pÃ¥ at du ikke blir drept eller det som verre er.,"Cóż, zadajesz dużo pytaÅ„ jak na kogoÅ›, kto próbuje nie zginąć. Upewnij siÄ™, że nie dasz siÄ™ zabić jak pójdziesz. Lub gorzej.","Bom, você está fazendo um monte de perguntas pra alguém que não está tentando morrer. Procure não acabar sendo morto por aí, ou coisa pior.",,"Păi, pui multe întrebări pentru cineva care încearcă să nu moară. Ai grijă să nu ajungi un om mort, sau mai rău.","Ты задаёшь Ñлишком много вопроÑов Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, кто не хочет Ñдохнуть. УбедиÑÑŒ, что не идёшь на верную Ñмерть или чего похуже.",,"Du ställer mÃ¥nga frÃ¥gor för nÃ¥gon som inte försöker dö. Se till att du inte gÃ¥r och blir dödad, eller värre.",Ölmeye çalışmayan biri için çok fazla soru soruyorsun. Gidip kendini öldürtmediÄŸinden ya da daha kötüsünü yapmadığından emin ol. More info.,TXT_RPLY0_SCRIPT02_D28804_MOREI,〃,,,Další informace,Mere info.,Mehr Informationen.,,Pliaj informoj,Más información,,Muuta tietoa.,Plus d'infos.,,Több infót.,Altre informazioni.,話ã®ãƒã‚¿ã€‚,정보를 좀 ë” ì•Œë ¤ì£¼ì‹œì˜¤.,Meer info.,Mer informasjon.,WiÄ™cej informacji.,Mais informações.,,Mai multe informaÈ›ii.,Говори дальше.,,Mer information.,Daha fazla bilgi. -There's more to the Order than meets the eye.,TXT_RYES0_SCRIPT02_D28804_THERE,〃,,,"Za Řádem se toho skrývá víc, než se zdá.","Der er mere i Ordenen, end man umiddelbart tror.",An dem Orden ist mehr dran als man auf den ersten Blick denkt.,,"Estas pli pri La Ordeno, +There's more to the Order than meets the eye.,TXT_RYES0_SCRIPT02_D28804_THERE,"〃 +(Four seconds)",,,"Za Řádem se toho skrývá víc, než se zdá.","Der er mere i Ordenen, end man umiddelbart tror.",An dem Orden ist mehr dran als man auf den ersten Blick denkt.,,"Estas pli pri La Ordeno, ol kiom ili Åajnigas.","Hay más de La Orden de lo que aparenta.",,"Veljeskunta ei ole sitä, miltä päältä päin näyttää.",L'Ordre cache bien son jeu.,,"Több van a Rend mögött, mint ami a külsÅ‘ sejtet.",C'è sotto molto di più sull'Ordine rispetto a come possa sembrare.,オーダーã¯è¦‹ãˆã¦ã‚‹ä»¥ä¸Šã«å¼·å¤§ã ã€‚,오ë”ì˜ ë’¤ì—는 ë” ë§Žì€ ê²ƒë“¤ì´ ìˆ¨ê²¨ì ¸ 있다네. 비밀 ê°™ì€ ê²ƒë“¤ ë§ì¼ì„¸.,De Orde heeft meer te bieden dan je op het eerste gezicht zou denken.,Det er mer ved Ordenen enn det som møter øyet.,"Zakonu jest wiÄ™cej, niż siÄ™ wydaje.",Há mais sobre a Ordem do que aparenta.,,Este ceva mai mult la Ordin decât ceea ce se poate vedea cu ochiul liber.,"Орден больше, чем кажетÑÑ.",,Det finns mer i Orden än vad man kan tro.,Tarikat'ta göründüğünden daha fazlası var. We'll talk when you get gold!,TXT_RNO0_SCRIPT02_D28804_WELLT,〃,,,"Popovídáme si, když budeÅ¡ mít zlato!","Vi taler sammen, nÃ¥r du fÃ¥r guld!","Wir werden reden, wenn du Gold hast!",,"Ni parolos kiam @@ -10526,7 +10533,7 @@ Who in the blazes are you? No one's supposed to be loitering about in this area! 許ã•れã¦ã„ãªã„ãž! ",너는 누구지? ì•„ë¬´ë„ ì´ ì§€ì—­ì—서 어슬ë ê±°ë¦´ 수 없다!,Wie ben jij in vredesnaam? Niemand wordt verondersteld op dit gebied rond te hangen!,Hvem i all verden er du? Ingen har lov til Ã¥ drive rundt i dette omrÃ¥det!,Kim u diaska jesteÅ›? Nikt nie powinien siÄ™ tu włóczyć!,Mas quem é você? Não pode ficar perambulando por aqui!,,Cine naiba mai eÈ™ti tu? Nimeni nu ar trebui să zăbovească în zona asta!,"Кто ты, чёрт побери, такой? Ðикому не разрешено тут шлÑтьÑÑ!",,Vem i helvete är du? Det är inte meningen att nÃ¥gon ska gÃ¥ omkring i det här omrÃ¥det!,Sen de kimsin be? Kimsenin bu bölgede dolaÅŸmaması gerekiyor! "You there, nobody's allowed in here. Move along!",TXT_DLG_SCRIPT02_D37900_YOUTH,MAP02: Golden guards.,,,"Hej ty, sem nikdo nesmí. Jdi dál!","Du der, ingen mÃ¥ komme ind her. Af sted med jer!","Hey, du da. Hier darfst du nicht rein. Geh weiter!",,"He, neniu rajtas esti ĉi tie. Plue movu vin!","Oye, este lugar está restringido. ¡Sigue moviéndote!",,"Sinä siellä, tämä on kielletty alue. Tiehesi!","Vous, là, personne n'est autorisé à traîner ici, dégagez!",,"Te ott, ide senki sem jöhet be, kotródj!","Ehi, tu! A nessuno è permesso di stare qua. Vai via!",ãŠå‰ã¯è¨±å¯ã—ã¦ã„ãªã„ãžã€‚ç«‹ã¡åŽ»ã‚Œ!,"거기 너, ì•„ë¬´ë„ ì´ê³³ì— 지나갈 수 없다. ê°€ë˜ ê¸¸ì´ë‚˜ ê°€!","Jij daar, niemand mag hier naar binnen. Ga verder!","Du der, ingen har lov til Ã¥ være her. GÃ¥ videre!","Ty tutaj, nikt nie ma prawa tu przebywać. Wynocha!","Ei, você, não pode ficar aqui. Vá embora!",,"Tu, nimeni nu are voie aici. MiÈ™că-te!","Эй, ты! Ðикому не позволено тут ошиватьÑÑ. Проваливай!",,"Du där, ingen fÃ¥r komma in här. Flytta pÃ¥ er!","Sen oradaki, kimsenin buraya girmesine izin yok. İlerleyin!" -Irale will set you right up!,TXT_DLG_SCRIPT02_D39416_IRALE,MAP02: Irale's shop.,,,Irale tÄ› hned vyzbrojí!,Irale vil sætte dig pÃ¥ plads!,Irale wird sich darum kümmern!,,"Irale vendos al vi ĉion, kion vi bezonas!",¡Irale te venderá lo necesario!,¡Irale te va a vender lo necesario!,Irale järkkää sinulle varusteet!,Irale a ce qu'il te faut!,,Irale majd ellát.,Irale ti darà quello che ti serve!,イラールãŒè²´æ–¹ã‚’手助ã‘ã—ã¾ã™!,여기 ì´ë¡¤ë¦¬ê°€ 중요한 ë¬¼í’ˆì„ íŒë§¤í•  거야.,Irale zal je er goed in luizen!,Irale skal ordne opp for deg!,Irale dobrze ciÄ™ przygotuje!,O Irale vai te ajudar!,,Irale te va pregăti adecvat!,ИрÑйл Ñнабдит Ñ‚ÐµÐ±Ñ Ð²Ñем необходимым!,,Irale kommer att sätta dig pÃ¥ plats!,İrale seni hemen ayarlayacak! +Irale will set you right up!,TXT_DLG_SCRIPT02_D39416_IRALE,MAP02: Irale's shop.,,,Irale tÄ› hned vyzbrojí!,Irale vil sætte dig pÃ¥ plads!,Irale wird sich darum kümmern!,,"Iralo vendos al vi ĉion, kion vi bezonas!",¡Irale te venderá lo necesario!,¡Irale te va a vender lo necesario!,Irale järkkää sinulle varusteet!,Irale a ce qu'il te faut!,,Irale majd ellát.,Irale ti darà quello che ti serve!,イラールãŒè²´æ–¹ã‚’手助ã‘ã—ã¾ã™!,여기 ì´ë¡¤ë¦¬ê°€ 중요한 ë¬¼í’ˆì„ íŒë§¤í•  거야.,Irale zal je er goed in luizen!,Irale skal ordne opp for deg!,Irale dobrze ciÄ™ przygotuje!,O Irale vai te ajudar!,,Irale te va pregăti adecvat!,ИрÑйл Ñнабдит Ñ‚ÐµÐ±Ñ Ð²Ñем необходимым!,,Irale kommer att sätta dig pÃ¥ plats!,İrale seni hemen ayarlayacak! I'm kinda a talent broker for the rebels. A guy who's as good as you could make a lot of gold... if you hooked up with the right people.,TXT_DLG_SCRIPT02_D40932_IMKIN,MAP02: Harris.,,,Jsem takový hledaÄ talentů pro rebely. NÄ›kdo tak dobrý jako ty by si mohl vydÄ›lat hodnÄ› zlata... pokud by se spojil s tÄ›mi správnými lidmi.,"Jeg er en slags talentmægler for oprørerne. En fyr, der er lige sÃ¥ god som dig, kan tjene en masse guld ... hvis du kommer i kontakt med de rigtige folk.","Ich bin sowas wie ein Talentsucher für die Rebellen. Jemand, der so gut ist wie du kann hier eine Menge Gold verdienen... wenn du mit den richtigen Leuten in Verbindung stehst.",,"Mi estas speco de talentul-serĉanto por la ribelantoj. Iu tiel lerta, kiel vi povus gajni multe da oro… se vi ligas vin kun la Äustaj homoj.",Soy algo así como un cazatalentos para los rebeldes. Alguien tan bueno como tú podría ganar mucho oro... Si te juntas con las personas adecuadas.,,Olen eräänlainen kykyjenvälittäjä kapinallisille. Kaltaisesi kaveri voisi tienata paljon kultaa... oikeiden kumppaneiden kanssa.,"Je suis une sorte de chasseur de têtes pour les rebelles. Un type comme toi pourrait se faire pas mal de fric... Avec les bons contacts, bien entendu.",,"Egyfajta toborzó vagyok a lázadóknál. Egy ürge aki ilyen jó mint te, elég sok aranyat kereshet...ha megfelelÅ‘ emberekkel jösz össze.",Sono sempre alla ricerca di persone talentuose per i ribelli. Una persona come te potrebbe ricevere molto oro... se fosse in contatto con le persone giuste.,"俺ã¯å乱者ã¸ã®æ–¡æ—‹å±‹ã•。æ‰èƒ½ã‚’生ã‹ã—㦠誰よりも儲ã‘ãŸã„ã¨æ€ã£ã¦ã‚‹ãªã‚‰... 相応ã—ã„人ã¨é–¢ä¿‚ã‚’æŒã¤ã“ã¨ã•。 @@ -10602,7 +10609,7 @@ Thanks.,TXT_RPLY0_SCRIPT02_D62156_THANK,"〃 (〃)",,,Díky.,Jeg vil ikke vide det. Tak.,Danke.,,Dankon.,Gracias.,,Kiitos.,Merci.,,Kösz.,Grazie.,ã‚りãŒã¨ã†ã€‚,고마워.,Bedankt.,Takk skal du ha.,DziÄ™ki.,Agradeço.,,MulÈ›umesc.,СпаÑибо.,,Jag vill inte veta. Tack.,TeÅŸekkürler. "Give you a hint. When I stop talking to you, you leave.",TXT_DLG_SCRIPT02_D63672_GIVEY,〃,,,"Dám ti radu: Když s tebou pÅ™estanu mluvit, znamená to odchod.","Jeg giver dig et hint. NÃ¥r jeg holder op med at tale med dig, gÃ¥r du.","Hier mal ein Tipp. Wenn ich aufgehört habe mit dir zu reden, dann gehst du.",,"Jen konsilo: Kiam mi ĉesas paroli al vi, vi foriras.","Un consejo: Cuando dejo de hablarte, te vas.",,"Pienenä vinkkinä, että kun lakkaan puhumasta sinulle, sinä lähdet.","Petit conseil. Quand j'arrète de vous parler, vous sortez d'ici.",,"Csak egy tanács. Amikor már nem beszélek hozzád, húzz el.","Ti do un consiglio. Quando io smetto di parlare, tu vai via.",ヒントを出ãã†ã€‚話ã™ã®ã‚’æ­¢ã‚ãŸã‚‰åŽ»ã‚ŠãŸã¾ãˆã€‚,"힌트를 하나 주지. 나랑 대화가 ë났으면, 넌 떠나는 거야.","Ik geef je een hint. Als ik niet meer met je praat, ga je weg.","Jeg skal gi deg et hint. NÃ¥r jeg slutter Ã¥ snakke med deg, drar du.",Dam ci wskazówkÄ™. Jak przestanÄ™ mówić to wyjdź.,"Uma dica. Quando eu terminar de falar contigo, você vai embora.",,"ÃŽÈ›i dau un sfat. Când termin de discutat cu tine, pleci.","Дам тебе подÑказку: когда Ñ Ð·Ð°ÐºÐ¾Ð½Ñ‡Ñƒ говорить, ты уходишь.",,"Jag ger dig en ledtrÃ¥d. När jag slutar prata med dig, gÃ¥r du.","Sana bir ipucu vereyim. Seninle konuÅŸmayı bıraktığımda, gideceksin." Do you have good news for me? I'm all ears.,TXT_DLG_SCRIPT02_D65188_DOYOU,"〃 -(after completing ""bloody"" chore)",,,Máš pro mÄ› dobré zprávy? UÅ¡i mám nastražené.,Har du gode nyheder til mig? Jeg er lutter ører.,Hast du gute Neuigkeiten für mich? Ich bin ganz Ohr.,,Ĉu vi alportas bonajn novaĵojn? Mi estas tute aÅ­skultema.,¿Tienes buenas noticias para mí? Soy todo oídos.,,Onko sinulla minulle hyviä uutisia? Olen pelkkänä korvana.,De bonnes nouvelles? Mes oreilles sont à vous.,,Van valami jó híred számomra? Csupa fül vagyok.,Hai buone notizie per me? Sono tutto orecchi...,ç§ã®è€³ã«å‰å ±ã‚’ä¼ãˆã«æ¥ãŸã‹ã­?,ë‚  위한 ì¢‹ì€ ì†Œì‹ì´ 있나? 열심히 듣고 있다구.,Heb je goed nieuws voor mij? Ik ben een en al oor.,Har du gode nyheter til meg? Jeg er lutter øre.,Masz dobre wieÅ›ci dla mnie? Zamieniam siÄ™ w sÅ‚uch.,Alguma notícia boa pra mim? Sou todo ouvidos.,,Ai veÈ™ti bune pentru mine? Sunt numai urechi.,ЕÑть Ð´Ð»Ñ Ð¼ÐµÐ½Ñ Ñ…Ð¾Ñ€Ð¾ÑˆÐ¸Ðµ новоÑти? Слушаю во вÑе уши.,,Har du goda nyheter till mig? Jag är alldeles nyfiken.,Benim için iyi haberlerin var mı? Can kulağıyla dinliyorum. +(after completing ""bloody"" chore)",,,Máš pro mÄ› dobré zprávy? UÅ¡i mám nastražené.,Har du gode nyheder til mig? Jeg er lutter ører.,Hast du gute Neuigkeiten für mich? Ich bin ganz Ohr.,,"Ĉu vi alportas bonajn novaĵojn? Mi estas tute aÅ­skultema... Ha, ha, ha, ha, ha, ha, ha!","¿Traes buenas noticias? Soy todo oídos... ¡Ja, ja, ja, ja, ja, ja, ja!",,Onko sinulla minulle hyviä uutisia? Olen pelkkänä korvana.,De bonnes nouvelles? Mes oreilles sont à vous.,,Van valami jó híred számomra? Csupa fül vagyok.,Hai buone notizie per me? Sono tutto orecchi...,ç§ã®è€³ã«å‰å ±ã‚’ä¼ãˆã«æ¥ãŸã‹ã­?,ë‚  위한 ì¢‹ì€ ì†Œì‹ì´ 있나? 열심히 듣고 있다구.,Heb je goed nieuws voor mij? Ik ben een en al oor.,Har du gode nyheter til meg? Jeg er lutter øre.,Masz dobre wieÅ›ci dla mnie? Zamieniam siÄ™ w sÅ‚uch.,Alguma notícia boa pra mim? Sou todo ouvidos.,,Ai veÈ™ti bune pentru mine? Sunt numai urechi.,ЕÑть Ð´Ð»Ñ Ð¼ÐµÐ½Ñ Ñ…Ð¾Ñ€Ð¾ÑˆÐ¸Ðµ новоÑти? Слушаю во вÑе уши.,,Har du goda nyheter till mig? Jag är alldeles nyfiken.,Benim için iyi haberlerin var mı? Can kulağıyla dinliyorum. The deed is done!,TXT_RPLY0_SCRIPT02_D65188_THEDE,"〃 (〃)",,,Skutek je dokonán!,Det er gjort!,Es ist geschehen!,,Interkonsento plenumita!,¡El trato está hecho!,,Se on tehty!,C'est bel est bien fait.,,Véghezvittem a büntettet.,È fatta!,ã‚„ã‚‹ã“ã¨ã¯ãƒ¤ã£ãŸ,임무 완료!,De daad is gedaan!,Gjerningen er utført!,Wszystko zrobione!,Está feito!,,Fapta e făcută.,Дело Ñделано!,,Det är gjort!,İş tamamdır! "Oh, I just love souvenirs. Here, this will get you into the prison. Talk to Warden Montag. Whatever you do after that, I don't want to know.",TXT_DLG_SCRIPT02_D66704_OHIJU,"〃 @@ -10655,14 +10662,14 @@ Liar! Go get the ring!,TXT_RNO0_SCRIPT02_D77316_LIARG,〃,,,Lháři! Jdi pro jeh Forprenu lian ringon!","¡Mentiroso! ¡Ve a por el anillo!","¡Mentiroso! ¡Ve por el anillo!",Valehtelija! Mene hakemaan sormus!,Mensonges! Ramenez-moi l'anneau!,,Hazug! Menj és szerezd meg a gyűrűt!,Bugiardo! Vai a prendere l'anello!,嘘をã¤ããªã€æŒ‡è¼ªã‚’å–ã£ã¦ã“ã„!,ê±°ì§“ë§ìŸì´! 가서 반지를 가져오게!,Leugenaar! Ga de ring halen!,Løgner! Hent ringen!,KÅ‚amca! Idź po pierÅ›cieÅ„!,Mentiroso! Vá pegar o anel!,,Mincinos! Adu inelul!,Лжец! ПринеÑи мне кольцо!,,Lögnare! Hämta ringen!,Yalancı! Git yüzüğü al! -"Here, you earned it. The traitor you killed was about to reveal the location of the Front. You saved lives. How would you like to earn more gold, and a future free from tyranny?",TXT_DLG_SCRIPT02_D78832_HEREY,〃,,,"Tady máš, zasloužil sis je. Zrádce, kterého jsi zabil, by vyzradil lokaci Fronty. Zachránil jsi životy. Jak by se ti líbilo vydÄ›lat si víc zlata spolu s budoucností bez tyranie?","Her, du har fortjent det. Den forræder, du dræbte, var ved at afsløre frontens placering. Du reddede liv. Hvad ville du sige til at tjene mere guld og en fremtid uden tyranni?","Hier, das hast du dir verdient. Der Verräter den du getötet hast war dabei über die Position der Front auszupacken. Du hast Leben gerettet. Wie würde es dir gefallen noch mehr Gold zu verdienen und eine tyrannenfreie Zukunft zu haben?",,"Jen Äi, vi gajnis Äin. La perfidulo, kiun vi mortigis, estis malkaÅonta la pozicion de la Fronto. Vi savis vivojn. Ĉu vi Åatus gajni plian oron kaj estontecon liberan de tiranio?","Toma, te lo ganaste. El traidor que has matado estaba a punto de revelar la ubicación del Frente. Has salvado vidas. ¿Te gustaría ganar más oro y un futuro libre de tiranía?","Toma, te lo ganaste. El traidor que mataste estaba a punto de revelar la ubicación del Frente. Salvaste vidas. ¿Te gustaría ganar más oro y un futuro libre de tiranía?","Tässä, olet sen ansainnut. Tappamasi petturi oli lähellä paljastaa Rintaman sijainnin. Pelastit henkiä. Haluaisitko tienata lisää kultaa, ja tyranniasta vapaan tulevaisuuden?","Voilà, vous l'avez bien mérité. Ce traître que vous avez tué allait révéler la base du Front. Vous avez sauvé de nombreuses vies. Est-ce que ça vous intéresserait de vous faire plus d'argent et garantir un futur libre de la tyrannie?",,"Tessék, megérdemelted. Az áruló akit eltettél láb alól közel járt ahhoz, hogy felfedje a Front bázisát. Életeket mentettél. Mit szólnál ahhoz, ha még több arany is ütné a mancsod, és még egy zsarnok mentes jövÅ‘d is lenne?","Ecco qua, te li sei guadagnati. Il traditore che hai ucciso stava per rivelare il nascondiglio del Fronte. Hai salvato molte vite. Che ne diresti di guadagnare altro oro, e un futuro libero dalla tirannia?","よã—ã€å ±é…¬ã¯ãŠå‰ã®ã‚‚ã®ã ã€‚ãŠå‰ãŒæ®ºã—㟠+"Here, you earned it. The traitor you killed was about to reveal the location of the Front. You saved lives. How would you like to earn more gold, and a future free from tyranny?",TXT_DLG_SCRIPT02_D78832_HEREY,〃,,,"Tady máš, zasloužil sis je. Zrádce, kterého jsi zabil, by vyzradil lokaci Fronty. Zachránil jsi životy. Jak by se ti líbilo vydÄ›lat si víc zlata spolu s budoucností bez tyranie?","Her, du har fortjent det. Den forræder, du dræbte, var ved at afsløre frontens placering. Du reddede liv. Hvad ville du sige til at tjene mere guld og en fremtid uden tyranni?","Hier, das hast du dir verdient. Der Verräter den du getötet hast war dabei über die Position der Front auszupacken. Du hast Leben gerettet. Wie würde es dir gefallen noch mehr Gold zu verdienen und eine tyrannenfreie Zukunft zu haben?",,"Jen Äi, vi gajnis Äin. La perfidulo, kiun vi mortigis, estis malkaÅonta la lokon de la Fronto. Vi savis vivojn. Ĉu vi Åatus gajni plian oron kaj estontecon liberan de tiranio?","Toma, te lo ganaste. El traidor que has matado estaba a punto de revelar la ubicación del Frente. Has salvado vidas. ¿Te gustaría ganar más oro y un futuro libre de tiranía?","Toma, te lo ganaste. El traidor que mataste estaba a punto de revelar la ubicación del Frente. Salvaste vidas. ¿Te gustaría ganar más oro y un futuro libre de tiranía?","Tässä, olet sen ansainnut. Tappamasi petturi oli lähellä paljastaa Rintaman sijainnin. Pelastit henkiä. Haluaisitko tienata lisää kultaa, ja tyranniasta vapaan tulevaisuuden?","Voilà, vous l'avez bien mérité. Ce traître que vous avez tué allait révéler la base du Front. Vous avez sauvé de nombreuses vies. Est-ce que ça vous intéresserait de vous faire plus d'argent et garantir un futur libre de la tyrannie?",,"Tessék, megérdemelted. Az áruló akit eltettél láb alól közel járt ahhoz, hogy felfedje a Front bázisát. Életeket mentettél. Mit szólnál ahhoz, ha még több arany is ütné a mancsod, és még egy zsarnok mentes jövÅ‘d is lenne?","Ecco qua, te li sei guadagnati. Il traditore che hai ucciso stava per rivelare il nascondiglio del Fronte. Hai salvato molte vite. Che ne diresti di guadagnare altro oro, e un futuro libero dalla tirannia?","よã—ã€å ±é…¬ã¯ãŠå‰ã®ã‚‚ã®ã ã€‚ãŠå‰ãŒæ®ºã—㟠ã‚ã®è£åˆ‡ã‚Šè€…ã¯ã€ãƒ•ロントã®éš ã‚Œå®¶ã‚’ãƒãƒ©ãã†ã¨ ã—ã¦ã„ãŸã‚“ã ã€‚ãŠã‹ã’ã§å¤§å‹¢ã®å‘½ãŒæ•‘ã‚れãŸã€‚ ã‚‚ã£ã¨å¤šãã®é‡‘ã¨ã€å¥´ç­‰ã®æ”¯é…ã‹ã‚‰è§£æ”¾ã•れ㟠未æ¥ã‚’å¾—ãŸã„ã¨ã¯æ€ã‚ãªã„ã‹?",여기 ë³´ìƒì¼ì„¸. 그는 í”„ë¡ íŠ¸ì˜ ë¹„ë°€ìž¥ì†Œë¥¼ ì•Œë¦¬ë ¤ë˜ ì°¸ì´ì—ˆë„¤. ìžë„¤ê°€ ìš°ë¦¬ë“¤ì˜ ëª©ìˆ¨ì„ êµ¬í–ˆì–´. í­ì •ì„ íƒ€ë„해서 ì´ì²˜ëŸ¼ ë” ë§Žì€ ë³´ìƒì„ ì–»ì„ ì¤€ë¹„ê°€ ë˜ì–´ìžˆë‚˜?,"Hier heb je het verdiend. De verrader die je vermoordde stond op het punt om de locatie van het Front te onthullen. Je hebt levens gered. Hoe zou je het vinden om meer goud te verdienen, en een toekomst vrij van tirannie?","Her, du har fortjent den. Forræderen du drepte var i ferd med Ã¥ avsløre hvor Fronten er. Du reddet liv. Hva sier du til Ã¥ tjene mer gull og en fremtid uten tyranni?","Masz, zasÅ‚użyÅ‚eÅ› sobie. Zdrajca, którego zabiÅ‚eÅ› zamierzaÅ‚ ujawnić gdzie znajduje siÄ™ baza Frontu. UratowaÅ‚eÅ› wiele żyć. Chcesz zarobić wiÄ™cej pieniÄ™dzy i mieć przyszÅ‚ość wolnÄ… od tyranii?","Tome, você merece. O traidor que você matou estava prestes a revelar a localização da Frente. Você salvou vidas. Que tal ganhar mais ouro e um futuro livre de tirania?",,"Aici, o meriÈ›i. Trădătorul pe care l-ai omorât urma să dezvăluie locaÈ›ia Frontului. Ai salvat vieÈ›i. Cum ai vrea să câștigi mai mult aur, È™i un viitor fără tiranie?","Вот, ты их заÑлужил. Убитый тобой предатель мог выдать Ордену меÑтоположение базы СопротивлениÑ. Ты ÑÐ¿Ð°Ñ Ð¼Ð½Ð¾Ð³Ð¾ жизней. Тебе бы не хотелоÑÑŒ ещё золота и будущего, Ñвободного от тирании?",,"Här, du har förtjänat den. Förrädaren du dödade skulle avslöja var fronten finns. Du räddade liv. Hur skulle du vilja tjäna mer guld och en framtid fri frÃ¥n tyranni?","İşte, onu hak ettin. Öldürdüğün hain Cephe'nin yerini açıklamak üzereydi. Hayat kurtardın. Daha fazla altın ve zorbalıktan uzak bir gelecek kazanmaya ne dersin?" Tell me more.,TXT_RPLY0_SCRIPT02_D78832_TELLM,〃,,,Řekni mi víc.,Fortæl mig mere.,Erzähl mir mehr.,,Mi aÅ­skultas vin.,Dime más.,,Kerro lisää.,Dites-moi en plus.,,Mesélj még.,Dimmi di più.,ã‚‚ã£ã¨æ•™ãˆã¦ãれ。,ë” ë§í•´ì£¼ì‹œì˜¤.,Vertel me meer.,Fortell meg mer.,Mów dalej.,Me conte mais.,,Spune-mi mai multe.,РаÑÑкажи мне больше.,,Berätta mer.,Biraz daha anlat. Not my style.,TXT_RPLY1_SCRIPT02_D78832_NOTMY,〃,,,To není můj styl.,Ikke min stil.,Nicht mein Stil.,,Tio ne estas mia stilo.,No es mi estilo.,,Ei kuulu tyyliini.,Pas mon genre.,,Nem az Én stílusom.,Non fa per me.,俺ã®è¶£å‘³ã˜ã‚ƒãªã„ãªã€‚,ë‚´ ì·¨í–¥ì€ ì•„ë‹Œë°.,Niet mijn stijl.,Ikke min stil.,To nie w moim stylu.,Não faz o meu estilo.,,Nu-i genul meu.,Ðе в моём вкуÑе.,,Inte min stil.,Benim tarzım deÄŸil. -"I have a business relationship with the Front's leader, Macil. I know he needs an incisive fellow like yourself, and he pays well. Take this recovered com unit and you'll be led to, shall we say, opportunities.",TXT_DLG_SCRIPT02_D80348_IHAVE,〃,,,"Mám obchodní vztah s vůdcem Fronty, Macilem. Vím, že potÅ™ebuje schopného ÄlovÄ›ka jako ty a dobÅ™e platí. Vem si tenhle spravený komunikátor; zavede tÄ› k, Å™eknÄ›me, příležitostem.","Jeg har et forretningsmæssigt forhold til frontens leder, Macil. Jeg ved, han har brug for en skarp fyr som dig, og han betaler godt. Tag denne genvundne com-enhed, og du vil blive ført til, skal vi sige, muligheder.","Ich habe Geschäftsbeziehungen mit dem Anführer der Front, Macil. Er braucht einen pfiffigen Typen wie dich und er bezahlt gut. Nimm dieses erbeutete Kommunikationsgerät und es wird dich zu, sagen wir mal, Möglichkeiten führen.",,"Mi havas negoco-ligon kun Macil la frontestro. Mi scias, ke li bezonas lertan kunulon kiel vi, aldone al tio, ke li pagas bone. Prenu ĉi tiun rekuperitan interfonon kaj vi havos la vojon al... ni diru, Åancoj.","Tengo una relación de negocios con Macil el líder del Frente. Sé que necesita un compañero hábil como tú, además de que paga bien. Toma este intercomunicador recuperado para que te guíe a... digamos, oportunidades.",,"Minulla on liikesuhteita Rintaman johtaja Maciliin. Tiedän hänen tarvitsevan kaltaistasi terävää kaveria, ja hän maksaa hyvin. Ota tämä talteenkerätty viestintälaite, ja tiesi johtaa, sanottaisiinko, mahdollisuuksien äärelle.","J'ai une relation d'affaires avec le leader du Front, Macil. Je sais qu'il a besoin de quelqu'un d'incisif comme vous, et il paye bien. Prenez ce communicateur avec vous, et vous serez mené à des.. on va dire des opportunités.",,"Van egy üzleti megállapodásom Macil-lal, a Front vezetÅ‘jével. pont egy ilyen belevaló embert keres mint te, és kiváló fizetést is adna. Használd ezt a kommunikációs egységet és - mondjuk úgy - odavezet a lehetÅ‘ségekhez.","Ho dei rapporti d'affari col capo del Fronte, Macil. So che è alla ricerca di una persona diretta e precisa come te, e lui paga molto bene. Tieni questa unità di comunicazione che abbiamo recuperato, e sarai condotto verso, come dire, nuove opportunità.","ç§ã¯ãƒ•ロントã®ãƒªãƒ¼ãƒ€ãƒ¼ã€ãƒžã‚·ãƒ«ã¨å–引を +"I have a business relationship with the Front's leader, Macil. I know he needs an incisive fellow like yourself, and he pays well. Take this recovered com unit and you'll be led to, shall we say, opportunities.",TXT_DLG_SCRIPT02_D80348_IHAVE,〃,,,"Mám obchodní vztah s vůdcem Fronty, Macilem. Vím, že potÅ™ebuje schopného ÄlovÄ›ka jako ty a dobÅ™e platí. Vem si tenhle spravený komunikátor; zavede tÄ› k, Å™eknÄ›me, příležitostem.","Jeg har et forretningsmæssigt forhold til frontens leder, Macil. Jeg ved, han har brug for en skarp fyr som dig, og han betaler godt. Tag denne genvundne com-enhed, og du vil blive ført til, skal vi sige, muligheder.","Ich habe Geschäftsbeziehungen mit dem Anführer der Front, Macil. Er braucht einen pfiffigen Typen wie dich und er bezahlt gut. Nimm dieses erbeutete Kommunikationsgerät und es wird dich zu, sagen wir mal, Möglichkeiten führen.",,"Mi havas negoco-ligon kun Macil la frontestro. Mi scias, ke li bezonas lertan kamaradon kiel vi, kaj cetere li pagas bone. Prenu ĉi tiun rekuperitan interfonon kaj vi havos la vojon al... ni diru, ebloj.","Tengo una relación de negocios con Macil el líder del Frente. Sé que necesita un compañero hábil como tú, además de que paga bien. Toma este intercomunicador recuperado para que te guíe a... digamos, oportunidades.",,"Minulla on liikesuhteita Rintaman johtaja Maciliin. Tiedän hänen tarvitsevan kaltaistasi terävää kaveria, ja hän maksaa hyvin. Ota tämä talteenkerätty viestintälaite, ja tiesi johtaa, sanottaisiinko, mahdollisuuksien äärelle.","J'ai une relation d'affaires avec le leader du Front, Macil. Je sais qu'il a besoin de quelqu'un d'incisif comme vous, et il paye bien. Prenez ce communicateur avec vous, et vous serez mené à des.. on va dire des opportunités.",,"Van egy üzleti megállapodásom Macil-lal, a Front vezetÅ‘jével. pont egy ilyen belevaló embert keres mint te, és kiváló fizetést is adna. Használd ezt a kommunikációs egységet és - mondjuk úgy - odavezet a lehetÅ‘ségekhez.","Ho dei rapporti d'affari col capo del Fronte, Macil. So che è alla ricerca di una persona diretta e precisa come te, e lui paga molto bene. Tieni questa unità di comunicazione che abbiamo recuperato, e sarai condotto verso, come dire, nuove opportunità.","ç§ã¯ãƒ•ロントã®ãƒªãƒ¼ãƒ€ãƒ¼ã€ãƒžã‚·ãƒ«ã¨å–引を ã—ã¦ã„ã‚‹ã‚“ã ã€‚å½¼ã¯å›ã®ã‚ˆã†ãªåˆ‡ã‚Œã‚‹äººç‰©ã‚’ å¿…è¦ã¨ã—ã¦ã„ã¦ã€é‡‘払ã„も良ã„。 ã“ã®ä¿®ç†ã•れãŸç„¡ç·šæ©Ÿã‚’æŒã£ã¦ã„ã‘ã°ã€ @@ -10713,7 +10720,7 @@ If you say it's illegal I want nothing to do with you. I have enough trouble as トラブルã¯å¾¡å…ã ã€‚",저게 불법ì´ë¼ê³  ë§í•œë‹¤ë©´ 난 ì´ì œ ë”는 모른다네. 관련 ë¬¸ì œë„ ì›ì¹˜ 않고 ë§ì¼ì„¸.,"Als je zegt dat het illegaal is, wil ik niets met je te maken hebben. Ik heb al genoeg problemen.","Hvis du sier det er ulovlig, vil jeg ikke ha noe med deg Ã¥ gjøre. Jeg har nok problemer som det er.","JeÅ›li mówisz, że jest nielegalny, to nie chcÄ™ mieć z tobÄ… nic wspólnego. Mam wystarczajÄ…co dużo kÅ‚opotów.",Se você diz que isso é ilegal então eu não quero envolvimento nenhum contigo. Já tenho problemas o suficiente.,,"Dacă zici că e ilegal, nu vreau să am nimic de-aface cu tine. Am destule probleme deja.","Раз ты говоришь, что оно нелегально, Ñ Ð½Ðµ хочу иметь Ñ Ñ‚Ð¾Ð±Ð¾Ð¹ дел. Мне и так хватает проблем.",,Om du säger att det är olagligt vill jag inte ha nÃ¥got med dig att göra. Jag har tillräckligt med problem som det är.,"EÄŸer bunun yasadışı olduÄŸunu söylüyorsanız, sizinle hiçbir ÅŸey yapmak istemiyorum. Başımda yeterince bela var zaten." Thanks,TXT_RPLY0_SCRIPT02_D103088_THANK,〃 (〃),,,Díky.,Tak,Danke,,Dankon.,Gracias.,,Kiitos,Merci.,,Kösz,Grazie,ã‚りãŒã¨ã†ã€‚,ê°ì‚¬í•©ë‹ˆë‹¤.,Bedankt,Takk.,DziÄ™ki,Obrigado,,MulÈ›umesc,СпаÑибо.,,Tack,TeÅŸekkürler. "Release me, leave an old man alone.",TXT_DLG_SCRIPT02_D104604_RELEA,〃 (〃),,,"Drž se dál, nech starce na pokoji.","Slip mig fri, lad en gammel mand være i fred.",Bitte lass einen alten Mann in Ruhe,,"Sufiĉe, ne Äenu maljunulon.",Déjame en paz. No molestes a un anciano.,,"Päästä minut, jätä vanha mies rauhaan.","Laissez moi tranquille, un vieil homme a le droit d'être seul.",,"Engedj el, hagyd békén öreg csontjaimat.","Lasciami stare, non inquietare un vecchio.",ã»ã£ã¨ã„ã¦ãれã€å¹´å¯„り一人ã«ã—ã¦ãれ。,보내주게. ì´ ë…¸ì¸ì€ ì´ì œ ì•„ë¬´ê²ƒë„ ëª°ë¼.,"Laat me vrij, laat een oude man met rust.","Slipp meg fri, la en gammel mann være i fred.","Nie zbliżaj siÄ™, zostaw starca w spokoju.","Me solte, deixe um velho em paz.",,"Lasă-mă, lasă bătrânii în pace.",ОÑтавь Ñтарика в покое!,,"Släpp mig, lÃ¥t en gammal man vara ifred.","Bırakın beni, yaÅŸlı bir adamı yalnız bırakın." -"Welcome to the last flicker of hope. Only we have the free will to oppose the Order. We have the sharpest scientific minds, and many able bodies, but we lack that one real, uh... Problemsolver, who will give us the edge we need. Help us.",TXT_DLG_SCRIPT03_D0_WELCO,MAP03: Macil.,,,"Vítej u posledního plamínku nadÄ›je. Jen my máme svobodnou vůli vzdorovat Řádu. Máme sice nejÄilejší vÄ›decké génie a mnoho schopných vojáků, ale chybí nám ten jeden... pomocník, který by nás posunul vpÅ™ed. Pomoz nám.","Velkommen til det sidste lille hÃ¥b. Kun vi har den frie vilje til at modsætte os Ordenen. Vi har de skarpeste videnskabelige hjerner og mange dygtige kroppe, men vi mangler den ene rigtige, øh... Problemløser, som vil give os den fordel, vi har brug for. Hjælp os.","Wilkommen beim letzten Hoffnungsschimmer. Nur wir haben noch den freien Willen, um gegen den Orden zu arbeiten. Wir haben die besten Wissenschaftler und viele fähige Kämpfer aber was uns fehlt ist ein spezieller... äh... Problemlöser, der uns den nötigen Vorteil verschafft. Bitte hilf uns.",,"Bonvenon en la lasta fajrero da espero. Nur ni havas liberan volon por kontraÅ­stari al La Ordeno. Ni havas la plej inteligentajn mensojn kaj multe da kapablaj homoj, sed mankas tiu, kiu estus, hm... vera problem-solvanto, por ke li donu al ni la necesan avantaÄon. Helpu nin.","Bienvenido al último destello de esperanza. Solo nosotros tenemos el libre albedrío para oponernos a La Orden. Tenemos las mentes científicas más brillantes y muchas personas capaces, pero nos falta el que sería, este... todo un apagafuegos para que nos dé la ventaja que necesitamos. Ayúdanos.",,"Tervetuloa viimeisen toivonpilkahduksen äärelle. Ainoastaan meillä on vapaa tahto vastustaa Veljeskuntaa. Meillä on tieteen terävimmät mielet ja monia ruumiiltaan vahvoja, mutta meiltä puuttuu se yksi todellinen, sanottaisiinko, Ongelmanratkoja, joka antaisi meille kaipaamamme etulyöntiaseman. Auta meitä.","Bienvenue au dernier lieu d'espoir. Nous seuls avons la liberté d'esprit pour opposer l'Ordre. Nous avons les esprits scientifiques les plus aiguisés et de nombreux hommes habiles et sains.. Mais il nous manque quelqu'un qui pourrait.. résoudre nos problèmes. Nous donner un peu d'aide, nous permettre de prendre l'avantage.",,"Köszöntelek a remény utolsó pislákoló fényénél. Már csak mi merünk szembeszállni a Renddel. Itt vannak a legélesebb elmék, életerÅ‘s emberek, de hiányzik egy igazi...probléma megoldó, aki elÅ‘nyt szerez számunkra. Segíts nekünk!","Benvenuto nell'ultimo barlume di speranza. Noi siamo i soli ad avere la determinazione per combattere l'Ordine. Abbiamo brillanti scienziati e molti soldati capaci, ma ci manca quel vero e proprio... risolutore di problemi, che ci può dare quel vantaggio che cerchiamo. Aiutaci!","よã†ã“ãã€ã“ã“ã¯æˆ‘々ã®åƒ…ã‹ãªå¸Œæœ›ãŒé›†ã¾ã‚‹ +"Welcome to the last flicker of hope. Only we have the free will to oppose the Order. We have the sharpest scientific minds, and many able bodies, but we lack that one real, uh... Problemsolver, who will give us the edge we need. Help us.",TXT_DLG_SCRIPT03_D0_WELCO,MAP03: Macil.,,,"Vítej u posledního plamínku nadÄ›je. Jen my máme svobodnou vůli vzdorovat Řádu. Máme sice nejÄilejší vÄ›decké génie a mnoho schopných vojáků, ale chybí nám ten jeden... pomocník, který by nás posunul vpÅ™ed. Pomoz nám.","Velkommen til det sidste lille hÃ¥b. Kun vi har den frie vilje til at modsætte os Ordenen. Vi har de skarpeste videnskabelige hjerner og mange dygtige kroppe, men vi mangler den ene rigtige, øh... Problemløser, som vil give os den fordel, vi har brug for. Hjælp os.","Wilkommen beim letzten Hoffnungsschimmer. Nur wir haben noch den freien Willen, um gegen den Orden zu arbeiten. Wir haben die besten Wissenschaftler und viele fähige Kämpfer aber was uns fehlt ist ein spezieller... äh... Problemlöser, der uns den nötigen Vorteil verschafft. Bitte hilf uns.",,"Bonvenon en la lasta fajrero da espero. Nur ni havas liberan volon por kontraÅ­stari al La Ordeno. Ni havas la plej intelektajn mensojn kaj multe da kapablaj homoj, sed mankas tiu, kiu estus, hm... vera problem-solvulo, por ke li donu al ni la necesan avantaÄon. Helpu nin.","Bienvenido al último destello de esperanza. Solo nosotros tenemos el libre albedrío para oponernos a La Orden. Tenemos las mentes científicas más brillantes y muchas personas capaces, pero nos falta el que sería, este... todo un apagafuegos para que nos dé la ventaja que necesitamos. Ayúdanos.",,"Tervetuloa viimeisen toivonpilkahduksen äärelle. Ainoastaan meillä on vapaa tahto vastustaa Veljeskuntaa. Meillä on tieteen terävimmät mielet ja monia ruumiiltaan vahvoja, mutta meiltä puuttuu se yksi todellinen, sanottaisiinko, Ongelmanratkoja, joka antaisi meille kaipaamamme etulyöntiaseman. Auta meitä.","Bienvenue au dernier lieu d'espoir. Nous seuls avons la liberté d'esprit pour opposer l'Ordre. Nous avons les esprits scientifiques les plus aiguisés et de nombreux hommes habiles et sains.. Mais il nous manque quelqu'un qui pourrait.. résoudre nos problèmes. Nous donner un peu d'aide, nous permettre de prendre l'avantage.",,"Köszöntelek a remény utolsó pislákoló fényénél. Már csak mi merünk szembeszállni a Renddel. Itt vannak a legélesebb elmék, életerÅ‘s emberek, de hiányzik egy igazi...probléma megoldó, aki elÅ‘nyt szerez számunkra. Segíts nekünk!","Benvenuto nell'ultimo barlume di speranza. Noi siamo i soli ad avere la determinazione per combattere l'Ordine. Abbiamo brillanti scienziati e molti soldati capaci, ma ci manca quel vero e proprio... risolutore di problemi, che ci può dare quel vantaggio che cerchiamo. Aiutaci!","よã†ã“ãã€ã“ã“ã¯æˆ‘々ã®åƒ…ã‹ãªå¸Œæœ›ãŒé›†ã¾ã‚‹ 最後ã®å ´æ‰€ã ã€‚オーダーã¸ã¨ç«‹ã¡å‘ã‹ã†æ„æ€ã‚’ æŒã£ã¦ã„ã‚‹ã®ã¯æˆ‘々ãらã„ã ã€‚ ã“ã“ã«ã¯ç´ æ™´ã‚‰ã—ã„頭脳をæŒã¤å­¦è€…ãŸã¡ã€ãã—㦠@@ -10744,7 +10751,7 @@ Frankly the situation is a mess. You must accomplish several missions to prepare å›ã«ã¯ãã®æ•らãˆã‚‰ã‚ŒãŸè€…ãŸã¡ã‚’ 救助ã—ã¦ã‚‚らã„ãŸã„。","ì• ì„하게ë„, 현재 ìƒí™©ì€ ì—‰ë§ìž…니다. 우리가 오ë”를 공습하기 ì „ì— ë‹¹ì‹ ì´ ìˆ˜í–‰í•´ì•¼ í•  ì¼ì´ 몇 가지 있습니다. 몇 주 ì „ ê³µìŠµì€ ë¹„ì°¸í•œ 결과로 ë났고, ëŒ€ë¶€ë¶„ì˜ ë³‘ì‚¬ê°€ ê°ê¸ˆë‹¹í–ˆìŠµë‹ˆë‹¤. ê·¸ë“¤ì„ í’€ì–´ì£¼ì–´ì•¼ë§Œ 합니다.",Eerlijk gezegd is de situatie een puinhoop. Je moet verschillende missies volbrengen om de weg te bereiden voor meer aanvallen op de Orde. Onze laatste inval was een ramp en de meeste van onze troepen werden gevangen genomen. Ik heb je nodig om deze gevangenen te bevrijden.,"Ærlig talt er situasjonen et rot. Du mÃ¥ utføre flere oppdrag for Ã¥ bane vei for flere angrep pÃ¥ Ordenen. VÃ¥rt siste angrep var en katastrofe, og de fleste av vÃ¥re tropper ble tatt til fange. Du mÃ¥ befri disse fangene.","Szczerze to mamy tu baÅ‚agan. Musisz ukoÅ„czyć kilka misji, by przygotować drogÄ™ na wiÄ™cej ataków na Zakon. Nasz ostatni nalot byÅ‚ porażkÄ… i wiÄ™kszość naszych żoÅ‚nierzy zostaÅ‚a zÅ‚apana. PotrzebujÄ™ ciÄ™, byÅ› uwolniÅ‚ tych więźniów.",Sinceramente a situação é uma bagunça. Você precisa cumprir algumas missões para preparar o caminho para mais ataques contra a Ordem. Nossa última invasão foi um desastre e muitas das nossas tropas foram capturadas. Preciso que você liberte esses prisioneiros.,,Sincer situaÈ›ia e nasoală. Trebuie să îndepliniÈ›i câteva misiune pentru a pregătii calea pentru mai multe atacuri asupra Ordinului. Ultimul nostru raid a fost un dezastru È™i majoritatea trupelor noastre au fost capturate. Am nevoie ca voi să eliberaÈ›i prizonierii.,"ЧеÑтно говорÑ, наши дела идут не лучшим образом. Тебе предÑтоит выполнить неÑколько заданий, чтобы подготовить почву Ð´Ð»Ñ Ð´Ð°Ð»ÑŒÐ½ÐµÐ¹ÑˆÐ¸Ñ… атак на Орден. Ðаша поÑледнÑÑ Ð²Ñ‹Ð»Ð°Ð·ÐºÐ° обернулаÑÑŒ катаÑтрофой, и большинÑтво наших бойцов были Ñхвачены. Мне нужно, чтобы ты оÑвободил их.",,Ärligt talat är situationen en enda röra. Du mÃ¥ste utföra flera uppdrag för att bereda vägen för fler attacker mot Orden. VÃ¥r senaste räd var en katastrof och de flesta av vÃ¥ra trupper blev tillfÃ¥ngatagna. Jag behöver dig för att befria dessa fÃ¥ngar.,Açıkçası durum tam bir karmaÅŸa. Tarikat'a daha fazla saldırının yolunu hazırlamak için birkaç görevi tamamlamalısınız. Son baskınımız bir felaketti ve askerlerimizin çoÄŸu esir alındı. Bu esirleri serbest bırakmanı istiyorum. I think I can handle it.,TXT_RPLY0_SCRIPT03_D6064_ITHIN,〃 (〃),,,"Myslím, že to zvládnu.","Jeg tror, jeg kan klare det.","Ich denke, das kann ich schaffen.",,"Mi pensas, ke mi povos fari tion.",Creo poder hacerlo.,,Eiköhän se onnistu.,Je pense pouvoir m'en occuper.,,Azt hiszem el tudom intézni.,Penso di potercela fare.,俺ãªã‚‰ã§ãã‚‹ã¯ãšã ã€‚,ì•„ë§ˆë„ ì œê°€ í•  수 ìžˆì„ ê²ƒ 같군요.,Ik denk dat ik het aankan.,Jeg tror jeg kan klare det.,"MyÅ›lÄ™, że dam sobie z tym radÄ™.",Acho que eu posso dar conta disso.,,Cred că mă pot descurca.,"Думаю, ÑправлюÑÑŒ.",,Jag tror att jag klarar av det.,Sanırım ben halledebilirim. -"Take this money and visit Irale who supplies our weapons. Then, this key will get you in to see the Governor. He's a corrupt puppet of the Order, but he loves to make deals. Do whatever you need to free our brothers in arms.",TXT_DLG_SCRIPT03_D7580_TAKET,〃 (〃),,,"Vem si tohle zlato a navÅ¡tiv Iraleho, který nás zásobuje zbranÄ›mi. Tento klÃ­Ä tÄ› pak dostane ke guvernérovi. Je to jen zkorumpovaná loutka Řádu, ale zbožňuje nabídky. UdÄ›lej cokoliv je tÅ™eba, abys osvobodil naÅ¡e bratry ve zbrani.","Tag disse penge og besøg Irale, som leverer vores vÃ¥ben. SÃ¥ vil denne nøgle give dig adgang til guvernøren. Han er en korrupt marionet af Ordenen, men han elsker at lave aftaler. Gør hvad du skal gøre for at befri vores vÃ¥benbrødre.","Nimm das Geld hier und geh zu Irale, der und mit Waffen versorgt. Und dieser Schlüssel erlaubt die Zutritt zum Haus des Gouverneurs. Er ist eine korrupte Marionette des Ordens aber er liebt es, Deals zu machen. Mach was nötig ist um unsere Waffenbrüder zu befreien.",,"Prenu ĉi tiun monon kaj renkontu Irale-n, la provizanto de niaj armiloj. Poste ĉi tiu Ålosilo ebligos al vi renkonti la registon. Li estas koruptita marioneto de La Ordeno, sed li Åategas intertraktadojn. Faru tion ajn, kion vi bezonas por liberigi niajn soldatojn.","Toma este dinero y ve a ver a Irale, que suministra nuestras armas. Después, esta llave te servirá para ver al gobernador. Es una marioneta corrupta de La Orden, pero le encantan los tratos. Haz lo que creas necesario para liberar a nuestros conmilitones.","Toma este dinero y ve a ver a Irale, que suministra nuestras armas. Después, esta llave te va a servir para ver al gobernador. Es una marioneta corrupta de La Orden, pero le encantan los tratos. Haz lo que creas necesario para liberar a nuestros conmilitones.","Ota tästä rahaa ja käväise Iralen luona, joka toimittaa meidän aseemme. Sitten tällä avaimella pääset tapaamaan kuvernööriä. Hän on Veljeskunnan läpimätä sätkynukke, mutta hän rakastaa tehdä kauppoja. Tee kaikkesi vapauttaaksesi aseveljemme.",Prenez cet argent et allez voir Irale qui nous approvisionne en armes. Utilisez ensuite cette clé pour aller voir le gouverneur. Il est un pantin de l'Ordre mais il adore faire affaires. Faites ce qu'il faut pour libérer nos compatriotes.,,"Fogd ezt a pénzt, és látogasd meg Irale-t a fegyverellátónkat. Azután, ezzel a kulccsal be tudsz jutni a kormányzóhoz. Å a Rend egy korrupt bábja, de imád üzletet kötni. Tegyél meg mindent, hogy kiszabadítsd a bajtársainkat.","Prendi questo denaro e vai da Irale, uno dei nostri fornitori d'armi. Dopodiché, con questa chiave potrai entrare nella dimora del governatore. È un pupazzo corrotto dell'Ordine, ma adora trattare sottobanco. Fai tutto il necessario per liberare i nostri compagni d'arme.","ã“ã®é‡‘ã§ã€æˆ‘ã‚‰ã®æ­¦å™¨ã‚’調é”ã—ã¦ãれã¦ã„ã‚‹ +"Take this money and visit Irale who supplies our weapons. Then, this key will get you in to see the Governor. He's a corrupt puppet of the Order, but he loves to make deals. Do whatever you need to free our brothers in arms.",TXT_DLG_SCRIPT03_D7580_TAKET,〃 (〃),,,"Vem si tohle zlato a navÅ¡tiv Iraleho, který nás zásobuje zbranÄ›mi. Tento klÃ­Ä tÄ› pak dostane ke guvernérovi. Je to jen zkorumpovaná loutka Řádu, ale zbožňuje nabídky. UdÄ›lej cokoliv je tÅ™eba, abys osvobodil naÅ¡e bratry ve zbrani.","Tag disse penge og besøg Irale, som leverer vores vÃ¥ben. SÃ¥ vil denne nøgle give dig adgang til guvernøren. Han er en korrupt marionet af Ordenen, men han elsker at lave aftaler. Gør hvad du skal gøre for at befri vores vÃ¥benbrødre.","Nimm das Geld hier und geh zu Irale, der und mit Waffen versorgt. Und dieser Schlüssel erlaubt die Zutritt zum Haus des Gouverneurs. Er ist eine korrupte Marionette des Ordens aber er liebt es, Deals zu machen. Mach was nötig ist um unsere Waffenbrüder zu befreien.",,"Prenu ĉi tiun monon kaj renkontu Iralon, kiu provizas nin per armiloj. Poste ĉi tiu Ålosilo ebligos al vi renkonti la registon. Li estas koruptita marioneto de La Ordeno, sed li Åategas intertraktadojn. Faru tion ajn, kion vi bezonas por liberigi niajn soldatojn.","Toma este dinero y ve con Irale, que es el que suministra nuestras armas. Después, esta llave te servirá para ver al gobernador. Es una marioneta corrupta de La Orden, pero le encantan los tratos. Haz lo que creas necesario para liberar a nuestros conmilitones.","Toma este dinero y ve con Irale, que es el que suministra nuestras armas. Después, esta llave te va a servir para ver al gobernador. Es una marioneta corrupta de La Orden, pero le encantan los tratos. Haz lo que creas necesario para liberar a nuestros conmilitones.","Ota tästä rahaa ja käväise Iralen luona, joka toimittaa meidän aseemme. Sitten tällä avaimella pääset tapaamaan kuvernööriä. Hän on Veljeskunnan läpimätä sätkynukke, mutta hän rakastaa tehdä kauppoja. Tee kaikkesi vapauttaaksesi aseveljemme.",Prenez cet argent et allez voir Irale qui nous approvisionne en armes. Utilisez ensuite cette clé pour aller voir le gouverneur. Il est un pantin de l'Ordre mais il adore faire affaires. Faites ce qu'il faut pour libérer nos compatriotes.,,"Fogd ezt a pénzt, és látogasd meg Irale-t a fegyverellátónkat. Azután, ezzel a kulccsal be tudsz jutni a kormányzóhoz. Å a Rend egy korrupt bábja, de imád üzletet kötni. Tegyél meg mindent, hogy kiszabadítsd a bajtársainkat.","Prendi questo denaro e vai da Irale, uno dei nostri fornitori d'armi. Dopodiché, con questa chiave potrai entrare nella dimora del governatore. È un pupazzo corrotto dell'Ordine, ma adora trattare sottobanco. Fai tutto il necessario per liberare i nostri compagni d'arme.","ã“ã®é‡‘ã§ã€æˆ‘ã‚‰ã®æ­¦å™¨ã‚’調é”ã—ã¦ãれã¦ã„ã‚‹ イラールã®å…ƒã‚’訪ã­ã¦è¡Œã‘。 ãã®æ¬¡ã«ã€ã“ã®éµã‚’使ã£ã¦çŸ¥äº‹ã«ä¼šãˆã€‚ 知事ã¯ã‚ªãƒ¼ãƒ€ãƒ¼ã®è…æ•—ã—ãŸå‚€å„¡ã ãŒå–引ã«ã¯ @@ -10912,7 +10919,7 @@ Here's my I.D.,TXT_RPLY0_SCRIPT04_D1516_HERES,〃,,,Tady je moje průkazka.,Her I've got clearance.,TXT_RPLY0_SCRIPT04_D4548_IVEGO,〃,,,Mám povolení.,Jeg har fÃ¥et tilladelse.,Ich habe eine Berechtigung.,,Mi havas Äin.,Tengo permiso.,,Minulla on lupa.,J'ai une autorisation.,,Van jogosultságom.,Ce l'ho l'autorizzazione.,クリアランスã¯ã‚る。,허가를 받았어.,Ik heb toestemming.,Jeg har klarering.,Mam zezwolenie.,Eu tenho permissão.,,Am acces.,У Ð¼ÐµÐ½Ñ ÐµÑть пропуÑк.,,Jag har tillstÃ¥nd.,İznim var. Go on.,TXT_DLG_SCRIPT04_D6064_GOON,〃,,,Tak jdi.,Jeg har tilladelse.,Alles klar.,,Vi rajtas iri.,Entonces ve.,,Jatkakaa.,Allez-y.,,Mesélj csak.,Va pure.,ç¶šã‘ã‚。,그럼 ê°€ë„ ì¢‹ë‹¤.,Ga door.,Jeg har klarering.,Wchodź.,Pode passar.,,Intră.,Проходи.,,Fortsätt.,Devam et. Do you know where he is?,TXT_RPLY0_SCRIPT04_D6064_DOYOU,〃,,,"Nevíš, kde je?","Ved du, hvor han er?","Weißt du, wo er ist?",,"Ĉu vi scias, kie li estas?",¿Sabes dónde está?,,"Tiedätkö, missä hän on?",Vous savez où il est?,,Tudod hol találom meg?,Sai dov'è?,ソイツãŒä½•処ã«ã„ã‚‹ã‹çŸ¥ã£ã¦ã„ã‚‹ã‹?,그가 ì–´ë”” 있는지 아는가?,Weet je waar hij is?,Vet du hvor han er?,Wiesz gdzie on jest?,Você sabe onde ele está?,,Știi unde e?,"Ð’Ñ‹ знаете, где Дервин?",,Vet du var han är?,Nerede olduÄŸunu biliyor musun? -I don't know where anybody is. I just keep the lid on the rat trap.,TXT_DLG_SCRIPT04_D7580_IDONT,〃,,,Já nevím kde kdo je. Já jen dohlížím na pastiÄku na myÅ¡i.,"Jeg ved ikke, hvor nogen er. Jeg holder bare lÃ¥get pÃ¥ rottefælden.","Ich weiß von niemandem, wo er ist. Ich kümmere mich nur um die Rattenfalle.",,Mi scias nenies pozicion. Mi nur estas komisiito de la pordoj de ĉi tiu ratkaptilo.,No me sé la ubicación de nadie. Solo estoy a cargo de las puertas de esta ratonera.,,"En tiedä, missä kukaan on. Huolehdin vain rotanloukun kannesta.",Je ne sais pas où se trouve qui que ce soit. Je ne fais que garder le couvercle de ce trou à rats.,,Ötletem sincs merre vannak az emberek. Én csak a patkány csapda tetejéért vagyok felelÅ‘s.,Non so dove sia nessuno di quelli che stanno nel magazzino. A me interessa solo che non scappino.,"俺ã«ã¯ä½•処ã«ã„ã‚‹ã‹ã¯åˆ†ã‹ã‚‰ãªã„。 +I don't know where anybody is. I just keep the lid on the rat trap.,TXT_DLG_SCRIPT04_D7580_IDONT,〃,,,Já nevím kde kdo je. Já jen dohlížím na pastiÄku na myÅ¡i.,"Jeg ved ikke, hvor nogen er. Jeg holder bare lÃ¥get pÃ¥ rottefælden.","Ich weiß von niemandem, wo er ist. Ich kümmere mich nur um die Rattenfalle.",,Mi scias nenies lokon. Mi nur estas komisiito de la pordoj de ĉi tiu ratkaptilo.,No me sé la ubicación de nadie. Solo estoy a cargo de las puertas de esta ratonera.,,"En tiedä, missä kukaan on. Huolehdin vain rotanloukun kannesta.",Je ne sais pas où se trouve qui que ce soit. Je ne fais que garder le couvercle de ce trou à rats.,,Ötletem sincs merre vannak az emberek. Én csak a patkány csapda tetejéért vagyok felelÅ‘s.,Non so dove sia nessuno di quelli che stanno nel magazzino. A me interessa solo che non scappino.,"俺ã«ã¯ä½•処ã«ã„ã‚‹ã‹ã¯åˆ†ã‹ã‚‰ãªã„。 俺ã¯ãŸã ãƒã‚ºãƒŸæ•りã«ãƒ•タを被ã›ã¦ã„ã‚‹ã ã‘ã ã€‚",그가 정확히 ì–´ë”” 있는지는 모른다. 난 ë„ë§ìžë¥¼ 그냥 ê°ì‹œí•  ë¿ì´ë‹¤.,Ik weet niet waar iemand is. Ik hou gewoon het deksel op de rattenval.,Jeg vet ikke hvor noen er. Jeg holder bare lokket pÃ¥ rottefella.,Nie wiem gdzie kto jest. Ja tylko plinujÄ™ tej puÅ‚apki na szczury.,Não sei onde ninguém está. Apenas mantenho a tampa neste bueiro.,,"Nu È™tiu unde e nimeni, eu doar È›in ochii pe capcana pentru È™oareci.","Ðичего Ñ Ð½Ðµ знаю. Я проÑто Ñлежу, чтобы никто из них не Ñбежал из западни.",,Jag vet inte var nÃ¥gon är. Jag hÃ¥ller bara locket pÃ¥ rÃ¥ttfällan.,Kimsenin nerede olduÄŸunu bilmiyorum. Ben sadece fare kapanının kapağını tutuyorum. You are an unpleasant distraction.,TXT_DLG_SCRIPT04_D9096_YOUAR,〃,,,Jsi nepříjemná otrava.,Du er en ubehagelig distraktion.,Du bist eine unerwünschte Ablenkung,,Vi estas malagrabla distraĵo.,Eres una distracción incómoda.,,Oletpa epämiellyttävä häiriötekijä.,Vous êtes une distraction déplaisante.,,Egy kellemetlen zavaró tényezÅ‘ vagy.,Sei una sgradevole distazione.,ãŠå‰ã¯ä¸å¿«ã§ç›®éšœã‚Šã ã€‚,ë‹¹ì‹ ì€ ì§€ê¸ˆ ë‚´ ê°ì‹œì¼ì„ 방해하고 있다.,Je bent een onaangename afleiding.,Du er en ubehagelig distraksjon.,Bardzo mnie rozpraszasz.,Você é uma distração bem incômoda.,,EÈ™ti o distragere neplăcută.,Ты — доÑÐ°Ð´Ð½Ð°Ñ Ð¿Ð¾Ð¼ÐµÑ…Ð°.,,Du är en obehaglig distraktion.,HoÅŸ olmayan bir dikkat dağıtıcısın. Move along or taste metal.,TXT_DLG_SCRIPT04_D10612_MOVEA,MAP04: Reactor Core gray guard.,,,"Jdi dál, nebo okus ocel.",GÃ¥ videre eller smag pÃ¥ metal.,Beweg dich oder du schmeckst Metall.,,Plue movu vin aÅ­ gustumu kuglojn.,Esfúmate o come plomo.,,"Ala vetää, tai maista metallia.",Circulez ou préparez vous à manger de l'acier.,,"Haladj tovább, vagy ízleld meg a kardom.",Sparisci o ti finisce male.,åŒè¡Œã‹é‰„を味ã‚ã†ã‹ã ã€‚,어서 움ì§ì—¬. ì´ì•Œ 먹기 싫으면.,Ga verder of proef metaal.,GÃ¥ videre eller smak pÃ¥ metall.,Odejdź stÄ…d albo posmakujesz mojej pukawki.,Vá embora ou vai tomar chumbo grosso.,,MÈ™că-te sau înghiÈ›i metal.,"Прочь, или отведаешь металла.",,GÃ¥ vidare eller smaka pÃ¥ metall.,İlerle yoksa metalin tadına bakarsın. @@ -10963,7 +10970,7 @@ Are you deaf? I just told you how busy I am. Get back to work.,TXT_DLG_SCRIPT04_ Who are you? Only clearance level two personnel are permitted in this area.,TXT_DLG_SCRIPT04_D34868_WHOAR,MAP04: Reactor Core → Southern alarm green door,,,Kdo jsi? Pouze pracovníci druhé úrovnÄ› sem mají povolen přístup.,Hvem er du? Kun personale pÃ¥ sikkerhedsniveau to har adgang til dette omrÃ¥de.,Wer bist du? Nur Personal mit Zugangsstufe 2 hst hier Zutritt.,,Kiu vi estas? Nur dunivelaj laboristoj estas permesataj ĉi tie.,¿Quién eres? Solo el personal con permiso de nivel dos puede estar aquí.,,Kuka sinä olet? Vain kakkoskulkuluvan omaavalla henkilöstöllä on pääsy tälle alueelle.,Qui êtes vous? Seuls les personnes avec un niveau d'autorisation deux peuvent accéder à cette zone.,,Te meg ki vagy? Csak kettes szintű engedéllyel rendekezÅ‘k jöhetnek be ide.,Chi sei? Solo il personale con l'autorizzazione di livello due può stare in questa sezione.,"誰ã ? レベル2ã®ã‚¯ãƒªã‚¢ãƒ©ãƒ³ã‚¹ã‚’æŒãŸãªã„者㯠ã“ã“ã«æ¥ã¦ã¯ã„ã‘ãªã„ãŒã€‚",넌 누구지? ì´ê³³ì€ 2급 ì¸ì›ë§Œì´ 출입할 수 있다.,Wie ben jij? Alleen ontruimingsniveau twee personeel is toegestaan in dit gebied.,Hvem er du? Bare personell med klareringsnivÃ¥ to har adgang til dette omrÃ¥det.,Kim jesteÅ›? Tylko personel posiadajÄ…cy zezwolenie poziomu drugiego może tu przebywać.,Quem é você? Só funcionários com permissão nível 2 podem entrar nesta área.,,Cine eÈ™ti? Doar personalul cu nivel 2 de acces are voie în zona aceasta.,Ты кто такой? Только перÑоналу второго ÑƒÑ€Ð¾Ð²Ð½Ñ Ð´Ð¾Ð¿ÑƒÑка разрешено находитьÑÑ Ð² Ñтой зоне.,,Vem är du? Endast personal pÃ¥ nivÃ¥ tvÃ¥ fÃ¥r vistas i det här omrÃ¥det.,Kimsin sen? Bu bölgeye sadece ikinci seviye personel girebilir. I'm the replacement worker.,TXT_RPLY0_SCRIPT04_D34868_IMTHE,〃,,,Jsem náhradník.,Jeg er afløseren.,Ich bin der Ersatzarbeiter.,,Mi estas la anstataÅ­anto.,Soy el sustituto.,,Olen täydennystyöntekijä.,Je suis le remplaçant.,,Én vagyok a helyettes.,Sono il lavoratore sostitutivo.,代替è¦å“¡ã ã€‚,후임 근무ìžë¡œ 왔습니다.,Ik ben de vervanger.,Jeg er vikaren.,Jestem tu na zastÄ™pstwo.,Eu sou o operário substituto.,,Sunt înlocuitorul muncitorului.,Я замещаю рабочего.,,Jag är ersättningsarbetaren.,Ben yedek işçiyim. -"About time you showed up. Go talk with Ketrick in the core. Oh, and take this key card. Don't want you getting shot on your first day, huh?",TXT_DLG_SCRIPT04_D36384_ABOUT,〃,,,"Už bylo na Äase, aby ses objevil. Jdi si promluvit s Ketrickem v jádÅ™e. A vezmi si tuhle kartu. PÅ™ece by ses nechtÄ›l nechat zastÅ™elit hned svůj první den, ne?","Det var pÃ¥ tide, du dukkede op. GÃ¥ hen og snak med Ketrick i kernen. Ã…h, og tag dette nøglekort. Du skal ikke blive skudt pÃ¥ din første dag, hva'?","Wird auch Zeit, dass du hier erscheinst. Rede mit Ketrick im Reaktorkern, Und nimm diesen Schlüssel. Du willst ja wohl nicht gleich am ersten Tag erschossen werden, oder?",,"Vi finfine aperis. Iru paroli kun Ketrick en la kerno. Ha, kaj prenu ĉi tiun Ålosil-karton; vi ne volas ricevi pafon dum via unua tago, ĉu?","Ya era hora. Ve a hablar con Ketrick en el núcleo. Ah, y toma esta tarjeta llave; no querrás que te peguen un tiro en tu primer día, ¿eh?",,"Jo oli aikakin ilmaantua. Mene ytimeen puhumaan Ketrickin kanssa. Ai niin, ja ota tämä avainkortti. Ei parane joutua ammutuksi heti ensimmäisenä työpäivänä, eikö niin?","Ah. Enfin. Il vous en a fallu, du temps! Allez parler avec Ketrick dans la section du cÅ“ur. Oh, et prenez ce passe. Vous ne voulez pas vous faire tirer dessus lors de votre premier jour, non?",,"Már ideje volt. Menj és beszélj Kettickkel a magban. Ja, és vidd magaddal ezt a mágneskártyát. Gondolom nem akarod magadat agyonlövetni az elsÅ‘ napodon.","Finalmente sei arrivato. Vai a parlare a Ketrick nel nucleo. Ah, e prendi questa tessera chiave. Non è il caso di farsi sparare il primo giorno di lavoro, eh?","ãã‚ãã‚æ¥ã‚‹ã¨æ€ã£ã¦ã„ãŸã‚ˆã€‚コアã«ã„ã‚‹ +"About time you showed up. Go talk with Ketrick in the core. Oh, and take this key card. Don't want you getting shot on your first day, huh?",TXT_DLG_SCRIPT04_D36384_ABOUT,〃,,,"Už bylo na Äase, aby ses objevil. Jdi si promluvit s Ketrickem v jádÅ™e. A vezmi si tuhle kartu. PÅ™ece by ses nechtÄ›l nechat zastÅ™elit hned svůj první den, ne?","Det var pÃ¥ tide, du dukkede op. GÃ¥ hen og snak med Ketrick i kernen. Ã…h, og tag dette nøglekort. Du skal ikke blive skudt pÃ¥ din første dag, hva'?","Wird auch Zeit, dass du hier erscheinst. Rede mit Ketrick im Reaktorkern, Und nimm diesen Schlüssel. Du willst ja wohl nicht gleich am ersten Tag erschossen werden, oder?",,"Vi finfine aperis. Iru paroli kun Ketrick en la kerno. Ha, kaj prenu ĉi tiun Ålosil-karton; vi ne volas ricevi pafon dum via unua tago, ĉu?","Ya era hora. Ve al área del núcleo a hablar con Ketrick. Ah, y toma esta tarjeta llave; no querrás que te peguen un tiro en tu primer día, ¿eh?",,"Jo oli aikakin ilmaantua. Mene ytimeen puhumaan Ketrickin kanssa. Ai niin, ja ota tämä avainkortti. Ei parane joutua ammutuksi heti ensimmäisenä työpäivänä, eikö niin?","Ah. Enfin. Il vous en a fallu, du temps! Allez parler avec Ketrick dans la section du cÅ“ur. Oh, et prenez ce passe. Vous ne voulez pas vous faire tirer dessus lors de votre premier jour, non?",,"Már ideje volt. Menj és beszélj Kettickkel a magban. Ja, és vidd magaddal ezt a mágneskártyát. Gondolom nem akarod magadat agyonlövetni az elsÅ‘ napodon.","Finalmente sei arrivato. Vai a parlare a Ketrick nel nucleo. Ah, e prendi questa tessera chiave. Non è il caso di farsi sparare il primo giorno di lavoro, eh?","ãã‚ãã‚æ¥ã‚‹ã¨æ€ã£ã¦ã„ãŸã‚ˆã€‚コアã«ã„ã‚‹ ケトリックã«è©±ã—ã‹ã‘ã¦ãれ。 ã‚ã¨ã“ã®ã‚­ãƒ¼ã‚«ãƒ¼ãƒ‰ã‚’å—ã‘å–ã£ã¦ãれ。 åˆæ—¥ã§æ’ƒãŸã‚ŒãŸããªã„ãªã‚‰ãªã€‚","마침 잘 와주었군. ì¤‘ì‹¬ë¶€ì— ìžˆëŠ” 케트릭ì—게 가서 얘기하ë¼. ì•„, 그리고 여기 키카드를 받아ë¼. 첫날부터 ì´ì— ë§žê³  싶지는 않겠지, ì‘?","Het werd tijd dat je kwam opdagen. Ga praten met Ketrick in de kern. Oh, en neem deze sleutelkaart. Wil je niet dat je op je eerste dag wordt neergeschoten, huh?",PÃ¥ tide du dukket opp. GÃ¥ og snakk med Ketrick i kjernen. Og ta dette nøkkelkortet. Du vil vel ikke bli skutt pÃ¥ din første dag?,"W koÅ„cu jesteÅ›. Idź, porozmawiaj z Ketrickiem przy rdzeniu. Oh, weź tÄ… kartÄ™-klucz. Nie chcesz chyba być zastrzelony podczas pierwszego dnia w pracy, co?","Já era hora de você aparecer. Vá falar com o Ketrick lá no núcleo. Ah, e leve este cartão de acesso. Você não vai querer levar um tiro no primeiro dia, hein?",,"Era È™i timpul. Du-te È™i vorbeÈ™te cu Ketrick în interiorul nucleului. Ah, È™i ia È™i cardul ăsta. Nu cred că vrei să fii împuÈ™cat în prima zi de lucru, eh?","Как раз вовремÑ. Иди поговори Ñ ÐšÐµÑ‚Ñ€Ð¸ÐºÐ¾Ð¼ возле Ñдра реактора. О, и возьми Ñту карточку. Мы ведь не хотим, чтобы Ñ‚ÐµÐ±Ñ Ð¿Ð¾Ð´Ñтрелили в первый же рабочий день?",,"Det var pÃ¥ tiden att du dök upp. GÃ¥ och prata med Ketrick i kärnan. Och ta det här nyckelkortet. Jag vill inte att du blir skjuten pÃ¥ din första dag, va?","Tam zamanında geldin. Git merkezdeki Ketrick ile konuÅŸ. Bu anahtar kartını da al. İlk gününde vurulmanı istemeyiz, deÄŸil mi?" @@ -11072,7 +11079,7 @@ You want his uniform?,TXT_RPLY0_SCRIPT06_D3032_YOUWA,〃,,,Ty chceÅ¡ jeho unifor Have you brought me what I want?,TXT_DLG_SCRIPT06_D6064_HAVEY,〃,,,"PÅ™inesl jsi mi, co chci?","Har du bragt mig det, jeg vil have?",Hast du mir was mitgebracht?,,"Ĉu vi alportis tion, kion mi volas?",¿Me has traído lo que quiero?,¿Trajiste lo que quiero?,"Oletko tuonut minulle sen, mitä haluan?",Avez-vous ramené ce dont j'ai besoin?,,"Elhoztad, amit kértem?",Mi hai portato ciò che volevo?,å¿…è¦ãªç‰©ã¯æŒã£ã¦ããŸã‹?,ë‚´ê°€ ì›í•˜ëŠ” ê²ƒì„ ê°€ì§€ê³  왔나랴?,Heb je me gebracht wat ik wil?,Har du det jeg vil ha?,PrzyniosÅ‚eÅ› to o co ciÄ™ prosiÅ‚em?,Você trouxe o que eu quero?,,Mi-ai adus ceea ce doresc?,"Ты Ð¿Ñ€Ð¸Ð½Ñ‘Ñ Ð¼Ð½Ðµ то, что Ñ Ð¿Ñ€Ð¾Ñил?",,Har du gett mig det jag vill ha?,İstediÄŸim ÅŸeyi getirdin mi? How about this uniform?,TXT_RPLY0_SCRIPT06_D6064_HOWAB,〃,,,Co tahle uniforma?,Hvad med denne uniform?,Wie findest du diese Uniform?,,Kion pri ĉi tiu uniformo?,¿Qué tal este uniforme?,¿Qué hay de este uniforme?,Miten olisi tämä univormu?,"Il vous convient, cet uniforme?",,És mi az újság ezzel az egyenruhával?,Che ne dici di questa uniforme?,ã“ã®åˆ¶æœã¯ã©ã†ã ?,ì´ ì „íˆ¬ë³µì€ ì–´ë•Œ?,Hoe zit het met dit uniform?,Hva med denne uniformen?,Może być ten mundur?,Que tal este uniforme?,,Ce zici de uniforma asta?,Эта униформа подойдёт?,,Vad sägs om den här uniformen?,Bu üniformaya ne dersin? Bring me the uniform.,TXT_RNO0_SCRIPT06_D6064_BRING,,,,PÅ™ines mi tu uniformu.,Giv mig hans uniform.,Bring mir seine Uniform.,,Alportu la uniformon.,Tráeme el uniforme.,,Tuo univormu minulle.,Amenez moi l'uniforme.,,Hozdd ide az egyenruhát.,Portami l'uniforme.,制æœã‚’æŒã£ã¦ã“ã„。,ì „íˆ¬ë³µì„ ê°€ì ¸ì™€ëž´...,Breng me het uniform.,Gi meg uniformen.,PrzynieÅ› mi mundur.,Me traga o uniforme.,,Adu-mi uniforma.,ПринеÑи мне униформу.,,Ge mig uniformen.,Bana üniformayı getir. -"Good. Here's something extra. My fellows tore this off of a fallen Crusader, it's the parts that make up a flamethrower. Now Irale can make one for you. You can have such fun.",TXT_DLG_SCRIPT06_D7580_GOODH,MAP06: Weran.,,,"DobÅ™e. Tady je nÄ›co navíc. Mí stoupenci tohle urvali ze zniÄeného KÅ™ižáka, jsou to Äásti, ze kterých se skládá plamenomet. TeÄ ti jeden může Irale postavit. MůžeÅ¡ mít takové legrace.","Godt. Her er noget ekstra. Mine kammerater rev den af en faldet korsfarer, det er de dele, der udgør en flammekaster. Nu kan Irale lave en til dig. Du kan have det sÃ¥ sjovt.",Gut so. Hier ist ein kleiner Bonus. Meine Kumpel haben das hier von einem gefallenen Ordensritter abgerissen. Es sind die Teile seines Flammenwerfers. Jetzt kann Irale dir auch einen machen. Du kannst so viel Spaß damit haben...,,Bone. Jen aldonaĵo; miaj kamaradoj deÅiris Äin de venkita Krucisto: ili estas partoj de flamĵetilo; Irale nun povos krei unu por vi. Vi vere amuziÄos.,Genial. Aquí tienes algo extra; mis compañeros lo arrancaron de un Cruzado caído: son las partes de un lanzallamas; ahora Irale puede hacer uno para ti. Te vas a divertir cantidad.,,"Hyvä. Tässä vielä jotain vähän lisäksi. Toverini repivät tämän kaatuneesta ristiretkeläisestä. Ne ovat liekinheittimen osia. Nyt Irale voi väsätä sinulle seillaisen. Ajattele sitä, kuinka hauskaa sinulla tulee olemaan.",Bien. Voilà un petit bonus. Mes amis ont arraché ceci à un croisé. C'est les pièces nécessaires à monter un lance-flammes. Irale peut en assembler un pour vous. Vous allez vraiment vous amuser.,,"Helyes. Itt van egy kis érdekesség. Egyik bajtársam letörte ezeket az egyik keresztesrÅ‘l, igazából egy lángszórót lehetne összerakni belÅ‘le. Irale össze tudna rakni egyet neked. Tuti örömteli pörkölÅ‘ pillanatokat fog okozni.","Eccellente. Ecco, ho qualcosa per te. I miei compagni hanno staccato questo da un Crociato distrutto, sono le parti che costituiscono il lanciafiamme. Adesso Irale ne può creare uno per te. Ti potrai divertire un sacco.","良ã—ã€‚ç¤¼ã¯æžœãŸãã†ã€‚ +"Good. Here's something extra. My fellows tore this off of a fallen Crusader, it's the parts that make up a flamethrower. Now Irale can make one for you. You can have such fun.",TXT_DLG_SCRIPT06_D7580_GOODH,MAP06: Weran.,,,"DobÅ™e. Tady je nÄ›co navíc. Mí stoupenci tohle urvali ze zniÄeného KÅ™ižáka, jsou to Äásti, ze kterých se skládá plamenomet. TeÄ ti jeden může Irale postavit. MůžeÅ¡ mít takové legrace.","Godt. Her er noget ekstra. Mine kammerater rev den af en faldet korsfarer, det er de dele, der udgør en flammekaster. Nu kan Irale lave en til dig. Du kan have det sÃ¥ sjovt.",Gut so. Hier ist ein kleiner Bonus. Meine Kumpel haben das hier von einem gefallenen Ordensritter abgerissen. Es sind die Teile seines Flammenwerfers. Jetzt kann Irale dir auch einen machen. Du kannst so viel Spaß damit haben...,,Bone. Jen aldonaĵo; miaj kamaradoj deÅiris Äin de venkita Krucisto: ili estas partoj de flamĵetilo; Iralo nun povos krei unu por vi. Vi vere amuziÄos.,Genial. Aquí tienes algo extra; mis compañeros lo arrancaron de un Cruzado caído: son las partes de un lanzallamas; ahora Irale puede hacer uno para ti. Te vas a divertir cantidad.,,"Hyvä. Tässä vielä jotain vähän lisäksi. Toverini repivät tämän kaatuneesta ristiretkeläisestä. Ne ovat liekinheittimen osia. Nyt Irale voi väsätä sinulle seillaisen. Ajattele sitä, kuinka hauskaa sinulla tulee olemaan.",Bien. Voilà un petit bonus. Mes amis ont arraché ceci à un croisé. C'est les pièces nécessaires à monter un lance-flammes. Irale peut en assembler un pour vous. Vous allez vraiment vous amuser.,,"Helyes. Itt van egy kis érdekesség. Egyik bajtársam letörte ezeket az egyik keresztesrÅ‘l, igazából egy lángszórót lehetne összerakni belÅ‘le. Irale össze tudna rakni egyet neked. Tuti örömteli pörkölÅ‘ pillanatokat fog okozni.","Eccellente. Ecco, ho qualcosa per te. I miei compagni hanno staccato questo da un Crociato distrutto, sono le parti che costituiscono il lanciafiamme. Adesso Irale ne può creare uno per te. Ti potrai divertire un sacco.","良ã—ã€‚ç¤¼ã¯æžœãŸãã†ã€‚ ãれã¨ä»²é–“ãŒå€’れãŸã‚¯ãƒ«ã‚»ã‚¤ãƒ€ãƒ¼ã‹ã‚‰ã“れを 奪ã£ã¦ããŸã€ç«ç‚Žæ”¾å°„器ã®éƒ¨å“らã—ã„。 ã‚¤ãƒ©ãƒ¼ãƒ«ã«æ¸¡ã›ã°ä½œã‚Šä¸Šã’ã‚‹ã¯ãšã ã€‚ @@ -11576,7 +11583,7 @@ Nothing to see here. Move along.,TXT_DLG_SCRIPT17_D4548_NOTHI,MAP17: Green guard "Shhhh... Keep it quiet, unless you want us both killed. Now, what can I do for you?",TXT_DLG_SCRIPT17_D7580_SHHHH,〃,,,"Pššš... BuÄ potichu, leda že bys nás oba chtÄ›l nechat popravit. Tak, co pro tebe můžu udÄ›lat?","Shhhh... Hold det stille, medmindre du vil have os begge dræbt. Hvad kan jeg gøre for dig?","Psst... sei leise, oder willst du uns beide umbringen? Nun, was kann ich für dich tun?",,"ÅœÅ! Ne tiel laÅ­te se vi ne intencas, ke ni ambaÅ­ estu mortigitaj. Nu, kion mi faru por vi?","Shhhh... No tan alto, a menos que quieras que nos maten a ambos. Y bueno, ¿qué puedo hacer por ti?",,"Hyss, suuta soukemmalle, ellet halua tapattaa meitä molempia. No niin, miten voin auttaa?","Silence! Baissez de ton, sauf si vous voulez que l'on se fasse descendre tous le deux.. Que puis-je faire pour vous?",,"Shhhh...csak csendben, különben mindkettÅ‘nket megöletsz. Akkor, miben is segíthetek?","Shhhh... Fai piano, a meno che non vuoi farci ammazzare tutti e due. Ora, che posso fare per te?","シーッ...é™ã‹ã«ã€ 殺ã•れるã‹ã‚‚ã—れãªã„ã‚“ã ãžã€‚ãれã§ä½•ã‹ç”¨ã‹?","쉿... 죽기 싫으면 조용히 í•´. ìž, ë‚´ê°€ ë­˜ ë„와줄까?","Shhhhhh.... Hou het stil, tenzij je wilt dat we allebei gedood worden. Wat kan ik nu voor u doen?","Vær stille, hvis du ikke vil at vi begge skal bli drept. Hva kan jeg gjøre for deg?","Cicho, chyba że chcesz nas obu zabić. Co mogÄ™ dla ciebie zrobić?","Shhhh... Fale baixo, a não ser que queira que nos matem. Agora, como posso te ajudar?",,"Șhhhh... Mai încet, dacă nu vrei să murim amândoi. Acum, ce pot face pentru tine?","ТÑÑ... Тише, еÑли не хочешь, чтобы Ð½Ð°Ñ Ð¾Ð±Ð¾Ð¸Ñ… убили. Итак, чем Ñ Ð¼Ð¾Ð³Ñƒ тебе помочь?",,"Shhhh... HÃ¥ll tyst, om du inte vill att vi bÃ¥da ska dödas. Vad kan jag göra för dig?","İkimizin de ölmesini istemiyorsan sessiz ol. Åžimdi, senin için ne yapabilirim?" Tell me how to find the Bishop.,TXT_RPLY0_SCRIPT17_D7580_TELLM,〃,,,"Řekni mi, jak najít Biskupa.","Fortæl mig, hvordan jeg finder biskoppen.","Sage mir, wie ich den Bischof finde.",,Diru kiel mi trovu la Episkopon.,Dime como encontrar al Obispo.,,"Kerro, miten löydän Piispan.",Où puis-je trouver l'évêque?,,"Ãruld el, hol találom meg a Püspököt.",Dimmi dove trovare il Vescovo.,ビショップã¯ä½•処ã‹èžããŸã„。,비ìˆì„ 어떻게 찾아야 하는지 알려줘.,Vertel me hoe ik de bisschop kan vinden.,Fortell meg hvordan jeg finner biskopen.,Powiedz mi jak znaleźć biskupa.,Diga onde eu encontro o Bispo.,,Spune-mi cum găsesc Episcopul.,"Скажи, как мне найти ЕпиÑкопа.",,Berätta hur jag hittar biskopen.,Bana Piskopos'u nasıl bulacağımı söyle. -"Ohhh, I knew you would ask me for that. Look behind you. That's the entrance to the Bishop's citadel. It's guarded by a force field that is only shut off for official visitors, not you.",TXT_DLG_SCRIPT17_D9096_OHHHI,〃,,,"Ohó, já vÄ›dÄ›l, že se mÄ› na tohle zeptáš. Podívej se za sebe. To je vchod do Biskupovy citadely. Je stÅ™ežená silovým polem, které se vypíná pouze kvůli oficiálním návÅ¡tÄ›vám, ne tobÄ›.","Jeg vidste, du ville spørge mig om det. Se bag dig. Det er indgangen til biskoppens citadel. Den er bevogtet af et kraftfelt, der kun er lukket for officielle besøgende, ikke dig.","Oh, hab ich's doch geahnt, dass du mich das fragen würdest. Schau mal hinter dich. Dort ist der Eingang zu der Festung des Bischofs. Er ist durch ein Kraftfeld geschützt, welches sich nur für offizielle Besucher abschaltet und nicht für dich.",,"Ho, mi sciis, ke vi estis petonta tion. Rigardu malantaÅ­en tra la fenestro: tie estas la enirejo al la citadelo de la Episkopo. Äœin baras fortokampo malÅaltinda nur por oficialaj vizitantoj, ne vi, ekzemple.","Ah, imaginaba que me ibas a pedir eso. Mira detrás de ti por la ventana: ahí está la entrada a la ciudadela del Obispo. La bloquea un campo de fuerza que solo se apaga para visitas oficiales, no tú, por ejemplo.",,"Oo, arvasin, että kysyisit sitä minulta. Katso taaksesi. Siellä on sisäänkäynti Piispan linnoitukseen. Sitä turvaa voimakenttä, joka kytketään pois päältä ainoastaan virallisille vieraille, ei sinulle.","Ohh, je savais que vous me demanderiez cela. Regardez derrière vous. C'est l'entrée de la citadelle de l'évêque. Elle est gardée par un champ de force qui ne s'ouvre que pour les visites officielles, pas vous.",,"Ohhh, tudtam, hogy ezt fogod kérdezni. Nézz csak magad mögé. Ez a püspök fellegvárához a bejárat. Egy erÅ‘pajzs védi, és csak hivatalos látogatóknak nyitják ki, aminek valljuk be Te nem igazán felelsz meg.","Ohhh, sapevo che me l'avresti chiesto. Guarda dietro di te. Quello è l'ingresso per la cittadella del Vescovo. È protetta da un campo di forza che viene disattivato solo per le visite ufficiali, quindi non per la tua.","ãŠãŠã…ã€ãã†ã„ã†äº‹ã ã‚ã†ã¨æ€ã£ãŸã€‚ +"Ohhh, I knew you would ask me for that. Look behind you. That's the entrance to the Bishop's citadel. It's guarded by a force field that is only shut off for official visitors, not you.",TXT_DLG_SCRIPT17_D9096_OHHHI,〃,,,"Ohó, já vÄ›dÄ›l, že se mÄ› na tohle zeptáš. Podívej se za sebe. To je vchod do Biskupovy citadely. Je stÅ™ežená silovým polem, které se vypíná pouze kvůli oficiálním návÅ¡tÄ›vám, ne tobÄ›.","Jeg vidste, du ville spørge mig om det. Se bag dig. Det er indgangen til biskoppens citadel. Den er bevogtet af et kraftfelt, der kun er lukket for officielle besøgende, ikke dig.","Oh, hab ich's doch geahnt, dass du mich das fragen würdest. Schau mal hinter dich. Dort ist der Eingang zu der Festung des Bischofs. Er ist durch ein Kraftfeld geschützt, welches sich nur für offizielle Besucher abschaltet und nicht für dich.",,"Ho, mi sciis, ke vi estis petonta tion. Rigardu malantaÅ­en tra la fenestro: tie estas la enirejo al la citadelo de la Episkopo. Äœin baras fortokampo malÅaltinda nur por oficialaj vizitantoj: ne vi, ekzemple.","Ah, imaginaba que me ibas a pedir eso. Mira detrás de ti por la ventana: ahí está la entrada a la ciudadela del Obispo. La bloquea un campo de fuerza que solo se apaga para visitas oficiales: no tú, por ejemplo.",,"Oo, arvasin, että kysyisit sitä minulta. Katso taaksesi. Siellä on sisäänkäynti Piispan linnoitukseen. Sitä turvaa voimakenttä, joka kytketään pois päältä ainoastaan virallisille vieraille, ei sinulle.","Ohh, je savais que vous me demanderiez cela. Regardez derrière vous. C'est l'entrée de la citadelle de l'évêque. Elle est gardée par un champ de force qui ne s'ouvre que pour les visites officielles, pas vous.",,"Ohhh, tudtam, hogy ezt fogod kérdezni. Nézz csak magad mögé. Ez a püspök fellegvárához a bejárat. Egy erÅ‘pajzs védi, és csak hivatalos látogatóknak nyitják ki, aminek valljuk be Te nem igazán felelsz meg.","Ohhh, sapevo che me l'avresti chiesto. Guarda dietro di te. Quello è l'ingresso per la cittadella del Vescovo. È protetta da un campo di forza che viene disattivato solo per le visite ufficiali, quindi non per la tua.","ãŠãŠã…ã€ãã†ã„ã†äº‹ã ã‚ã†ã¨æ€ã£ãŸã€‚ 後ã‚を見ã‚。ãã“ãŒãƒ“ショップã®å¤§è–å ‚ã ã€‚ ã—ã‹ã—è¦äººä»¥å¤–ã¯å…¥ã‚Œãªã„よㆠフォースフィールドãŒå¼µã£ã¦ã‚る〠@@ -11625,7 +11632,7 @@ Maulers?,TXT_RPLY0_SCRIPT18_D1516_MAULE,〃,,,TrhaÄe?,Maulers?,Vernichter?,,Ĉu The Templar's favorite weapon.,TXT_RYES0_SCRIPT18_D1516_THETE,〃,,The templar's favourite weapon.,Oblíbená zbraň templářů.,Tempelriddernes yndlingsvÃ¥ben.,Die Lieblingswaffe der Templer.,,"La preferata armilo de la templanoj.","El arma favorita de los templarios.",,Temppeliherrojen lempiase.,L'arme favorite du templier.,,A keresztesek kedvenc fegyvere.,È l'arma preferita dei Templari.,騎士団ã«äººæ°—ã®å…µå™¨ã ã€‚,템플러가 ì œì¼ ì¢‹ì•„í•˜ëŠ” 무기야.,Het favoriete wapen van de Tempeliers.,Tempelriddernes favorittvÃ¥pen.,Ulubiona broÅ„ Templariuszy.,A arma favorita dos Templários.,,Arma preferată a Templierilor.,Их любимое оружие.,,Tempelriddarens favoritvapen.,Tapınakçıların en sevdiÄŸi silah. -They came in earlier. That's what all the security's about. I got a chance to look at them when I locked up. Nobody's supposed to go in or out of there until the rest of the platoon comes to claim their weapons.,TXT_DLG_SCRIPT18_D3032_THEYC,〃,,,"Dorazily už dřív, proto ta ochranka. MÄ›l jsem možnost se na nÄ› podívat, když jsem zamykal. Nikdo nesmí vejít dovnitÅ™ ani ven dokud si zbytek Äety nepÅ™ijde pro zbranÄ›.","De kom ind tidligere. Det er derfor, der er sÃ¥ meget sikkerhed. Jeg fik en chance for at se dem, da jeg lÃ¥ste af. Ingen mÃ¥ gÃ¥ ind eller ud derfra, før resten af delingen kommer for at hente deres vÃ¥ben.","Die sind heute früh hier eingetroffen. Daher all die Wachen. Ich konnte aber mal einen Blick darauf werfen. Niemand darf da rein, bevor die Waffen an die Truppen verteilt worden sind.",,"Ili alvenis antaÅ­ nelonge, tial estas ĉi tiom da sekureco; mi havis la Åancon vidi ilin kiam mi enÅlosis ilin. Neniu supozeble devas iri nek en tiun deponejon nek el Äi Äis la resto de la plotono venos peti siajn armilojn.","Han llegado hace poco, para eso toda esta seguridad; he tenido la oportunidad de verlos al cerrar el depósito. Se supone que nadie puede entrar o salir hasta que el resto del pelotón venga a pedir sus armas.","Llegaron hace poco, para eso toda esta seguridad; tuve la oportunidad de verlos al cerrar el depósito. Se supone que nadie puede entrar o salir hasta que el resto del pelotón venga a pedir sus armas.","He kävivät aiemmin. Siitä näissä kaikissa turvatoimissa on kyse. Sain mahdollisuuden vilkaista niitä, kun lukitsin paikkoja. Kukaan ei saa kulkea sisään eikä ulos, ennen kuin loput joukkueesta tulee noutamaan aseensa.",Ils sont passés plus tôt. C'est pour ça qu'il y a autant de personnel de sécurité. J'ai eu l'opportunité de les voire quand je suis parti verouiller les accès. Personne n'est sensé entrer ou sortir tant que la section n'a pas fini de récupérer leur armes.,,"Most hamarabb jöttek. Emiatt van akkora biztonsági cécó. Jó megnéztem magamnak Å‘ket amikor bezártam. Senki nem mehet se ki se be, míg a szakasz be nem megy a fegyvereiért.",Sono arrivati da poco. Ecco perché c'è così tanta sicurezza. Ho avuto l'opportunità di vederli quando sono uscito per bloccare gli accessi. Nessuno dovrebbe entrare o uscire finché il plotone non ha finito di raccogliere le armi.,"以剿‰€å±žã—ãŸã°ã‹ã‚Šã ã€‚ +They came in earlier. That's what all the security's about. I got a chance to look at them when I locked up. Nobody's supposed to go in or out of there until the rest of the platoon comes to claim their weapons.,TXT_DLG_SCRIPT18_D3032_THEYC,〃,,,"Dorazily už dřív, proto ta ochranka. MÄ›l jsem možnost se na nÄ› podívat, když jsem zamykal. Nikdo nesmí vejít dovnitÅ™ ani ven dokud si zbytek Äety nepÅ™ijde pro zbranÄ›.","De kom ind tidligere. Det er derfor, der er sÃ¥ meget sikkerhed. Jeg fik en chance for at se dem, da jeg lÃ¥ste af. Ingen mÃ¥ gÃ¥ ind eller ud derfra, før resten af delingen kommer for at hente deres vÃ¥ben.","Die sind heute früh hier eingetroffen. Daher all die Wachen. Ich konnte aber mal einen Blick darauf werfen. Niemand darf da rein, bevor die Waffen an die Truppen verteilt worden sind.",,"Ili alvenis antaÅ­ nelonge, tial estas ĉi tiom da sekureco; mi havis la eblon vidi ilin tiam, kiam mi enÅlosis ilin. Neniu supozeble devas iri nek en tiun deponejon nek el Äi Äis la resto de la plotono venos peti siajn armilojn.","Han llegado hace poco, para eso toda esta seguridad; he tenido la oportunidad de verlos al cerrar el depósito. Se supone que nadie puede entrar o salir hasta que el resto del pelotón venga a pedir sus armas.","Llegaron hace poco, para eso toda esta seguridad; tuve la oportunidad de verlos al cerrar el depósito. Se supone que nadie puede entrar o salir hasta que el resto del pelotón venga a pedir sus armas.","He kävivät aiemmin. Siitä näissä kaikissa turvatoimissa on kyse. Sain mahdollisuuden vilkaista niitä, kun lukitsin paikkoja. Kukaan ei saa kulkea sisään eikä ulos, ennen kuin loput joukkueesta tulee noutamaan aseensa.",Ils sont passés plus tôt. C'est pour ça qu'il y a autant de personnel de sécurité. J'ai eu l'opportunité de les voire quand je suis parti verouiller les accès. Personne n'est sensé entrer ou sortir tant que la section n'a pas fini de récupérer leur armes.,,"Most hamarabb jöttek. Emiatt van akkora biztonsági cécó. Jó megnéztem magamnak Å‘ket amikor bezártam. Senki nem mehet se ki se be, míg a szakasz be nem megy a fegyvereiért.",Sono arrivati da poco. Ecco perché c'è così tanta sicurezza. Ho avuto l'opportunità di vederli quando sono uscito per bloccare gli accessi. Nessuno dovrebbe entrare o uscire finché il plotone non ha finito di raccogliere le armi.,"以剿‰€å±žã—ãŸã°ã‹ã‚Šã ã€‚ 彼等ã¯å…¨ã¦è­¦å‚™ã«å‘¨ã‚‹ã ã‚ã†ã€‚ 門を閉ã‚る時ã«è¦‹ã‚‹æ©Ÿä¼šãŒã‚ã£ãŸã‹ã‚‰ãªã€‚ 残りã®å°éšŠãŒå…µå™¨ã‚’請求ã™ã‚‹ã¾ã§ @@ -12164,7 +12171,7 @@ Here you go.,TXT_RYES0_SCRIPT34_D16676_HEREY,〃,,,Tady máš.,Værsgo.,Bittesch (Similar to row 774)",,,"Dám ti radu: Jestli nÄ›kdy uvidíš jednoho z plecháÄků Řádu, otoÄ se a jdi. Jsou rychlí a suroví.","Her er et godt rÃ¥d: Hvis du nogensinde ser nogen af Ordenens tinsoldater, sÃ¥ løb den anden vej. Jeg kan især ikke lide Reavers, de er bare forbandet hurtige!","Hier ist ein guter Rat, falls du jemals einen dieser Zinnsoldaten des Ordens vorbeigehen siehst. Sie sind schnell und brutal. ",,Jen konsilo: estu ĉiam fore de la artefaritaj soldatoj de La Ordeno. Mi plej malÅatas Lertulojn; ili estas rapidegaj!,Un consejo: mantente siempre alejado de los soldados artificiales de La Orden. Los Saqueadores son los peores; ¡esas cosas son jodidamente rápidas!,Un consejo: mantente siempre alejado de los soldados artificiales de La Orden. Los Saqueadores son los peores; ¡esos desgraciados son rápidos!,"Annan pienen vinkin: Jos koskaan näet Veljeskunnan tinasotilaita, juokse toiseen suuntaan. Inhoan varsinkin raastajia; ne ovat vaan niin pirun nopeita!","Voilà un conseil. Si vous voyez un des soldats mécanisés de l'Ordre, retournez vous et courez. Ils sont rapides et brutaux. J'aime surtout pas les Reavers, ces satanés trucs vont trop vite!",,"Az a tanácsom számodra, hogy ha meglátod a Rend ólomkatonáit, vedd az utad az ellentétes irányba. Különösen igaz ez a fosztogatókra, piszkosul gyorsak.","Ecco un consiglio. Se vedi uno dei soldatini di latta dell'Ordine, vai nella direzione opposta. In particolare temo i Reaver, quei dannati robot sono davvero veloci!","ä¼ãˆã¦ãŠãã€ã‚ªãƒ¼ãƒ€ãƒ¼ã®éŒ«å…µã¯ ä»–æ–¹ã‹ã‚‰å‘¼ã³å‡ºã•れ押ã—寄ã›ã¦ãる。 奴等ã¯ç´ æ—©ã残å¿ã ã€‚","ì¶©ê³  하나 해주죠. 오ë”ì˜ ë¡œë´‡ ë³‘ì‚¬ë“¤ì„ ë°œê²¬í•œë‹¤ë©´, 조용히 피해가세요. 특히나 성가신 놈 중 하나가 리버ì¸ë°, ê·¸ë†ˆì˜ ì‚°íƒ„ì´ì€ 장난 아니ì—ìš”!","Hier is een advies, als je ooit een van de tinnen soldaten van de Orde ziet, ren dan de andere kant op. Ik hou vooral niet van de Reavers, die dingen zijn gewoon verdomd snel!","Hvis du ser noen av Ordenens tinnsoldater, sÃ¥ løp den andre veien. Jeg liker spesielt ikke Reavers, de er sÃ¥ jævla raske!","Oto rada, jeÅ›li kiedykolwiek zobaczysz któregoÅ› z blaszanych żoÅ‚nierzy Zakonu, uciekaj w drugÄ… stronÄ™. Szczególnie nie lubiÄ™ Åupieżców, te rzeczy sÄ… cholernie szybkie!","Um conselho pra você: se você ver algum desses soldados de lata da Ordem, fuja para o lado oposto. Eu particularmente detesto os Saqueadores. Eles são rapidos pra caramba!",,"Iată un sfat, dacă vezi vreodată un soldat al Ordinului, fugi în direcÈ›ia opusă. ÃŽmi displac mai ales Războinicii, ăștia sunt pur È™i simplu rapizi!","Ðебольшой Ñовет: еÑли увидишь «оловÑнных Ñолдатиков» Ордена, Ñворачивай в другую Ñторону. Я оÑобенно не люблю похитителей — Ñти твари довольно быÑтрые!",,"Här är ett rÃ¥d, om du nÃ¥gonsin ser nÃ¥gon av ordens tennsoldater, spring Ã¥t andra hÃ¥llet. Jag gillar särskilt inte Reavers, de där sakerna är förbannat snabba!","Size bir tavsiye, Tarikat'ın teneke askerlerinden birini görürseniz, diÄŸer tarafa kaçın. Özellikle YaÄŸmacıları sevmiyorum, o ÅŸeyler çok hızlı!" -"Welcome to the last flicker of hope. Only we have the free will to oppose the Order. We have the sharpest scientific minds, and many able bodies. But we lack that one real problem solver, who will give us the edge we need.",TXT_DLG_SCRIPT34_D27288_WELCO,MAP34: Macil.,,,"Vítej u posledního plamínku nadÄ›je. Jen my máme svobodnou vůli vzdorovat Řádu. Máme sice nejÄilejší vÄ›decké génie a mnoho schopných vojáků, ale chybí nám ten jeden Å™eÅ¡itel problémů, který by nás posunul vpÅ™ed.","Velkommen til det sidste lille hÃ¥b. Kun vi har den frie vilje til at modsætte os Ordenen. Vi har de skarpeste videnskabelige hjerner og mange dygtige kroppe. Men vi mangler den ene rigtige problemløser, som vil give os den fordel, vi har brug for.","Wilkommen beim letzten Hoffnungsschimmer. Nur wir haben noch den freien Willen, um gegen den Orden zu arbeiten. Wir haben die besten Wissenschaftler und viele fähige Kämpfer aber was uns fehlt ist ein spezieller Problemlöser, der uns den nötigen Vorteil verschafft.",,"Bonvenon en la lasta fajrero da espero. Nur ni havas liberan volon por kontraÅ­stari al La Ordeno. Ni havas la plej inteligentajn mensojn kaj multe da kapablaj homoj, sed mankas tiu... tiu, kiu estus vera problem-solvanto, por ke li donu al ni la necesan avantaÄon.","Bienvenido al último destello de esperanza. Solo nosotros tenemos el libre albedrío para oponernos a La Orden. Tenemos las mentes científicas más brillantes y muchas personas capaces, pero nos falta el que... el que sería todo un apagafuegos para que nos dé la ventaja que necesitamos.",,"Tervetuloa viimeisen toivonpilkahduksen äärelle. Ainoastaan meillä on vapaa tahto vastustaa Veljeskuntaa. Meillä on tieteen terävimmät mielet ja monia ruumiiltaan vahvoja, mutta meiltä puuttuu se yksi todellinen ongelmanratkoja, joka antaisi meille kaipaamamme etulyöntiaseman.","Bienvenue au dernier lieu d'espoir. Nous seuls avons la liberté d'esprit pour opposer l'Ordre. Nous avons les esprits scientifiques les plus aiguisés et de nombreux hommes habiles et sains.. Mais il nous manque quelqu'un qui pourrait.. résoudre nos problèmes. Nous donner un peu d'aide, nous permettre de prendre l'avantage.",,"Köszöntelek a remény utolsó pislákoló fényénél. Már csak mi merünk szembeszállni a Renddel. Itt vannak a legélesebb elmék, életerÅ‘s emberek, de hiányzik egy igazi...probléma megoldó, aki elÅ‘nyt szerez számunkra. Segíts nekünk!","Benenuto nell'ultimo barlume di speranza. Noi siamo i soli ad avere la determinazione per combattere l'Ordine. Abbiamo brillanti scienziati e molti soldati capaci, ma ci manca quel vero e proprio risolutore di problemi, che ci può dare quel vantaggio che cerchiamo.","よã†ã“ãã€ã“ã“ã¯æˆ‘々ã®åƒ…ã‹ãªå¸Œæœ›ãŒé›†ã¾ã‚‹ +"Welcome to the last flicker of hope. Only we have the free will to oppose the Order. We have the sharpest scientific minds, and many able bodies. But we lack that one real problem solver, who will give us the edge we need.",TXT_DLG_SCRIPT34_D27288_WELCO,MAP34: Macil.,,,"Vítej u posledního plamínku nadÄ›je. Jen my máme svobodnou vůli vzdorovat Řádu. Máme sice nejÄilejší vÄ›decké génie a mnoho schopných vojáků, ale chybí nám ten jeden Å™eÅ¡itel problémů, který by nás posunul vpÅ™ed.","Velkommen til det sidste lille hÃ¥b. Kun vi har den frie vilje til at modsætte os Ordenen. Vi har de skarpeste videnskabelige hjerner og mange dygtige kroppe. Men vi mangler den ene rigtige problemløser, som vil give os den fordel, vi har brug for.","Wilkommen beim letzten Hoffnungsschimmer. Nur wir haben noch den freien Willen, um gegen den Orden zu arbeiten. Wir haben die besten Wissenschaftler und viele fähige Kämpfer aber was uns fehlt ist ein spezieller Problemlöser, der uns den nötigen Vorteil verschafft.",,"Bonvenon en la lasta fajrero da espero. Nur ni havas liberan volon por kontraÅ­stari al La Ordeno. Ni havas la plej intelektajn mensojn kaj multe da kapablaj homoj, sed mankas tiu... tiu, kiu estus vera problem-solvulo, por ke li donu al ni la necesan avantaÄon.","Bienvenido al último destello de esperanza. Solo nosotros tenemos el libre albedrío para oponernos a La Orden. Tenemos las mentes científicas más brillantes y muchas personas capaces, pero nos falta el que... el que sería todo un apagafuegos para que nos dé la ventaja que necesitamos.",,"Tervetuloa viimeisen toivonpilkahduksen äärelle. Ainoastaan meillä on vapaa tahto vastustaa Veljeskuntaa. Meillä on tieteen terävimmät mielet ja monia ruumiiltaan vahvoja, mutta meiltä puuttuu se yksi todellinen ongelmanratkoja, joka antaisi meille kaipaamamme etulyöntiaseman.","Bienvenue au dernier lieu d'espoir. Nous seuls avons la liberté d'esprit pour opposer l'Ordre. Nous avons les esprits scientifiques les plus aiguisés et de nombreux hommes habiles et sains.. Mais il nous manque quelqu'un qui pourrait.. résoudre nos problèmes. Nous donner un peu d'aide, nous permettre de prendre l'avantage.",,"Köszöntelek a remény utolsó pislákoló fényénél. Már csak mi merünk szembeszállni a Renddel. Itt vannak a legélesebb elmék, életerÅ‘s emberek, de hiányzik egy igazi...probléma megoldó, aki elÅ‘nyt szerez számunkra. Segíts nekünk!","Benenuto nell'ultimo barlume di speranza. Noi siamo i soli ad avere la determinazione per combattere l'Ordine. Abbiamo brillanti scienziati e molti soldati capaci, ma ci manca quel vero e proprio risolutore di problemi, che ci può dare quel vantaggio che cerchiamo.","よã†ã“ãã€ã“ã“ã¯æˆ‘々ã®åƒ…ã‹ãªå¸Œæœ›ãŒé›†ã¾ã‚‹ 最後ã®å ´æ‰€ã ã€‚オーダーã¸ã¨ç«‹ã¡å‘ã‹ã†æ„æ€ã‚’ æŒã£ã¦ã„ã‚‹ã®ã¯æˆ‘々ãらã„ã ã€‚ ã“ã“ã«ã¯ç´ æ™´ã‚‰ã—ã„頭脳をæŒã¤å­¦è€…ãŸã¡ã€ @@ -12314,22 +12321,22 @@ Find a way to free the prisoners. Use the judge's hand to operate the hand scann è£åˆ¤å®˜ã®æ‰‹ã‚’使用ã—ã¦ãƒãƒ³ãƒ‰ã‚¹ã‚­ãƒ£ãƒŠãƒ¼ã‚¹ã‚¤ãƒƒãƒã‚’æ“作ã™ã‚‹ã€‚",수ê°ìžë“¤ì„ 풀어줄 ë°©ë²•ì„ ì°¾ìž. íŒì‚¬ì˜ ì†ì„ ì¨ì„œ 지문ì¸ì‹ìž¥ì¹˜ë¥¼ 통과하는 거야.,Zoek een manier om de gevangenen te bevrijden. Gebruik de hand van de rechter om de handscanner schakelaar te bedienen.,Finn en mÃ¥te Ã¥ befri fangene pÃ¥. Bruk dommerens hÃ¥nd til Ã¥ betjene hÃ¥ndskannerbryteren.,"Znajdź sposób na uwolnienie więźniów. Użyj rÄ™ki sÄ™dziego, aby uruchomić przełącznik skanera rÄ™cznego.",Encontre uma maneira de libertar os prisioneiros. Use a mão do juiz para operar o scanner.,,GăseÈ™te o cale să eliberezi prizonierii. FoloseÈ™te mâna judecătorului să operezi scanner-ul de mâini.,"Ðайди ÑпоÑоб оÑвободить пленников. ИÑпользуй руку Ñудьи, чтобы открыть замок Ñо Ñканером отпечатка руки.",,Hitta ett sätt att befria fÃ¥ngarna. Använd domarens hand för att använda handskannern.,Mahkûmları serbest bırakmanın bir yolunu bulun. El tarayıcı anahtarını çalıştırmak için yargıcın elini kullan. Way to go my friend. Good work freeing the prisoners. Jump on one of the teleporters and it will bring you back to base.,TXT_ILOG21,MAP05: After setting the prisoners free.,,,"Jen tak dál, příteli, dobrá práce. SkoÄ na teleportér, dostane tÄ› to zpÄ›t na základnu.","Godt gÃ¥et, min ven. Godt arbejde med at befri fangerne. Hop pÃ¥ en af teleportørerne, sÃ¥ kommer du tilbage til basen.","Das war gute Arbeit, wie du die Gefangenen befreit hast. Benutze einen der Teleporter um zur Basis zurückzugelangen.",,Danke al vi niaj kamaradoj estas liberaj. Uzu la teleportilon en la ĉelaro por reiri al la bazo.,Buen trabajo liberando a los prisioneros. Usa el teletransportador de la galería para volver a la base.,,"Niin sitä pitää, ystäväni. Hyvin toimittu vankien vapauttamisessa. Hyppää johonkin kaukosiirtimistä, ja se tuo sinut takaisin tukikohtaan.",Bien joué mon ami! Bon travail avec ces prisonniers. Utilise l'un des téléporteurs pour revenir à la base.,,"Remek volt. Szép munka volt a rabok kiszabadítása. Ugorj be valamelyik teleportba, és visszavisz a bázisba.","Ottimo lavoro, amico. Grazie a te i prigionieri sono stati liberati. Prendi uno dei teletrasporti e ti riporteremo alla base.","よãã‚„ã£ãŸç›¸æ£’。囚人を解放ã§ããŸã®ã¯è‰¯ã„åƒãã ã€‚ ã„ãšã‚Œã‹ã®ãƒ†ãƒ¬ãƒãƒ¼ã‚¿ãƒ¼ã«é£›ã³è¾¼ã‚€ã¨åŸºåœ°ã«æˆ»ã‚‹ã€‚","잘했어, 친구. 수ê°ìžë“¤ì„ 풀어주ëŠë¼ ê³ ìƒí–ˆì–´. ì € 텔레í¬í„°ë“¤ 중 하나를 타면 우리 기지로 복귀할 수 ìžˆì„ ê±°ì•¼.","Goed gedaan, mijn vriend. Goed werk om de gevangenen te bevrijden. Spring op een van de teleporters en die brengt je terug naar de basis.","SÃ¥nn skal det gjøres, min venn. Godt jobbet med Ã¥ befri fangene. Hopp pÃ¥ en av teleporterne, sÃ¥ kommer du tilbake til basen.","Brawo mój przyjacielu. Dobra robota z uwolnieniem więźniów. Wskocz na jeden z teleporterów, a przeniesie ciÄ™ on z powrotem do bazy.","Boa, meu amigo. Bom trabalho ao libertar os prisioneiros. Entre num desses teletransportadores e você voltará pra base.",,Bună treabă prietene. Sari într-un teleportor È™i te va duce la baza noastră.,"Так держать, дружище. Ты отлично поработал и оÑвободил пленников. Прыгай в любой телепорт, и он вернёт Ñ‚ÐµÐ±Ñ Ð½Ð° базу.",,"Bra jobbat, min vän. Bra jobbat med att befria fÃ¥ngarna. Hoppa pÃ¥ en av teleporterna sÃ¥ tar den dig tillbaka till basen.",Aferin dostum. Mahkûmları serbest bırakarak iyi iÅŸ çıkardın. Işınlayıcılardan birine atla ve seni üsse geri getirsin. -"Destroy the power crystal that runs the power grid which drives the Order's shields. Go visit Worner, a spy we recruited in the warehouse of the power station.",TXT_ILOG22,,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi navÅ¡tívit Wornera, Å¡pióna, kterého jsme najali ve skladu elektrárny.","Ødelæg energikrystallen, der driver det strømnet, som driver Ordenens skjolde. Besøg Worner, en spion, som vi rekrutterede i lageret pÃ¥ kraftværket.","Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Suche Worner auf, einen Spion, den wir im Lagerhaus des Kraftwerks rekrutiert haben. ",,"Detruu la energian kristalon, kiu impulsas la elektrizan sistemon, kiu tenas la Åildon de la kastelo. Renkontu nian spionon Worner en la magazeno (Warehouse) de la centralo (Power Station).","Destruye el cristal de poder que impulsa la red eléctrica que mantiene los escudos del castillo. Busca a Worner, un espía nuestro, en el almacén (Warehouse) de la central eléctrica (Power Station).",,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Mene tapaamaan Worneria, vakoojaa, jonka värväsimme voimalaitoksen varastolta.","Détruis le cristal qui contrôle la grille énergétique des boucliers de l'Ordre. Va voir Worner, un espion que l'on a recruté dans l'entrepôt de la centrale.",,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Keresd fel az erÅ‘műtÅ‘l betoborzott kémünket, Wornert.","Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Trova Worner, una spia che abbiamo reclutato nel magazzino della centrale energetica.","é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚ã‚。 +"Destroy the power crystal that runs the power grid which drives the Order's shields. Go visit Worner, a spy we recruited in the warehouse of the power station.",TXT_ILOG22,,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi navÅ¡tívit Wornera, Å¡pióna, kterého jsme najali ve skladu elektrárny.","Ødelæg energikrystallen, der driver det strømnet, som driver Ordenens skjolde. Besøg Worner, en spion, som vi rekrutterede i lageret pÃ¥ kraftværket.","Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Suche Worner auf, einen Spion, den wir im Lagerhaus des Kraftwerks rekrutiert haben. ",,"Renkontu nian spionon Worner en la magazeno (Warehouse) de la centralo (Power Station). Detruu la energian kristalon, kiu impulsas la elektrizan sistemon, kiu tenas la Åildon de la kastelo.","Busca a Worner, un espía nuestro, en el almacén (Warehouse) de la central eléctrica (Power Station). Destruye el cristal de poder que impulsa la red eléctrica que mantiene los escudos del castillo.",,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Mene tapaamaan Worneria, vakoojaa, jonka värväsimme voimalaitoksen varastolta.","Détruis le cristal qui contrôle la grille énergétique des boucliers de l'Ordre. Va voir Worner, un espion que l'on a recruté dans l'entrepôt de la centrale.",,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Keresd fel az erÅ‘műtÅ‘l betoborzott kémünket, Wornert.","Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Trova Worner, una spia che abbiamo reclutato nel magazzino della centrale energetica.","é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚ã‚。 発電所ã®å€‰åº«ã«æ½œã‚‰ã›ãŸã‚¹ãƒ‘イã€ãƒ¯ãƒ¼ãƒŠãƒ¼ã‚’訪ã­ã‚ˆã€‚","오ë”ì˜ ë°©ì–´ë§‰ì„ ìœ ì§€í•˜ëŠ” ë™ë ¥ ë§ì˜ ì „ë ¥ì›ì¸ 수정체를 파괴하ìž. ìš°ì„ , 발전소 ì°½ê³ ì— ìžˆëŠ” 우리 쪽 첩ìžì¸ 워너ì—게 찾아가 ë³´ìž.","Vernietig het energiekristal dat het stroomnet dat de schilden van de Orde aandrijft. Ga op bezoek bij Worner, een spion die we gerekruteerd hebben in het magazijn van de centrale.","Ødelegg kraftkrystallen som driver kraftnettet som driver Ordenens skjold. Besøk Worner, en spion vi rekrutterte i kraftstasjonens lager.","Zniszcz krysztaÅ‚ mocy, który zasila sieć energetycznÄ… napÄ™dzajÄ…cÄ… tarcze Zakonu. Idź odwiedzić Wornera, szpiega, którego zwerbowaliÅ›my w magazynie elektrowni.","Destrua o cristal de energia que alimenta a rede elétrica por trás dos escudos da Ordem. Visite o Worner, um espião que recrutamos no depósito da usina de energia.",,"Distruge cristalul de energie care alimentează scuturile Ordinului. Vizitează-l pe Worner, un spion recrutat în depozitul staÈ›iei de energie.","Уничтожь криÑталл, питающий ÑнергоÑеть Ордена, от которой работают их щиты. Ð’ÑтретьÑÑ Ñ Ð£Ð¾Ñ€Ð½Ñром, нашим шпионом, на Ñкладе ÑлектроÑтанции.",,"Förstör kraftkristallen som driver kraftnätet som driver Ordens sköldar. GÃ¥ och besök Worner, en spion som vi rekryterade i kraftverkets lager.",Tarikat'ın kalkanlarını çalıştıran güç ÅŸebekesini çalıştıran güç kristalini yok edin. Güç istasyonunun deposunda iÅŸe aldığımız casus Worner'ı ziyaret et. Let's get lost. There's more fun to come.,TXT_ILOG24,MAP04: After leaving Reactor Core.,,,"ZtraÅ¥me se, za chvíli tu bude víc srandy.",Lad os fare vild. Der er mere sjov pÃ¥ vej.,Lass uns verschwinden. Hier wird's gleich ungemütlich.,,Ni devas eliri: la alarmo eksonis.,Hay que irse: la central está alerta.,,Häivytään. Lisää hupia on luvassa.,"Partons d'ici, il y a encore de quoi s'amuser.",,Húzzunk innen. Most jön még az izgi rész.,Leviamoci di torno. C'è ancora da divertirsi.,è¿·å­ã«ãªã£ãŸã‚‰ã€ã‚‚ã£ã¨æ¥½ã—ã‚ã‚‹ãžã€‚,어서 여길 떠나ìž. ë…€ì„ë“¤ì´ ì˜¤ê¸° ì „ì— ë§ì´ì•¼.,Laten we verdwalen. Er komt nog meer plezier.,La oss stikke. Det er mer moro i vente.,Zgubmy siÄ™. Czeka nas jeszcze wiÄ™cej zabawy.,Então se manda. Tem mais diversão por vir.,,Să ne pierdem. O să mai fie distracÈ›ii.,Давай-ка иÑчезнем отÑюда. Скоро начнётÑÑ Ð²ÐµÑелье.,,LÃ¥t oss gÃ¥ vilse. Det finns mer kul att göra.,Hadi kaybolalım. Daha çok eÄŸlence var. -"One more adventure before command feels it's safe to attack the castle. Macil's arranged for Irale to give you gold and some training. After you visit him, see the medic in town for strength.",TXT_ILOG25,MAP02: After destroying the crystal in the Power Station.,,,"JeÅ¡tÄ› jedno dobrodružství, než se velení rozhodne zaútoÄit. Macil sjednal u Iraleho zlato a trénink. Pak navÅ¡tiv mÄ›stského lékaÅ™e pro sílu.","Et eventyr mere, før kommandoen føler, at det er sikkert at angribe slottet. Macil har sørget for, at Irale giver dig guld og noget træning. NÃ¥r du har besøgt ham, skal du gÃ¥ til lægen i byen for at fÃ¥ styrke.","Eine weitere Aufgabe noch, bevor Macil es für sicher hält, die Burg anzugreifen. Macil hat Irale gebeten, dir Gold zu geben und Training zu organisieren. Nachdem du bei ihm warst, lass dir vom Sanitäter in der Stadt deine Ausdauer erhöhen.",,"AnkoraÅ­ unu komisio por ke la Fronto sentu, ke sturmi la kastelon estas sekure. Macil diris al Irale, ke li donu oron kaj trejnadon al vi. Post tio, iru ĉe la kuracisto (Hospital) de la urbo pro la enplantaĵo.","Una misión más para que el comando sienta que es seguro asaltar el castillo. Macil le ha dicho a Irale que te dé oro y entrenamiento. Tras visitarlo, ve con el médico del pueblo para cambiar el implante.","Una misión más para que el comando sienta que es seguro asaltar el castillo. Macil le dijo a Irale que te dé oro y entrenamiento. Tras visitarlo, ve con el médico del pueblo para cambiar el implante.","Vielä yksi seikkailu, ennen kuin komentokeskus katsoo tilanteen hyväksi hyökätä linnaa vastaan. Macil on järjestänyt Iralen antamaan sinulle kultaa ja koulutusta. Kun olet tavannut häntä, tapaa kaupungilla lääkintämiestä vahvistusta varten.","Encore une aventure avant que le commandement ne pense qu'il soit bon temps d'attaquer le château. Macil s'est arrangé pour qu'Irale te donne de l'argent et qu'il t'entraîne. Une fois que tu en as fini avec lui, va visiter le médecin en ville pour qu'il te rende des forces.",,"Még egy kaland, mielÅ‘tt az irányítás hatékony támadást indíthat a kastély ellen. Macil elintézte, hogy Irale ellásson arannyal és kiképzéssel. Utána keresd fel a szanitécet, hogy kicsit kipofozzon.","Un'ultima avventura prima dell'assedio al castello. Macil ha parlato ad Irale per farti avere dell'oro e un po' di addestramento. Dopo che gli hai fatto visita, parla anche dal medico in città per una marcia in più.","ã‚‚ã†ä¸€ã¤ã®ä»»å‹™ã‚’ã“ãªã™å‰ã«æˆ¦åŠ›ã®å¢—å¼·ãŒå¿…è¦ã¨è€ƒãˆã‚‰ã‚Œã‚‹ã€‚ +"One more adventure before command feels it's safe to attack the castle. Macil's arranged for Irale to give you gold and some training. After you visit him, see the medic in town for strength.",TXT_ILOG25,MAP02: After destroying the crystal in the Power Station.,,,"JeÅ¡tÄ› jedno dobrodružství, než se velení rozhodne zaútoÄit. Macil sjednal u Iraleho zlato a trénink. Pak navÅ¡tiv mÄ›stského lékaÅ™e pro sílu.","Et eventyr mere, før kommandoen føler, at det er sikkert at angribe slottet. Macil har sørget for, at Irale giver dig guld og noget træning. NÃ¥r du har besøgt ham, skal du gÃ¥ til lægen i byen for at fÃ¥ styrke.","Eine weitere Aufgabe noch, bevor Macil es für sicher hält, die Burg anzugreifen. Macil hat Irale gebeten, dir Gold zu geben und Training zu organisieren. Nachdem du bei ihm warst, lass dir vom Sanitäter in der Stadt deine Ausdauer erhöhen.",,"AnkoraÅ­ unu komisio por ke la Fronto sentu, ke sturmi la kastelon estas sekure. Macil diris al Iralo, ke li donu oron kaj trejnadon al vi. Post tio, iru ĉe la kuracisto (Hospital) de la urbo pro la enplantaĵo.","Una misión más para que el comando sienta que es seguro asaltar el castillo. Macil le ha dicho a Irale que te dé oro y entrenamiento. Tras visitarlo, ve con el médico del pueblo para cambiar el implante.","Una misión más para que el comando sienta que es seguro asaltar el castillo. Macil le dijo a Irale que te dé oro y entrenamiento. Tras visitarlo, ve con el médico del pueblo para cambiar el implante.","Vielä yksi seikkailu, ennen kuin komentokeskus katsoo tilanteen hyväksi hyökätä linnaa vastaan. Macil on järjestänyt Iralen antamaan sinulle kultaa ja koulutusta. Kun olet tavannut häntä, tapaa kaupungilla lääkintämiestä vahvistusta varten.","Encore une aventure avant que le commandement ne pense qu'il soit bon temps d'attaquer le château. Macil s'est arrangé pour qu'Irale te donne de l'argent et qu'il t'entraîne. Une fois que tu en as fini avec lui, va visiter le médecin en ville pour qu'il te rende des forces.",,"Még egy kaland, mielÅ‘tt az irányítás hatékony támadást indíthat a kastély ellen. Macil elintézte, hogy Irale ellásson arannyal és kiképzéssel. Utána keresd fel a szanitécet, hogy kicsit kipofozzon.","Un'ultima avventura prima dell'assedio al castello. Macil ha parlato ad Irale per farti avere dell'oro e un po' di addestramento. Dopo che gli hai fatto visita, parla anche dal medico in città per una marcia in più.","ã‚‚ã†ä¸€ã¤ã®ä»»å‹™ã‚’ã“ãªã™å‰ã«æˆ¦åŠ›ã®å¢—å¼·ãŒå¿…è¦ã¨è€ƒãˆã‚‰ã‚Œã‚‹ã€‚ マシルãŒã‚¤ãƒ©ãƒ¼ãƒ«ã‚’介ã—ã¦è³‡é‡‘ã¨è¨“ç·´ã®æ‰‹é…ã‚’ã—ã¦ãれるã®ã§ä¼šã„ãªã•ã„。 ãれã¨è¿‘所ã®ãƒ¡ãƒ‡ã‚£ãƒƒã‚¯ã‚‚支æ´ã—ã¦ãれるã‚。",본부가 ê³µì„±ì „ì„ ì§„í–‰í•˜ê¸° ì „ì— í•œ 가지 해야 í•  ì¼ì´ 있어. ë§ˆì‹¤ì´ ì´ë¡¤ë¦¬ì—게 골드랑 í›ˆë ¨ì„ ì œê³µí•´ë‹¬ë¼ê³  ë¶€íƒí–ˆëŒ€. 그리고 ë§ˆì„ ë³‘ì›ì— 가서 ê·¼ë ¥ì„ í‚¤ì›Œë‹¬ë¼ê³  í•´.,"Nog een avontuur voor het commando het gevoel heeft dat het veilig is om het kasteel aan te vallen. Macil heeft geregeld dat Irale je goud en wat training geeft. Nadat je hem hebt bezocht, ga je naar de dokter in de stad voor kracht.","Ett eventyr til før kommandoen mener det er trygt Ã¥ angripe slottet. Macil har sørget for at Irale gir deg gull og litt trening. Etter at du har besøkt ham, gÃ¥ til legen i byen for Ã¥ fÃ¥ styrke.","Jeszcze jedna przygoda, zanim dowództwo uzna, że można bezpiecznie zaatakować zamek. Macil umówiÅ‚ siÄ™ z Irale, że da ci zÅ‚oto i trochÄ™ treningu. Po wizycie u niego, udaj siÄ™ do medyka w mieÅ›cie po siłę.","Mais uma aventura antes que o comando ache que é seguro o bastante para atacar o castelo. Macil instruiu Irale a te passar ouro e treinamento. Após visitá-lo, vá ao médico para te dar uma força.",,"ÃŽncă o aventură până ce comanda e încrezătoare în atacarea castelului. Macil a aranjat ca Irale să îți ofere bani È™i antrenament. Dupa ce îț vizitezi, mergi la medicul din oraÈ™ pentru forță.","Ещё одно приключение, и штаб будет уверен в уÑпешноÑти атаки на замок. МÑйÑил поручил ИрÑйлу выплатить тебе награду и дать пару уроков. ПоÑле вÑтречи Ñ Ð½Ð¸Ð¼, поÑети городÑкого медика.",,Ett äventyr till innan kommandot känner att det är säkert att attackera slottet. Macil har ordnat sÃ¥ att Irale ger dig guld och lite träning. Efter att du besökt honom ska du gÃ¥ till läkaren i staden för att fÃ¥ styrka.,"Komutanlık kaleye saldırmanın güvenli olduÄŸunu düşünmeden önce bir macera daha. Macil, İrale'nin sana altın ve biraz eÄŸitim vermesini ayarladı. Onu ziyaret ettikten sonra, güç için kasabadaki doktoru gör." -"Visit Irale and the medic in town for gold and training, then find the sewers. Head along the river across from the Governor's mansion.",TXT_ILOG26,〃,,,"NavÅ¡tiv Iraleho a doktora pro zlato a trénink, pak najdi stoky. Jdi podél Å™eky naproti guvernérovÄ› sídlu.","Besøg Irale og lægen i byen for at fÃ¥ guld og træning, og find derefter kloakkerne. GÃ¥ langs floden over for guvernørens palæ.","Geh zu Irale und dem Sanitäter in der Stadt, um etwas Gold und Training zu bekommen, dann finde die Kanalisation. Folge dem Fluss auf der gegenüberliegenden Seite der Villa des Gouverneurs.",,"Iru ĉe Irale kaj ĉe la kuracisto (Hospital) de la urbo, tiam trovu la kloakon: serĉu enirejon inter la rojo kaj la urbodomo.","Visita a Irale y al médico del pueblo, luego encuentra las alcantarillas: busca una entrada entre el arroyo y el ayuntamiento.",,"Tapaa Iralea ja lääkintämiestä kaupungilla kultaa ja koulutusta varten, minkä jälkeen löydä viemärit. Kulje joen vartta kuvernöörin kartanoa vastapäätä.","Visite Irale et le médecin en ville pour de l'argent et de l'entraînement, puis trouve l'entrée des égouts. Descend le long de la rivière de l'autre côté du manoir du gouverneur.",,"Keresd fel Iralet és a szanitécet a városban egy kis aranyért és kiképzésért, aztán irány a kanáris. Menj a folyón ami átszeli a kormányzó kúriáját.","Visita Irale e il medico in città per oro e addestramento, dopodiché trova le fogne. Vi puoi accedere dal fiume vicino alla dimora del governatore.","イラールã¨ãƒ¡ãƒ‡ã‚£ãƒƒã‚¯ã‚’å°‹ã­ã¦è³‡é‡‘ã¨è¨“ç·´ã‚’å—ã‘ã€ä¸‹æ°´é“ã«å‘ã‹ã†ã‚“ã ã€‚ +"Visit Irale and the medic in town for gold and training, then find the sewers. Head along the river across from the Governor's mansion.",TXT_ILOG26,〃,,,"NavÅ¡tiv Iraleho a doktora pro zlato a trénink, pak najdi stoky. Jdi podél Å™eky naproti guvernérovÄ› sídlu.","Besøg Irale og lægen i byen for at fÃ¥ guld og træning, og find derefter kloakkerne. GÃ¥ langs floden over for guvernørens palæ.","Geh zu Irale und dem Sanitäter in der Stadt, um etwas Gold und Training zu bekommen, dann finde die Kanalisation. Folge dem Fluss auf der gegenüberliegenden Seite der Villa des Gouverneurs.",,"Iru ĉe Iralo kaj ĉe la kuracisto (Hospital) de la urbo, tiam trovu la kloakon: serĉu enirejon inter la rojo kaj la urbodomo.","Visita a Irale y al médico del pueblo, luego encuentra las alcantarillas: busca una entrada entre el arroyo y el ayuntamiento.",,"Tapaa Iralea ja lääkintämiestä kaupungilla kultaa ja koulutusta varten, minkä jälkeen löydä viemärit. Kulje joen vartta kuvernöörin kartanoa vastapäätä.","Visite Irale et le médecin en ville pour de l'argent et de l'entraînement, puis trouve l'entrée des égouts. Descend le long de la rivière de l'autre côté du manoir du gouverneur.",,"Keresd fel Iralet és a szanitécet a városban egy kis aranyért és kiképzésért, aztán irány a kanáris. Menj a folyón ami átszeli a kormányzó kúriáját.","Visita Irale e il medico in città per oro e addestramento, dopodiché trova le fogne. Vi puoi accedere dal fiume vicino alla dimora del governatore.","イラールã¨ãƒ¡ãƒ‡ã‚£ãƒƒã‚¯ã‚’å°‹ã­ã¦è³‡é‡‘ã¨è¨“ç·´ã‚’å—ã‘ã€ä¸‹æ°´é“ã«å‘ã‹ã†ã‚“ã ã€‚ 入りå£ã¯çŸ¥äº‹ã®ãƒžãƒ³ã‚·ãƒ§ãƒ³ã®å·ã‚’挟んã å‘ã‹ã„å´ã ã€‚","ì´ë¡¤ë¦¬ì™€ ë§ˆì„ ì˜ë¬´ê´€ì—게 가서 골드와 í›ˆë ¨ì„ ë°›ê³ , 하수구를 찾아. ì´ë…ì˜ ê´€ì € ê±´ë„ˆíŽ¸ì˜ ê°•ì„ ë”°ë¼ì„œ ê°€ë´.","Bezoek Irale en de dokter in de stad voor goud en training, en zoek dan de riolen. Loop langs de rivier tegenover het landhuis van de gouverneur.","Besøk Irale og legen i byen for gull og trening, og finn deretter kloakken. GÃ¥ langs elven overfor guvernørens hus.","Odwiedź Irale i medyka w mieÅ›cie po zÅ‚oto i trening, a nastÄ™pnie znajdź kanaÅ‚y. Skieruj siÄ™ wzdÅ‚uż rzeki naprzeciwko rezydencji gubernatora.",Visite o Irale e o médico na cidade para ouro e treinamento. Depois encontre o esgoto. Vá para o rio do outro lado da mansão do Governador.,,"Vizitează pe Irale È™i medicul din oraÈ™ pentru aur È™i antrenament, apoi găseÈ™te canalele. Urmează râul de lângă complexul Guvernatorului.","Зайди к ИрÑйлу и городÑкому медику за золотом и обучением, потом ÑпуÑтиÑÑŒ в канализацию. Иди вдоль берега реки, ÑпуÑк прÑмо напротив оÑобнÑка губернатора.",,Besök Irale och läkaren i staden för att fÃ¥ guld och träning och hitta sedan kloakerna. GÃ¥ längs floden mittemot guvernörens herrgÃ¥rd.,"Altın ve eÄŸitim için İrale'yi ve kasabadaki doktoru ziyaret edin, ardından kanalizasyonları bulun. Valinin konağının karşısındaki nehir boyunca ilerleyin." "We're looking for Weran, who calls himself the Rat King. I'm sure it's descriptive as well as colorful.",TXT_ILOG28,MAP06: Entrance.,,,"Hledáma Werana, který si říká Krysí král. Jsem si jistá, že to je jak barvité, tak pÅ™esné.","Vi leder efter Weran, som kalder sig selv Rottekongen. Det er sikkert bÃ¥de beskrivende og farverigt.","Wir suchen Weran, den man auch den Rattenkönig nennt. Ich bin sicher, dass dies ihn sehr anschaulich beschreibt.",,"Ni serĉas Weran-on, kiu proklamas sin mem la Rat-reÄo. Mi estas certa, ke tio estas tre fidela priskribo.","Estamos buscando a Weran, quien se autoproclama el Rey de las Ratas. Estoy segura de que es fielmente descriptivo.",,"Etsimme Werania, joka kutsuu itseään Rottakuninkaaksi. Varmastikin kuvaava titteli, mutta myös väritetty.","Cherche Weran, il se fait appeler le Roi des Rats. Je suis sur que c'est aussi descriptif que flatteur.",,"Werant keressük, aki magát csak a Patkány Királyként emlegeti. Biztos vagyok, hogy nem csak színes név, de valszeg hasonlít is hozzá.","Stiamo cercando Weran, che si fa chiamare il Re dei Ratti. Sono sicura che lo descrive appieno.","誰ãŒå‘¼ã‚“ã ã‹ãƒ©ãƒƒãƒˆã‚­ãƒ³ã‚°ã®ã‚¦ã‚§ãƒ©ãƒ³ã‚’探ã™ã‚“ã ã€‚ ãれãŒå™è¿°çš„ã§ã‚‚ã‚り派手ãªå‘¼ã³åãªã®ã‚‚定ã‹ã ã€‚",우리는 ìžì‹ ì„ 시ê¶ì¥ 왕ì´ë¼ê³  부르는 ì›Œë Œì„ ì°¾ì•„ì•¼ í•´. 알ë¡ë‹¬ë¡í•´ì„œ ëˆˆì— ìž˜ ëŒ ê²ƒ ê°™ì€ë° ë§ì´ì•¼.,"We zijn op zoek naar Weran, die zichzelf de rattenkoning noemt. Ik weet zeker dat het zowel beschrijvend als kleurrijk is.","Vi leter etter Weran, som kaller seg Rottekongen. Det er sikkert bÃ¥de beskrivende og fargerikt.","Szukamy Werana, który nazywa siebie Królem Szczurów. Na pewno jest to opisowe, jak i barwne.","Estamos procurando pelo Weran, que se autodenomina o Rato Rei. Imagino que isso seja tão descritivo quanto gracioso.",,"ÃŽl căutam pe Weran, care ăși spune Regele Șobolanilor. Sunt sigur că e descriptiv È™i colorat.","Ðам нужен УÑран — он называет ÑÐµÐ±Ñ ÐšÑ€Ñ‹Ñиным королём. Уверена, прозвище так же емко, как и краÑочно.",,Vi letar efter Weran som kallar sig för rÃ¥ttkungen. Jag är säker pÃ¥ att det är bÃ¥de beskrivande och färgstarkt.,Kendisine Fare Kral diyen Weran'ı arıyoruz. Renkli olduÄŸu kadar açıklayıcı da olduÄŸuna eminim. -"Take the flamethrower parts to Irale. Drain the reclamation tank. At the bottom is a hidden entrance to the sewers. The gate controls are down there, somewhere. Destroy them.",TXT_ILOG33,MAP06: After giving the guard's uniform to Weran.,,,Vezmi Äásti plamenometu k Iralemu. VypusÅ¥ obnovovací nádrž. Na dnÄ› je skrytý vchod do stok. NÄ›kde tam je ovládání bran. ZniÄ jej.,Tag flammekasterdelene med til Irale. Tøm genvindingstanken. I bunden er der en skjult indgang til kloakkerne. Kontrolelementerne til porten er dernede et sted. Ødelæg dem.,Bringe dir Flammenwerferteile zu Irale. Leere den Sammeltank. Am Boden ist ein versteckter Eingang zur Kanalisation. Dort ist die Steuerung für die Tore. Zerstöre sie.,,"Portu la partojn de la flamĵetilo al Irale. Drenu la rezervujon en la kastelo: en Äia fundo estas kaÅita enirejo al la kloako, kaj ie tie estas la regilo de la pordo; detruu Äin.",Llévale las partes del lanzallamas a Irale. Drena el tanque de recuperación del castillo: al fondo hay una entrada secreta a las alcantarillas y por ahí mismo el control de la puerta; destrúyelo.,,Toimita liekinheittimen osat Iralelle. Tyhjennä nesteentalteenottoallas. Sen pohjalla on viemärin salainen sisäänkäynti. Portin ohjaimet sijaitsevat jossakin siellä. Tuhoa ne.,Amène les pièces du lance-flamme à Irale. Fais vider le réservoir de recyclage. Au fond se trouve une entrée cachée vers les égouts. Les contrôles de la porte s'y trouvent. Détruis-les.,,Vidd a lángszóró darabokat Iralehez. Ereszd le a szárító tartályt. Az alján megtalálod a titkos lejáratot a kanárisba. A kapu vezérlése ott van lent valahol. Semmisítsd meg Å‘ket.,"Porta i pezzi del lanciafiamme da Irale. Svuota la vasca di recupero fluidi. Nel fondo c'è un'entrata nascosta per le fogne. I controlli dei cancelli sono laggiù, da qualche parte. Distruggili.","ç«ç‚Žæ”¾å°„器ã®éƒ¨å“ã‚’ã‚¤ãƒ©ãƒ¼ãƒ«ã«æŒã£ã¦ã„ã。 +"Take the flamethrower parts to Irale. Drain the reclamation tank. At the bottom is a hidden entrance to the sewers. The gate controls are down there, somewhere. Destroy them.",TXT_ILOG33,MAP06: After giving the guard's uniform to Weran.,,,Vezmi Äásti plamenometu k Iralemu. VypusÅ¥ obnovovací nádrž. Na dnÄ› je skrytý vchod do stok. NÄ›kde tam je ovládání bran. ZniÄ jej.,Tag flammekasterdelene med til Irale. Tøm genvindingstanken. I bunden er der en skjult indgang til kloakkerne. Kontrolelementerne til porten er dernede et sted. Ødelæg dem.,Bringe dir Flammenwerferteile zu Irale. Leere den Sammeltank. Am Boden ist ein versteckter Eingang zur Kanalisation. Dort ist die Steuerung für die Tore. Zerstöre sie.,,"Portu la partojn de la flamĵetilo al Iralo. Drenu la rezervujon en la kastelo: en Äia fundo estas kaÅita enirejo al la kloako, kaj ie tie estas la regilo de la pordo; detruu Äin.",Llévale las partes del lanzallamas a Irale. Drena el tanque de recuperación del castillo: al fondo hay una entrada secreta a las alcantarillas y por ahí mismo el control de la puerta; destrúyelo.,,Toimita liekinheittimen osat Iralelle. Tyhjennä nesteentalteenottoallas. Sen pohjalla on viemärin salainen sisäänkäynti. Portin ohjaimet sijaitsevat jossakin siellä. Tuhoa ne.,Amène les pièces du lance-flamme à Irale. Fais vider le réservoir de recyclage. Au fond se trouve une entrée cachée vers les égouts. Les contrôles de la porte s'y trouvent. Détruis-les.,,Vidd a lángszóró darabokat Iralehez. Ereszd le a szárító tartályt. Az alján megtalálod a titkos lejáratot a kanárisba. A kapu vezérlése ott van lent valahol. Semmisítsd meg Å‘ket.,"Porta i pezzi del lanciafiamme da Irale. Svuota la vasca di recupero fluidi. Nel fondo c'è un'entrata nascosta per le fogne. I controlli dei cancelli sono laggiù, da qualche parte. Distruggili.","ç«ç‚Žæ”¾å°„器ã®éƒ¨å“ã‚’ã‚¤ãƒ©ãƒ¼ãƒ«ã«æŒã£ã¦ã„ã。 浄化タンクを排水ã›ã‚ˆã€‚ãã®ä¸‹å±¤ã«ä¸‹æ°´é“ã¸ã®éš ã•れãŸå…¥ã‚Šå£ãŒã‚る。 ゲートコントロールを探ã—出ã—ã€ç ´å£Šã›ã‚ˆã€‚",ì´ í™”ì—¼ë°©ì‚¬ê¸° ë¶€í’ˆì„ ê°€ì§€ê³  ì´ë¡¤ë¦¬ì—게 ë§ ê±¸ì–´ë´. ìˆ˜ì¡°ì— ìžˆëŠ” ë¬¼ì„ ë¹„ìš°ë©´ ë°‘ì— í•˜ìˆ˜ë„로 향하는 비밀 입구가 ë³´ì¼ ê±°ì•¼. ì„±ë¬¸ì˜ ê´€ë¦¬ 장치가 여기 ì–´ë”˜ê°€ì— ìžˆì„ ê±°ì•¼. 찾으면 파괴해.,"Neem de vlammenwerperonderdelen mee naar Irale. Laat de terugwinnings tank leeglopen. Onderaan is een verborgen ingang naar de riolering. De poortbediening is daar beneden, ergens. Vernietig ze.","Ta flammekasterdelene til Irale. Tøm utvinningstanken. Nederst er det en skjult inngang til kloakken. Portkontrollene er der nede, et sted. Ødelegg dem.",Zabierz części miotacza ognia do Irale. Opróżnij zbiornik rekultywacyjny. Na dole jest ukryte wejÅ›cie do kanałów. Sterowniki bramy sÄ… gdzieÅ› tam na dole. Zniszcz je.,Leve as peças do lança-chamas ao Irale. Drene o tanque de recuperação. No fundo há uma entrada oculta para o esgoto. Os controles do portão estão lá embaixo em algum lugar. Destrua-os.,,"Du bucățiile aruncătorului la Irale. Drenează rezervorul recuperării. La fund e o intrare secretă către canale. Controlul porÈ›ii e jos, undeva. Distruge-l.","ОтнеÑи детали огнемёта ИрÑйлу. Слей жидкоÑть из бака Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÑ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸. Ðа его дне Ñкрытый вход в канализацию. Механизм ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð²Ð¾Ñ€Ð¾Ñ‚Ð°Ð¼Ð¸ где-то там, внизу. Сломай его.",,Ta med dig flamethrowerdelarna till Irale. Töm Ã¥tervinningstanken. I botten finns en dold ingÃ¥ng till kloakerna. Grindkontrollerna finns där nere nÃ¥gonstans. Förstör dem.,Alev makinesi parçalarını İrale'ye götür. Islah tankını boÅŸaltın. En altta kanalizasyona gizli bir giriÅŸ var. Kapı kontrolleri aÅŸağıda bir yerde. Onları yok et. "Command, he's done it! The gates are open. Send in the shock troops and tell Macil we're coming in! Let's get back to the Front base.",TXT_ILOG37,MAP06: After destroying the gate controls.,,,"Velení, dokázal to! Brány jsou otevÅ™ené. PoÅ¡lete tam úderníky a Å™eknÄ›te Macilovi, že pÅ™icházíme! VraÅ¥ se na základnu Fronty.","Kommando, han har gjort det! Portene er Ã¥bne. Send choktropperne ind og fortæl Macil, at vi kommer ind! Lad os komme tilbage til frontbasen.",Kommandozentrale: Er hat's geschafft! Die Tore sind offen. Schickt die Schocktruppen hinein und sagt Macil das wir auf dem Weg sind! Lass uns zur Frontbasis zurückkehren.,,"Fronto, li sukcesis! Li malfermis la pordojn. Sendu la sturmantojn kaj diru al Macil, ke estas tempo eniri! Ni reiru al la bazo de la Fronto.","¡Comando, lo ha conseguido! Las puertas ya están abiertas. ¡Enviad a las tropas de choque y decidle a Macil que ya es hora de entrar! Volvamos a la base del Frente.","¡Comando, lo logró! Las puertas ya están abiertas. ¡Manden a las tropas de choque y díganle a Macil que ya es hora de entrar! Volvamos a la base del Frente.","Komentokeskus, hän teki sen! Portit ovat auki; lähettäkää iskujoukot ja kertokaa Macilille, että vyörymme sisään! Palataan Rintaman tukikohtaan.","C'est bon, commandement, il a réussi! Les portes sont ouvertes, envoyez les soldats de choc et dites à Macil que l'on arrive! Retournons à la base du Front.",,"Irányítás, megcsinálta! A kpuk kinyíltak. Küldjétek be az elektromos egységeket, és értesítsd Macilt hogy jövünk! Irány vissza a homlokfronthoz.","Comando, ce l'ha fatta! I cancelli sono aperti. Mandate avanti le truppe e dite a Macil che stiamo arrivando! Torniamo alla base del Fronte.","å¸ä»¤å®˜ã€å½¼ã¯ã‚„ã£ãŸãž! ゲートãŒé–‹ã„ãŸã€‚çªæ’ƒéƒ¨éšŠã‚’é€ã£ã¦æˆ‘々も続ãã¨ãƒžã‚·ãƒ«ã«ä¼ãˆã¦ãれ!",본부! 그가 해냈습니다! ì„±ë¬¸ì´ ì—´ë ¸ìŠµë‹ˆë‹¤. 공습부대를 성문으로 ì´ë™ì‹œí‚¤ê³  사령관님께 ìš°ë¦¬ë“¤ë„ ë”°ë¼ì˜¬ ê±°ë¼ê³  전해주세요. ì´ì œ 프론트 기지로 ëŒì•„ê°€!,"Commando, hij heeft het gedaan! De poorten zijn open. Stuur de stoottroepen naar binnen en zeg Macil dat we binnenkomen! Laten we teruggaan naar de voorste basis.","Kommando, han har klart det! Portene er Ã¥pne. Send inn sjokktroppene og si til Macil at vi kommer inn! La oss dra tilbake til frontbasen.","Dowództwo, udaÅ‚o mu siÄ™! Wrota sÄ… otwarte. WyÅ›lij oddziaÅ‚y uderzeniowe i powiedz Macilowi, że wchodzimy! Wracajmy do bazy frontowej.","Comando, ele conseguiu! Os portões estão abertos. Mande as tropas de choque e diga ao Macil que estamos entrando! Vamos voltar pra base da Frente.",,"Comandă, a reuÈ™it! PorÈ›ile sunt deschise. Trimite trupele de È™oc È™i spune-i lui Macil că intrăm! Să ne întoarcem la baza Frontului.","Штаб, он выполнил задание! Ворота открыты. ПоÑылайте ударный отрÑд и доложите МÑйÑилу, что мы пробилиÑÑŒ! ВозвращайÑÑ Ð½Ð° базу СопротивлениÑ.",,"Kommando, han har gjort det! Portarna är öppna. Skicka in chocktrupperna och säg till Macil att vi kommer in! LÃ¥t oss Ã¥tervända till Frontbasen.","Komuta, baÅŸardı! Kapılar açıldı. Åžok birliklerini gönderin ve Macil'e geldiÄŸimizi söyleyin! Cephe üssüne geri dönelim." -"Join the assault on the castle. Find and take out the Programmer. We have conflicting reports about his location. One says he in a computer room, another hiding in a sub-level temple, and yet another at the end of a long hallway.",TXT_ILOG38,MAP07: Entrace.,,,"PÅ™ipoj se k útoku na hrad. Najdi a zabij Programátora. Informace o jeho pozici se různí. Jedna mluví o místnosti s poÄítaÄi, druhá o podzemním chrámu a tÅ™etí o konci dlouhé chodby.","Deltag i angrebet pÃ¥ slottet. Find og udslæt programmøren. Vi har modstridende rapporter om hans placering. En siger, at han er i et computerrum, en anden, at han gemmer sig i et tempel pÃ¥ et underliggende niveau, og endnu en anden for enden af en lang korridor.","Schließe dich dem Angriff auf die Burg an. Finde den Programmierer und schalte ihn aus. Wir haben widersprüchliche Angaben über seinen Aufenthaltsort. Eine sagt, in einem Computerraum, ein anderer, dass er sich in einem Tempelkeller versteckt und noch ein anderer sagt was vom Ende eines langen Flurs.",,Trovu kaj mortigu la Programiston. Estas kontraÅ­diraj informoj pri lia pozicio: en komputila ĉambro aÅ­ en la subtera etaÄo de templo aÅ­ ĉe la fundo de longa koridoro.,"Encuentra y elimina al Programador. Tenemos informes contradictorios sobre su ubicación: uno que está en una sala de ordenadores, otro en el sótano de un templo y otro al final de un largo pasillo.","Encuentra y elimina al Programador. Tenemos informes contradictorios sobre su ubicación: uno que está en una sala de computadoras, otro en el sótano de un templo y otro al final de un largo pasillo.","Osallistu hyökkäykseen linnaa vastaan. Etsi ja kukista Ohjelmoitsija. Meillä on hänen sijainnistaan eriäviä tietoja: Yhden mukaan hän on tietokonehuoneessa, toisen mukaan maanalaisessa temppelissä ja vielä kolmannen mukaan pitkän käytävän päässä.","Rejoins l'assaut sur le château. Trouve et élimine le Programmeur. On a des rapports en conflit sur sa position. L'un d'entre eux nous dit qu'il se trouve dans la salle des ordinateurs, l'autre nous dit qu'il se cache dans un temple en sous sol, et un autre encore au bout d'un long couloir.",,"Csatlakozz a kastély ostromához. Keresd meg és iktasd ki a Programozót. Ellentmondó információink vannak a hollétérÅ‘l. Az egyik szerint a számítógép szobában van, másik szerint egy föld alatti templomban, harmadik szerint egy hosszú folyosó végén.","Unisciti all'assalto sul castello. Trova e fai fuori il Programmatore. I rapporti che abbiamo sulla sua posizione si contraddicono. Uno lo piazza in una stanza di computer, un altro in un tempio di sottolivello, e un altro ancora alla fine di un lungo corridoio.","城ã«å‘ã‹ã†çªæ’ƒéƒ¨éšŠã«å‚加ã—ã¦ãƒ—ログラマーを探ã—出ã›ã€‚ +"Join the assault on the castle. Find and take out the Programmer. We have conflicting reports about his location. One says he in a computer room, another hiding in a sub-level temple, and yet another at the end of a long hallway.",TXT_ILOG38,MAP07: Entrace.,,,"PÅ™ipoj se k útoku na hrad. Najdi a zabij Programátora. Informace o jeho pozici se různí. Jedna mluví o místnosti s poÄítaÄi, druhá o podzemním chrámu a tÅ™etí o konci dlouhé chodby.","Deltag i angrebet pÃ¥ slottet. Find og udslæt programmøren. Vi har modstridende rapporter om hans placering. En siger, at han er i et computerrum, en anden, at han gemmer sig i et tempel pÃ¥ et underliggende niveau, og endnu en anden for enden af en lang korridor.","Schließe dich dem Angriff auf die Burg an. Finde den Programmierer und schalte ihn aus. Wir haben widersprüchliche Angaben über seinen Aufenthaltsort. Eine sagt, in einem Computerraum, ein anderer, dass er sich in einem Tempelkeller versteckt und noch ein anderer sagt was vom Ende eines langen Flurs.",,Trovu kaj mortigu la Programiston. Estas kontraÅ­diraj informoj pri lia loko: en komputila ĉambro aÅ­ en la subtera etaÄo de templo aÅ­ ĉe la fundo de longa koridoro.,"Encuentra y elimina al Programador. Tenemos informes contradictorios sobre su ubicación: uno que está en una sala de ordenadores, otro en el sótano de un templo y otro al final de un largo pasillo.","Encuentra y elimina al Programador. Tenemos informes contradictorios sobre su ubicación: uno que está en una sala de computadoras, otro en el sótano de un templo y otro al final de un largo pasillo.","Osallistu hyökkäykseen linnaa vastaan. Etsi ja kukista Ohjelmoitsija. Meillä on hänen sijainnistaan eriäviä tietoja: Yhden mukaan hän on tietokonehuoneessa, toisen mukaan maanalaisessa temppelissä ja vielä kolmannen mukaan pitkän käytävän päässä.","Rejoins l'assaut sur le château. Trouve et élimine le Programmeur. On a des rapports en conflit sur sa position. L'un d'entre eux nous dit qu'il se trouve dans la salle des ordinateurs, l'autre nous dit qu'il se cache dans un temple en sous sol, et un autre encore au bout d'un long couloir.",,"Csatlakozz a kastély ostromához. Keresd meg és iktasd ki a Programozót. Ellentmondó információink vannak a hollétérÅ‘l. Az egyik szerint a számítógép szobában van, másik szerint egy föld alatti templomban, harmadik szerint egy hosszú folyosó végén.","Unisciti all'assalto sul castello. Trova e fai fuori il Programmatore. I rapporti che abbiamo sulla sua posizione si contraddicono. Uno lo piazza in una stanza di computer, un altro in un tempio di sottolivello, e un altro ancora alla fine di un lungo corridoio.","城ã«å‘ã‹ã†çªæ’ƒéƒ¨éšŠã«å‚加ã—ã¦ãƒ—ログラマーを探ã—出ã›ã€‚ プログラマーã®å±…場所ã«ã¤ã„ã¦çŸ›ç›¾ã™ã‚‹å ±å‘Šã‚‚ã‚る。 ã‚る人ã¯ã‚³ãƒ³ãƒ”ューター室ã«ã„ã‚‹ã€åˆ¥ã®äººã¯ä¿ç®¡å®¤ã«ã„る〠ã¾ãŸåˆ¥ã®äººã¯é•·ã„廊下ã«ã„ã‚‹ã¨ã‚‚言ã£ã¦ã„ãŸã€‚","ì„±ì„ ê³µëžµí•  ê³µì„±ì „ì— ì°¸ì—¬í•´. 침투해서 프로그래머를 ì£½ì¼ ìˆ˜ 있게. ê·¸ì˜ ìž¥ì†Œì™€ ê´€ë ¨ëœ ë³´ê³ ë“¤ì´ ë­”ê°€ 혼란스러운ë°, ì»´í“¨í„°ì‹¤ì— ìžˆë‹¤ê³  하고, ë‚®ì€ ì¸µ ì‹ ì „ì— ìˆ¨ì–´ 있다고 하고, 다른 한 ëª…ì€ ê¸´ ë³µë„ ë§¨ ëì— ì„œ 있대.","Doe mee aan de aanval op het kasteel. Zoek en schakel de Programmeur uit. We hebben tegenstrijdige berichten over zijn locatie. De ene zegt dat hij in een computerkamer zit, de andere verstopt zich in een tempel op subniveau, en nog een andere aan het einde van een lange gang.","Bli med pÃ¥ angrepet pÃ¥ slottet. Finn og drep programmereren. Vi har motstridende rapporter om hvor han befinner seg. En sier at han er i et datarom, en annen at han gjemmer seg i et underjordisk tempel, og en tredje at han er i enden av en lang gang.","Dołącz do szturmu na zamek. Znajdźcie i zlikwidujcie ProgramistÄ™. Mamy sprzeczne raporty na temat jego lokalizacji. Jeden mówi, że w sali komputerowej, inny, że ukrywa siÄ™ w podpoziomowej Å›wiÄ…tyni, a jeszcze inny, że na koÅ„cu dÅ‚ugiego korytarza.","Junte-se ao ataque no castelo. Ache e elimine o Programador. Temos relatos conflitantes sobre onde ele se encontra. Um deles diz que ele está na sala de informática, outro que ele está escondido num templo no subsolo e mais outro diz que ele está no fim de um longo corredor.",,"Alăturăte asaltului asupra castelului. GăseÈ™te-l È™i elimină-l pe Programator. Avem rapoarte diferite în ceea ce priveÈ™te locaÈ›ia lui. Unul spune că e într-o sală de calculatoare, iar altul că se ascunde într-un subnivel al templului, È™i încă unul care spune că e în capătul unui coridor lung.","ПриÑоединиÑÑŒ к атаке на замок. Ðайди и уничтожь ПрограммиÑта. Ðаши ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾ его меÑтонахождении противоречивы. Одни говорÑÑ‚, что он в компьютерной комнате, другие — что он укрываетÑÑ Ð² подземном храме, а третьи — что он в конце длинного коридора.",,"GÃ¥ med i anfallet mot slottet. Hitta och ta ut programmeraren. Vi har motstridiga rapporter om var han befinner sig. En säger att han befinner sig i ett datorrum, en annan gömmer sig i ett tempel pÃ¥ en underliggande nivÃ¥ och ytterligare en annan i slutet av en lÃ¥ng korridor.","Kaleye yapılan saldırıya katılın. Programcıyı bulup çıkarın. Yeri hakkında çeliÅŸkili raporlarımız var. Biri bilgisayar odasında, diÄŸeri alt kattaki bir tapınakta, bir baÅŸkası da uzun bir koridorun sonunda saklandığını söylüyor." @@ -12415,33 +12422,33 @@ The Loremaster's lab is beyond the teleporter that just opened. Let's go find ou フォースフィールドã¯åˆ‡ã£ãŸã€ãƒ“ショップã®å¡”ã¸å‘ã‹ãŠã†ã€‚ ビショップを殺ã—ãŸã‚‰ã‚ªãƒ©ã‚¯ãƒ«ã®å…ƒã¸æˆ»ã‚Œã€‚ ","ì´ì œ ì´ê³³ì„ 당장 떠나. ë°©ì–´ë§‰ì´ êº¼ì¡Œìœ¼ë‹ˆ! 비ìˆì˜ 성으로 ì´ë™í•´ì„œ 비ìˆì„ 죽ì´ê³ , 오ë¼í´ì„ 만나러 ê°€.","Kom op, laten we hier als de sodemieter weggaan. Het krachtveld is naar beneden, op weg naar de bisschopstoren. Als hij dood is, ga dan terug naar het Orakel.","Kom igjen, la oss komme oss vekk herfra. Kraftfeltet er nede, av gÃ¥rde til biskopens tÃ¥rn. NÃ¥r han er død, gÃ¥ tilbake til oraklet.","Chodź, wynoÅ›my siÄ™ stÄ…d. Pole siÅ‚owe wyłączone, idziemy do wieży biskupa. Kiedy bÄ™dzie martwy, wróć do Wyroczni.","Vamos dar o fora daqui. O campo de força está desligado. Vamos para a torre do Bispo. Assim que ele morrer, volte ao oráculo.",,"Hai, să ieÈ™im de aici. Câmpul de forță e la pământ, către turnul Episcopului. Odată ce e mort, întoarce-te la Oracol.","Ðу же, двигаем отÑюда. Силовое поле отключено, пора двигатьÑÑ Ðº башне ЕпиÑкопа. Когда он умрёт, возвращайÑÑ Ðº Оракулу.",,"Kom igen, vi sticker härifrÃ¥n. Kraftfältet är nere, vi gÃ¥r till biskopens torn. När han är död, Ã¥tervänder vi till Oraklet.","Hadi, buradan defolup gidelim. Güç alanı kapandı, Piskopos'un kulesine gidiyoruz. O öldüğünde, Kahin'e geri dönün." -Find the sanctuary by the river. Steal the chalice from inside. Bring it to Harris in the tavern.,TXT_ILOG1001,MAP33?,,,Najdi svatyni u Å™eky. ZevnitÅ™ ukradni kalich. PÅ™ines ho Harrisovi do taverny.,Find helligdommen ved floden. Stjæl bægeret indefra. Bring den til Harris pÃ¥ tavernaen.,"Finde das Heiligtum am Fluss. Stehle den Kelch, der dort aufbewahrt wird und bringe ihn zu Harris in der Taverne.",,"Trovu la sanktejon apud la rojo, Åtelu la kalikon kaj portu Äin al Harriso en la taverno.","Encuentra el santuario junto al arroyo, roba el cáliz de dentro y llévaselo a Harris en la taberna (Tavern).","Encuentra el santuario junto al arroyo, roba el cáliz de adentro y llévaselo a Harris a la taberna (Tavern).",Löydä pyhäkkö joen varrella. Varasta kalkki sisältä. Vie se Harrisille kapakkaan.,Trouve le sanctuaire près de la rivière. Prends le Calice qui s'y trouve et amène le à Harris dans la taverne.,,A szentély a folyó mellett van. Lopd el a serleget bentrÅ‘l. Vidd travishez a kocsmába.,Trova il santuario accanto al fiume. Ruba il calice al suo interno. Portalo ad Harris nella taverna.,"å·æ²¿ã„ã® è–域 を探ã—ã€ä¸­ã«ã‚ã‚‹è–æ¯ã‚’盗む。 +Find the sanctuary by the river. Steal the chalice from inside. Bring it to Harris in the tavern.,TXT_ILOG1001,MAP33?,,,Najdi svatyni u Å™eky. ZevnitÅ™ ukradni kalich. PÅ™ines ho Harrisovi do taverny.,Find helligdommen ved floden. Stjæl bægeret indefra. Bring den til Harris pÃ¥ tavernaen.,"Finde das Heiligtum am Fluss. Stehle den Kelch, der dort aufbewahrt wird und bringe ihn zu Harris in der Taverne.",,"Trovu la sanktejon apud la rojo, Åtelu la kalikon kaj portu Äin al Harriso en la tavern'.","Encuentra el santuario junto al arroyo, roba el cáliz de dentro y llévaselo a Harris en la taberna (Tavern).","Encuentra el santuario junto al arroyo, roba el cáliz de adentro y llévaselo a Harris a la taberna (Tavern).",Löydä pyhäkkö joen varrella. Varasta kalkki sisältä. Vie se Harrisille kapakkaan.,Trouve le sanctuaire près de la rivière. Prends le Calice qui s'y trouve et amène le à Harris dans la taverne.,,A szentély a folyó mellett van. Lopd el a serleget bentrÅ‘l. Vidd travishez a kocsmába.,Trova il santuario accanto al fiume. Ruba il calice al suo interno. Portalo ad Harris nella taverna.,"å·æ²¿ã„ã® è–域 を探ã—ã€ä¸­ã«ã‚ã‚‹è–æ¯ã‚’盗む。 é…’å ´ã«ã„ã‚‹ãƒãƒªã‚¹ã«æ¸¡ã™ã€‚",ê°• ê·¼ì²˜ì— ìžˆëŠ” 성소를 찾아서 ì•ˆì— ë³´ê´€ë˜ì–´ìžˆëŠ” 성배를 훔치고 선술집으로 향해.,Zoek het heiligdom bij de rivier. Steel de kelk van binnenuit. Breng het naar Harris in de herberg.,Finn helligdommen ved elven. Stjel begeret fra innsiden. Ta det med til Harris i vertshuset.,Znajdź sanktuarium nad rzekÄ…. Ukradnij ze Å›rodka kielich. ZanieÅ› go Harrisowi w tawernie.,Encontre o santuário perto do rio. Roube o cálice que está lá dentro. Leve-o para o Harris na taverna.,,GăseÈ™te sanctuarul de lângă râu. Fură potirul din interior. Du-l la Harris în tavernă.,Ðайди ÑвÑтилище возле реки. Проникни туда и выкради чашу. ПринеÑи её ХарриÑу в таверну.,,Hitta helgedomen vid floden. Stjäl kalken frÃ¥n insidan. Ta med den till Harris i tavernan.,Nehir kenarındaki tapınağı bulun. İçeriden kadehi çalın. Tavernadaki Harris'e götür. Find the Governor. Talk to him about your reward.,TXT_ILOG1002,MAP33?,,,Najdi guvernéra. Promluv si s ním o své odmÄ›nÄ›.,Find guvernøren. Tal med ham om din belønning.,Finde den Gouverneur. Rede mit ihm über deine Belohnung.,,Trovu la registon kaj parolu al li pri via rekompenco.,Encuentra al Gobernador y háblale de tu recompensa.,,Löydä kuvernööri. Puhu hänen kanssaan palkkiostasi.,Trouve le gouverneur et demande ta récompense.,,Keresd meg a Kormányzót. Beszélj vele a jutalomról.,Trova il governatore. Parlargli della tua ricompensa.,知事を探ã›ã€‚å½¼ã¨è©±ã—ã¦å ±é…¬ã‚’è²°ã†ã€‚,ì´ë…ì„ ë§Œë‚˜ì„œ ë³´ìƒì— 대해 예기해.,Vind de Gouverneur. Spreek met hem over je beloning.,Finn guvernøren. Snakk med ham om belønningen din.,Znajdź gubernatora. Porozmawiaj z nim o swojej nagrodzie.,Encontre o Governador. Fale com ele sobre a sua recompensa.,,GăseÈ™te Guvernatorul. VorbeÈ™te cu el despre recompensa ta.,Ðайди губернатора. ОбÑуди Ñ Ð½Ð¸Ð¼ Ñвою награду.,,Hitta guvernören. Prata med honom om din belöning.,Vali'yi bul. Onunla ödülün hakkında konuÅŸ. -"Find the sanctuary by the river. Inside someone called Beldin is being held. Shut him up, and bring his ring back to Rowan as proof.",TXT_ILOG1003,MAP02: After accepting Rowan's chore.,,,Najdi svatyni u Å™eky. UvnitÅ™ je držen jakýsi Beldin. UmlÄ ho a pÅ™ines Rowanovi jeho prsten jako důkaz.,"Find helligdommen ved floden. Indenfor bliver en person ved navn Beldin holdt fanget. FÃ¥ ham til at lukke munden pÃ¥ ham, og giv Rowan hans ring tilbage som bevis.",Finde das Heiligtum am Fluss. Dort drinnen wird jemand namens Beldin festgehalten. Bringe ihn zum Schweigen und gebe Rowan seinen Ring als Beweis.,,Trovu la sanktejon apud la rojo. Iu nomata Beldin estas retenita en Äi. Silentigu lin kaj donu lian ringon kiel pruvon al Rowan.,Encuentra el santuario junto al arroyo. Alguien llamado Beldin está retenido dentro. Siléncialo y dale su anillo a Rowan como prueba.,Encuentra el santuario junto al arroyo. Alguien llamado Beldin está retenido adentro. Siléncialo y dale su anillo a Rowan como prueba.,"Löydä pyhäkkö joen varrella. Sisällä on vangittuna joku nimeltään Beldin. Vaienna hänet, ja tuo hänen sormuksensa takaisin Rowanille todisteena.","Trouve le sanctuaire près de la rivière. Quelqu'un qui s'appelle Beldin s'y trouve emprisonné. Cloue-lui le bec, et ramène son anneau à Rowan comme preuve.",,"Megtalálod a szentélyt a folyó mellett. Egy Beldin nevű figurát tartanak fogva. Halgattasd el örökre, és hozd vissza a gyűrűjét Rowannak bizonyítékként.","Trova il santuario accanto al fiume. Dentro, qualcuno chiamato Beldin è sotto chiave. Fallo tacere per sempre, e riporta il suo anello a Rowan come prova.","å·æ²¿ã„ã® è–域 を探ã›ã€‚中ã«ã„るベルディン㨠+"Find the sanctuary by the river. Inside someone called Beldin is being held. Shut him up, and bring his ring back to Rowan as proof.",TXT_ILOG1003,MAP02: After accepting Rowan's chore.,,,Najdi svatyni u Å™eky. UvnitÅ™ je držen jakýsi Beldin. UmlÄ ho a pÅ™ines Rowanovi jeho prsten jako důkaz.,"Find helligdommen ved floden. Indenfor bliver en person ved navn Beldin holdt fanget. FÃ¥ ham til at lukke munden pÃ¥ ham, og giv Rowan hans ring tilbage som bevis.",Finde das Heiligtum am Fluss. Dort drinnen wird jemand namens Beldin festgehalten. Bringe ihn zum Schweigen und gebe Rowan seinen Ring als Beweis.,,Trovu la sanktejon apud la rojo. Iu nomata Beldin estas retenita en Äi. «Silentigu» lin kaj donu lian ringon kiel pruvon al Rowan.,Encuentra el santuario junto al arroyo. Alguien llamado Beldin está retenido dentro. «Siléncialo» y dale su anillo a Rowan como prueba.,Encuentra el santuario junto al arroyo. Alguien llamado Beldin está retenido adentro. «Siléncialo» y dale su anillo a Rowan como prueba.,"Löydä pyhäkkö joen varrella. Sisällä on vangittuna joku nimeltään Beldin. Vaienna hänet, ja tuo hänen sormuksensa takaisin Rowanille todisteena.","Trouve le sanctuaire près de la rivière. Quelqu'un qui s'appelle Beldin s'y trouve emprisonné. Cloue-lui le bec, et ramène son anneau à Rowan comme preuve.",,"Megtalálod a szentélyt a folyó mellett. Egy Beldin nevű figurát tartanak fogva. Halgattasd el örökre, és hozd vissza a gyűrűjét Rowannak bizonyítékként.","Trova il santuario accanto al fiume. Dentro, qualcuno chiamato Beldin è sotto chiave. Fallo tacere per sempre, e riporta il suo anello a Rowan come prova.","å·æ²¿ã„ã® è–域 を探ã›ã€‚中ã«ã„るベルディン㨠呼ã°ã‚Œã¦ã„ã‚‹æ•ç²ã•れãŸè€…を見ã¤ã‘彼を黙らã›ã‚‹ã€‚ æŒ‡è¼ªã‚’ãƒ­ãƒ¯ãƒ³ã«æ¸¡ã™ã€‚","ê°• ê·¼ì²˜ì— ìžˆëŠ” 성소를 찾아서 벨딘ì´ë¼ëŠ” ì‚¬ëžŒì„ ì°¾ê³ , ìž…ì„ ë‹¤ë¬¼ê²Œ í•´. ê·¸ í›„ì— ê·¸ì˜ ë°˜ì§€ë¥¼ 얻어서 로완ì—게 그를 죽였다고 ì¦ëª…í•´.","Zoek het heiligdom bij de rivier. Binnenin wordt iemand met de naam Beldin vastgehouden. Laat hem zwijgen, en breng zijn ring terug naar Rowan als bewijs.","Finn helligdommen ved elven. Der inne holdes en som heter Beldin fanget. FÃ¥ ham til Ã¥ holde kjeft, og ta med ringen hans tilbake til Rowan som bevis.",Znajdź sanktuarium nad rzekÄ…. W Å›rodku przetrzymywany jest ktoÅ› o imieniu Beldin. Ucisz go i przynieÅ› jego pierÅ›cieÅ„ Rowanowi jako dowód.,Encontre o santuário perto do rio. Dentro há alguém chamado Beldin sendo detido. Cale a boca dele e traga o seu anel de volta ao Rowan como prova.,,GăseÈ™te sanctuarul de lângă râu. ÃŽnăuntru cineva numit Beldin e reÈ›inut. Redu-l la tăcere È™i întoarce-te cu inelul la Rowan drept dovadă.,"Ðайди ÑвÑтилище возле реки. Внутри ÑодержитÑÑ Ð¿Ð»ÐµÐ½Ð½Ð¸Ðº по имени Белдин. ЗаÑтавь его замолчать, и принеÑи его кольцо РоуÑну в качеÑтве доказательÑтва.",,Hitta helgedomen vid floden. Där inne hÃ¥lls nÃ¥gon som heter Beldin fÃ¥ngen. FÃ¥ tyst pÃ¥ honom och ta med dig hans ring tillbaka till Rowan som bevis.,Nehir kenarındaki sığınağı bul. İçeride Beldin adında biri tutuluyor. Onu susturun ve yüzüğünü kanıt olarak Rowan'a geri getirin. -Find the location of the Front and talk to Macil.,TXT_ILOG1004,MAP02: After getting the com unit from Rowan.,,,Najdi umístÄ›ní Fronty a promluv si s Macilem.,Find placeringen af fronten og tal med Macil.,Finde die Basis der Front und rede mit Macil.,,,Encuentra la localización del Frente y habla con Macil.,,Löydä Rintaman olinpaikka ja keskustele Macilin kanssa.,Trouve où se cache le Front et parle à Macil.,,"Keresd meg a Front központját, és beszélj Macillal.",Trova il quartier generale del Fronte e parla a Macil.,フロントã®å ´æ‰€ã‚’見ã¤ã‘ã€ãƒžã‚·ãƒ«ã¨ä¼šè©±ã™ã‚‹ã€‚,í”„ë¡ íŠ¸ì˜ ê¸°ì§€ë¥¼ 찾아서 마실과 대화해ë´.,Zoek de locatie van het Front en praat met Macil.,Finn plasseringen til Fronten og snakk med Macil.,Znajdź lokalizacjÄ™ Frontu i porozmawiaj z Macil.,Ache a localização da Frente e fale com o Macil.,,GăseÈ™te locaÈ›ia Frontului È™i vorbeÈ™te cu Macil.,Ðайди базу Ð¡Ð¾Ð¿Ñ€Ð¾Ñ‚Ð¸Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸ поговори Ñ ÐœÑйÑилом.,,Hitta platsen för fronten och prata med Macil.,Cephe'nin yerini bulun ve Macil ile konuÅŸun. -"Go down the stairs, find and talk to Macil.",TXT_ILOG1005,MAP02?,,,Jdi dolů po schodech a najdi a promluv si s Macilem.,"GÃ¥ ned ad trappen, find og tal med Macil.",Geh die Treppe runter und rede mit Macil.,,,Baja por las escaleras. Encuentra y habla con Macil.,,"Kulje portaat alas, löydä Macil ja keskustele hänen kanssaan.","Descends les escaliers, trouve et parle à Macil.",,"Menj le a lépcsÅ‘n, és keresd meg Macilt.","Scendi le scale, e trova e parla con Macil.",階段をé™ã‚Šã€ãƒžã‚·ãƒ«ã¨ä¼šè©±ã™ã‚‹ã€‚,계단 밑으로 내려가서 마실과 대화해ë´.,"Ga de trap af, zoek en praat met Macil.","GÃ¥ ned trappene, finn og snakk med Macil.","Zejdź na dół po schodach, znajdź i porozmawiaj z Macil.","Desça as escadas, encontre e fale com o Macil.",,"Du-te în josul scărilor, găseÈ™te-l È™i vorbeÈ™te cu Macil.","СпуÑтиÑÑŒ на Ñтаж ниже, найди МÑйÑила и поговори Ñ Ð½Ð¸Ð¼.",,"GÃ¥ ner för trapporna, hitta och prata med Macil.","Merdivenlerden aÅŸağı inin, Macil'i bulun ve onunla konuÅŸun." -"Visit Irale, the Front's weapons supplier in town. He's behind the door next to the weapons shop. Then, use the key Macil gave you to talk to the Governor.",TXT_ILOG1006,MAP03: After joining the Front.,,,"NavÅ¡tiv Iraleho, dodavatele zbraní pro Frontu ve mÄ›stÄ›. Je za dveÅ™mi vedle obchodu se zbranÄ›mi. Pak použij klÃ­Ä od Macila, aby sis promluvil s guvernérem.","Besøg Irale, Frontens vÃ¥benleverandør i byen. Han befinder sig bag døren ved siden af vÃ¥benbutikken. Brug derefter den nøgle, som Macil gav dig, til at tale med guvernøren.","Suche Irale auf, den Waffenbeschaffer der Front in der Stadr. Er hält sich in dem Gebäude neben dem Waffengeschäft auf. Danach benutze den Schlüssel, den Macil dir gegeben hat, um mit dem Gouverneur zu reden",,,"Visita a Irale, el proveedor de armamento del Frente; está pasando la puerta junto a la tienda de armas (Weapons). Luego usa la llave que te dio Macil para hablar con el Gobernador.",,"Tapaa Iralea, joka on Rintaman asetoimittaja kaupungissa. Hän on asekaupan viereisen oven takana. Käytä sen jälkeen Macilin antamaa avainta puhuaksesi kuvernöörin kanssa.","Va voir Irale, le chef de l'arsenal du Front, en ville. Il se trouve derrière la porte à côté du marchand d'armes. Utilise la clé que Macil t'a donné pour parler au gouverneur.",,"Keresd fel Iralet, a Front fegyver beszállítóját a városban. A fegyverbolt melletti ajtó mögött vár rád. Utána használd a maciltól kapott kulcsot, hogy tudj beszélni a Kormányzóval.","Visita Irale, il fornitore di armi del Fronte in città. Si trova dietro la porta accanto al negozio di armi. Dopodiché, usa la chiave che Macil ti ha dato per andare a parlare al governatore.","町ã«ã„るフロントã¸ã®æ­¦å™¨ä¾›çµ¦è€…ã§ã‚るイラールã®å…ƒã‚’訪ã­ã‚‹ã€‚ +Find the location of the Front and talk to Macil.,TXT_ILOG1004,MAP02: After getting the com unit from Rowan.,,,Najdi umístÄ›ní Fronty a promluv si s Macilem.,Find placeringen af fronten og tal med Macil.,Finde die Basis der Front und rede mit Macil.,,Trovu la lokon de la Fronto kaj parolu kun Macil.,Encuentra la ubicación del Frente y habla con Macil.,,Löydä Rintaman olinpaikka ja keskustele Macilin kanssa.,Trouve où se cache le Front et parle à Macil.,,"Keresd meg a Front központját, és beszélj Macillal.",Trova il quartier generale del Fronte e parla a Macil.,フロントã®å ´æ‰€ã‚’見ã¤ã‘ã€ãƒžã‚·ãƒ«ã¨ä¼šè©±ã™ã‚‹ã€‚,í”„ë¡ íŠ¸ì˜ ê¸°ì§€ë¥¼ 찾아서 마실과 대화해ë´.,Zoek de locatie van het Front en praat met Macil.,Finn plasseringen til Fronten og snakk med Macil.,Znajdź lokalizacjÄ™ Frontu i porozmawiaj z Macil.,Ache a localização da Frente e fale com o Macil.,,GăseÈ™te locaÈ›ia Frontului È™i vorbeÈ™te cu Macil.,Ðайди базу Ð¡Ð¾Ð¿Ñ€Ð¾Ñ‚Ð¸Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸ поговори Ñ ÐœÑйÑилом.,,Hitta platsen för fronten och prata med Macil.,Cephe'nin yerini bulun ve Macil ile konuÅŸun. +"Go down the stairs, find and talk to Macil.",TXT_ILOG1005,MAP02?,,,Jdi dolů po schodech a najdi a promluv si s Macilem.,"GÃ¥ ned ad trappen, find og tal med Macil.",Geh die Treppe runter und rede mit Macil.,,Iru malsupren laÅ­ la Åtuparo. Trovu Macil-on kaj parolu kun li.,Baja por las escaleras. Encuentra a Macil y habla con él.,,"Kulje portaat alas, löydä Macil ja keskustele hänen kanssaan.","Descends les escaliers, trouve et parle à Macil.",,"Menj le a lépcsÅ‘n, és keresd meg Macilt.","Scendi le scale, e trova e parla con Macil.",階段をé™ã‚Šã€ãƒžã‚·ãƒ«ã¨ä¼šè©±ã™ã‚‹ã€‚,계단 밑으로 내려가서 마실과 대화해ë´.,"Ga de trap af, zoek en praat met Macil.","GÃ¥ ned trappene, finn og snakk med Macil.","Zejdź na dół po schodach, znajdź i porozmawiaj z Macil.","Desça as escadas, encontre e fale com o Macil.",,"Du-te în josul scărilor, găseÈ™te-l È™i vorbeÈ™te cu Macil.","СпуÑтиÑÑŒ на Ñтаж ниже, найди МÑйÑила и поговори Ñ Ð½Ð¸Ð¼.",,"GÃ¥ ner för trapporna, hitta och prata med Macil.","Merdivenlerden aÅŸağı inin, Macil'i bulun ve onunla konuÅŸun." +"Visit Irale, the Front's weapons supplier in town. He's behind the door next to the weapons shop. Then, use the key Macil gave you to talk to the Governor.",TXT_ILOG1006,MAP03: After joining the Front.,,,"NavÅ¡tiv Iraleho, dodavatele zbraní pro Frontu ve mÄ›stÄ›. Je za dveÅ™mi vedle obchodu se zbranÄ›mi. Pak použij klÃ­Ä od Macila, aby sis promluvil s guvernérem.","Besøg Irale, Frontens vÃ¥benleverandør i byen. Han befinder sig bag døren ved siden af vÃ¥benbutikken. Brug derefter den nøgle, som Macil gav dig, til at tale med guvernøren.","Suche Irale auf, den Waffenbeschaffer der Front in der Stadr. Er hält sich in dem Gebäude neben dem Waffengeschäft auf. Danach benutze den Schlüssel, den Macil dir gegeben hat, um mit dem Gouverneur zu reden",,"Iru ĉe Iralo la armil-provizanto de la Fronto; li estas post la pordo apud la armil-vendejo (Weapons). Tiam uzu la Ålosilon, kiun Macil donis al vi, por paroli kun la registo.",Ve con Irale el proveedor de armas del Frente; está pasando la puerta junto a la tienda de armas (Weapons). Luego usa la llave que te dio Macil para hablar con el gobernador.,,"Tapaa Iralea, joka on Rintaman asetoimittaja kaupungissa. Hän on asekaupan viereisen oven takana. Käytä sen jälkeen Macilin antamaa avainta puhuaksesi kuvernöörin kanssa.","Va voir Irale, le chef de l'arsenal du Front, en ville. Il se trouve derrière la porte à côté du marchand d'armes. Utilise la clé que Macil t'a donné pour parler au gouverneur.",,"Keresd fel Iralet, a Front fegyver beszállítóját a városban. A fegyverbolt melletti ajtó mögött vár rád. Utána használd a maciltól kapott kulcsot, hogy tudj beszélni a Kormányzóval.","Visita Irale, il fornitore di armi del Fronte in città. Si trova dietro la porta accanto al negozio di armi. Dopodiché, usa la chiave che Macil ti ha dato per andare a parlare al governatore.","町ã«ã„るフロントã¸ã®æ­¦å™¨ä¾›çµ¦è€…ã§ã‚るイラールã®å…ƒã‚’訪ã­ã‚‹ã€‚ å½¼ã¯æ­¦å™¨å±‹ã®éš£ã®ãƒ‰ã‚¢ã«å±…る。ãれã§ã€çŸ¥äº‹ã¨ä¼šã†ç‚ºã«ãƒžã‚·ãƒ«ã‹ã‚‰è²°ã£ãŸã‚­ãƒ¼ã‚’使ã†ã€‚","í”„ë¡ íŠ¸ì˜ ë¬´ê¸°ìƒì¸ ì´ë¡¤ë¦¬ë¥¼ 마ì„ì—서 찾아. 무기 ìƒì  ê·¼ì²˜ì— ë¬¸ì´ ìžˆì„ ê±°ì•¼. ë§ˆì‹¤ì´ ì¤€ 열쇠로 ë¬¸ì„ ì—´ê³  방문한 ë’¤, ì´ë…ê³¼ 대화해.","Bezoek Irale, de wapenleverancier van het Front in de stad. Hij zit achter de deur naast de wapenwinkel. Gebruik dan de sleutel die Macil je gaf om met de Gouverneur te praten.","Besøk Irale, Frontens vÃ¥penleverandør i byen. Han er bak døren ved siden av vÃ¥penbutikken. Bruk nøkkelen Macil ga deg til Ã¥ snakke med guvernøren.","Odwiedź w mieÅ›cie Irale, dostawcÄ™ broni dla Frontu. Jest on za drzwiami obok sklepu z broniÄ…. NastÄ™pnie użyj klucza, który daÅ‚ ci Macil, by porozmawiać z gubernatorem.","Visite o Irale, o fornecedor de armas da Frente. Ele está atrás da porta perto da loja de armas. Depois, use a chave que o Macil te deu para falar com o Governador.",,"Vizitează pe Irale, distribuitorul de arme alFrontului în oraÈ™. E dincolo de uÈ™a de lângă magazinul de arme. Apoi, foloseÈ™te cheia pe care Macil È›i-a dat-o È™i vorbeÈ™te cu Guvernatorul.","ПоÑети ИрÑйла, поÑтавщика Ð¾Ñ€ÑƒÐ¶Ð¸Ñ Ð¡Ð¾Ð¿Ñ€Ð¾Ñ‚Ð¸Ð²Ð»ÐµÐ½Ð¸Ñ Ð² городе. Он за Ñледующей поÑле оружейного магазина дверью. Затем, воÑпользуйÑÑ ÐºÐ»ÑŽÑ‡Ð¾Ð¼, который тебе дал МÑйÑил, чтобы поговорить Ñ Ð³ÑƒÐ±ÐµÑ€Ð½Ð°Ñ‚Ð¾Ñ€Ð¾Ð¼.",,"Besök Irale, frontens vapenleverantör i staden. Han befinner sig bakom dörren bredvid vapenbutiken. Använd sedan nyckeln som Macil gav dig för att prata med guvernören.","Kasabada Cephe'nin silah tedarikçisi İrale'yi ziyaret edin. Silah dükkanının yanındaki kapının arkasında. Sonra, Vali ile konuÅŸmak için Macil'in size verdiÄŸi anahtarı kullanın." -"Find the power tap on the mains, and shut it off. Bring something back to the Governor as proof.",TXT_ILOG1007,"MAP02: After accepting ""messy"" chore.",,,Najdi stáÄedlo energie na síti a vypni jej. PÅ™ines guvernérovi nÄ›co zpÄ›t jako důkaz.,"Find elhanen pÃ¥ hovedledningen, og luk den. Tag noget med tilbage til guvernøren som bevis.",Finde die Energieanzapfung und schalte sie aus. Bringe dem Gouverneur irgend etwas als Beweis.,,,Encuentra la toma en la red eléctrica y destrúyela. Tráele algo al Gobernador como prueba.,,Etsi verkkovirran piilokytkentä ja katkaise se. Tuo kuvernöörille jotain todisteeksi.,Trouve la connection pirate sur le transformateur et enlève la. Amène quelque chose au gouverneur comme preuve.,,"Keresd meg a fÅ‘vonalon az áramkapcsolót, és kapcsold ki. Hozz vissza valamit a kormányzótól bizonyítékként.","Trova il marchingegno che sta venendo usato per rubare energia alla rete, e distruggilo. Porta dei resti al governatore come prova.","電力盗用ã®å ´æ‰€ã‚’見ã¤ã‘ã€ãれを止ã‚る。 +"Find the power tap on the mains, and shut it off. Bring something back to the Governor as proof.",TXT_ILOG1007,"MAP02: After accepting ""messy"" chore.",,,Najdi stáÄedlo energie na síti a vypni jej. PÅ™ines guvernérovi nÄ›co zpÄ›t jako důkaz.,"Find elhanen pÃ¥ hovedledningen, og luk den. Tag noget med tilbage til guvernøren som bevis.",Finde die Energieanzapfung und schalte sie aus. Bringe dem Gouverneur irgend etwas als Beweis.,,"Trovu la konektilon en la elektriza sistemo, detruu Äin kaj portu pruvon al la registo.","Encuentra la toma en la red eléctrica, destrúyela y tráele algo al gobernador como prueba.",,Etsi verkkovirran piilokytkentä ja katkaise se. Tuo kuvernöörille jotain todisteeksi.,Trouve la connection pirate sur le transformateur et enlève la. Amène quelque chose au gouverneur comme preuve.,,"Keresd meg a fÅ‘vonalon az áramkapcsolót, és kapcsold ki. Hozz vissza valamit a kormányzótól bizonyítékként.","Trova il marchingegno che sta venendo usato per rubare energia alla rete, e distruggilo. Porta dei resti al governatore come prova.","電力盗用ã®å ´æ‰€ã‚’見ã¤ã‘ã€ãれを止ã‚る。 çŸ¥äº‹ã«æ¸¡ã™è¨¼æ‹ ã‚‚å¿…è¦ã ã€‚",주 ë™ë ¥ì„  ì–´ë”˜ê°€ì— ìžˆëŠ” 추출기를 처리해. ê·¸ 잔해를 ì´ë…ì—게 들고 가서 ì¦ëª…í•´.,"Zoek het kraantje op het lichtnet, en zet het uit. Breng iets terug naar de Gouverneur als bewijs.","Finn strømbryteren pÃ¥ strømnettet, og slÃ¥ den av. Ta med noe tilbake til guvernøren som bevis.",Znajdź kurek z prÄ…dem w sieci i wyłącz go. PrzynieÅ› coÅ› gubernatorowi jako dowód.,Encontre a ligação clandestina no transformador e desligue-a. Traga algo de volta para o Governador como prova.,,GăseÈ™te cuplul de putere È™i opreÈ™te-l. Adu ceva drept dovadă Guvernatorului.,Ðайди нелегальное подключение к ÑнергоÑети и выведи его из ÑтроÑ. ПринеÑи что-нибудь губернатору в качеÑтве доказательÑтва.,,Leta reda pÃ¥ strömkranen pÃ¥ elnätet och stäng av den. Ta med dig nÃ¥got tillbaka till guvernören som bevis.,Åžebekedeki güç musluÄŸunu bulun ve kapatın. Kanıt olarak Vali'ye bir ÅŸey getirin. -"Find Derwin in the warehouse of the power station. Kill him, and bring Mourel his ear.",TXT_ILOG1008,"MAP02: After accepting ""bloody"" chore.",,,Najdi Derwina ve skladu elektrárny. Zabij ho a pÅ™ines Mourelovi jeho ucho.,"Find Derwin i lageret pÃ¥ kraftværket. Dræb ham, og giv Mourel hans øre.",Finde Derwin im Lagerhaus des Kraftwerks. Töte ihn und bringe Mourel sein Ohr.,,,Encuentra a Derwin en el almacén (Warehouse) de la central eléctrica (Power Station). Mátalo y llévale su oreja a Mourel.,,"Etsi Derwin voimalaitoksen varastolta. Tapa hänet, ja tuo Mourelille hänen korvansa.","Trouve Derwin dans l'entrepôt de la centrale électrique. Tue-le, et amène son oreille à Mourel.",,"Keresd meg Derwint az erÅ‘mű raktárjában. öld meg, és vidd vissza Mourelnek a fülét.","Trova Derwin nel magazzino della centrale energetica. Uccidilo, e porta il suo orecchio a Mourel.","発電所ã®å€‰åº«ã«ã„るダーウィンを見ã¤ã‘る。 +"Find Derwin in the warehouse of the power station. Kill him, and bring Mourel his ear.",TXT_ILOG1008,"MAP02: After accepting ""bloody"" chore.",,,Najdi Derwina ve skladu elektrárny. Zabij ho a pÅ™ines Mourelovi jeho ucho.,"Find Derwin i lageret pÃ¥ kraftværket. Dræb ham, og giv Mourel hans øre.",Finde Derwin im Lagerhaus des Kraftwerks. Töte ihn und bringe Mourel sein Ohr.,,Trovu Derwin-on en la magazeno (Warehouse) de la centralo (Power Station). Mortigu lin kaj portu lian orelon al Mourel.,Encuentra a Derwin en el almacén (Warehouse) de la central eléctrica (Power Station). Mátalo y llévale su oreja a Mourel.,,"Etsi Derwin voimalaitoksen varastolta. Tapa hänet, ja tuo Mourelille hänen korvansa.","Trouve Derwin dans l'entrepôt de la centrale électrique. Tue-le, et amène son oreille à Mourel.",,"Keresd meg Derwint az erÅ‘mű raktárjában. öld meg, és vidd vissza Mourelnek a fülét.","Trova Derwin nel magazzino della centrale energetica. Uccidilo, e porta il suo orecchio a Mourel.","発電所ã®å€‰åº«ã«ã„るダーウィンを見ã¤ã‘る。 始末ã—ãŸã‚‰ã€ãã„ã¤ã®è€³ã‚’ãƒ¢ãƒ¼ãƒ¬ãƒ«ã«æ¸¡ã™ã€‚",발전소 창고 ì£¼ë³€ì— ìžˆëŠ” ë”ìœˆì„ ì°¾ì•„. 그를 ì£½ì¸ ë’¤ 귀를 뜯어서 ëª¨ë  ì´ë…ì—게 건네줘.,"Vind Derwin in het magazijn van de centrale. Dood hem, en breng Mourel zijn oor.","Finn Derwin i lageret pÃ¥ kraftstasjonen. Drep ham, og gi Mourel øret hans.",Znajdź Derwina w magazynie elektrowni. Zabij go i przynieÅ› Mourelowi jego ucho.,Encontre o Derwin no depósito da usina elétrica. Mate-o e leve a sua orelha para o Mourel.,,GăseÈ™te-l pe Derwin în depozit. Omoară-l È™i adu-i urechea lui Mourel.,Ðайди Дервина на Ñкладе ÑлектроÑтанции. Убей его и принеÑи Морелу его ухо.,,Hitta Derwin i lagret i kraftverket. Döda honom och ge Mourel hans öra.,Elektrik santralinin deposunda Derwin'i bulun. Onu öldürün ve Mourel'e kulağını getirin. -"Use the pass Mourel gave you to get into the prison. Once inside, talk to Warden Montag. Find a way to free the prisoners.",TXT_ILOG1009,MAP03: After completing Mourel's chore.,,,"Použij propustku od Mourela pro vstup do vÄ›zení. UvnitÅ™ si promluv s dozorÄím Montagem. Najdi způsob, jak vysvobodit vÄ›znÄ›.","Brug det adgangskort, som Mourel gav dig, til at komme ind i fængslet. NÃ¥r du er inde, skal du tale med direktør Montag. Find en mÃ¥de at befri fangerne pÃ¥.","Benutze den Pass, den Mourel dir gegeben hat um in das Gefängnis hereinzukommen. Wenn du drin bist, rede mit Direktor Montag. Finde einen Weg um die Gefangenen zu befreien.",,,"Usa el pase que Mourel te dió para entrar en la prisión. Una vez dentro, habla con el Carcelero Montag. Encuentra una forma de liberar a los prisioneros.",,Käytä Mourelin antamaa pääsylupaa päästäksesi vankilaan. Päästyäsi sisälle puhu vankilanjohtaja Montagin kanssa. Keksi keino vapauttaa vangit.,"Utilise le passe que Mourel t'a donné pour entrer dans la prison. Une fois à l'intérieur, parle au gardien Montag. Trouve un moyen de libérer les prisonniers.",,"Használd a Mourel által adott belépÅ‘t, hogy bejuss a börtönbe. Ha bent vagy, beszélj Montag börtön igazgatóval. Találd meg a módját, hogy kiszabadítsd a rabokat.","Usa il tesserino che Mourel ti ha dato per entrare nella prigione. Una volta dentro, parla con il direttore Montag. Trova un modo per liberare i prigionieri.","モーレルã‹ã‚‰ã‚‚らã£ãŸåˆ‘務所ã®è¨±å¯è¨¼ã‚’使ã£ã¦å…¥ã‚‹ã€‚ +"Use the pass Mourel gave you to get into the prison. Once inside, talk to Warden Montag. Find a way to free the prisoners.",TXT_ILOG1009,MAP03: After completing Mourel's chore.,,,"Použij propustku od Mourela pro vstup do vÄ›zení. UvnitÅ™ si promluv s dozorÄím Montagem. Najdi způsob, jak vysvobodit vÄ›znÄ›.","Brug det adgangskort, som Mourel gav dig, til at komme ind i fængslet. NÃ¥r du er inde, skal du tale med direktør Montag. Find en mÃ¥de at befri fangerne pÃ¥.","Benutze den Pass, den Mourel dir gegeben hat um in das Gefängnis hereinzukommen. Wenn du drin bist, rede mit Direktor Montag. Finde einen Weg um die Gefangenen zu befreien.",,"Uzu la ernipermeson de Mourel por iri en la malliberejon (Prison) de Norwall. Enirinte, parolu al la provoso Montag. Trovu manieron liberigi la kaptitojn.","Usa el pase que te dio Mourel para entrar en la prisión de Norwall. Una vez dentro, habla con el carcelero Montag. Encuentra una forma de liberar a los prisioneros.","Usa el pase que te dio Mourel para entrar a la prisión de Norwall. Una vez adentro, habla con el carcelero Montag. Encuentra una forma de liberar a los prisioneros.",Käytä Mourelin antamaa pääsylupaa päästäksesi vankilaan. Päästyäsi sisälle puhu vankilanjohtaja Montagin kanssa. Keksi keino vapauttaa vangit.,"Utilise le passe que Mourel t'a donné pour entrer dans la prison. Une fois à l'intérieur, parle au gardien Montag. Trouve un moyen de libérer les prisonniers.",,"Használd a Mourel által adott belépÅ‘t, hogy bejuss a börtönbe. Ha bent vagy, beszélj Montag börtön igazgatóval. Találd meg a módját, hogy kiszabadítsd a rabokat.","Usa il tesserino che Mourel ti ha dato per entrare nella prigione. Una volta dentro, parla con il direttore Montag. Trova un modo per liberare i prigionieri.","モーレルã‹ã‚‰ã‚‚らã£ãŸåˆ‘務所ã®è¨±å¯è¨¼ã‚’使ã£ã¦å…¥ã‚‹ã€‚ å…¥ã£ãŸã‚‰ãƒ¢ãƒ³ã‚¿ãƒ¼ã‚°ã¨è©±ã—ã€å›šäººã‚’解放ã™ã‚‹æ–¹æ³•を探ã™ã€‚ ",ì´ë…ì´ ì¤€ ê°ì˜¥ 통행ì¦ì„ ì´ìš©í•´ì„œ ê°ì˜¥ìœ¼ë¡œ 들어가. 들어간 í›„ì— ìˆ˜ê°ìžë“¤ì„ 해방하기 위해 몬탕 간수장과 대화해. ,"Gebruik de pas die Mourel je gaf om in de gevangenis te komen. Eenmaal binnen, praat je met Warden Montag. Zoek een manier om de gevangenen te bevrijden.","Bruk passet Mourel ga deg for Ã¥ komme inn i fengselet. NÃ¥r du er inne, snakk med fengselsdirektør Montag. Finn en mÃ¥te Ã¥ befri fangene pÃ¥.","Użyj przepustki, którÄ… daÅ‚ ci Mourel, aby dostać siÄ™ do wiÄ™zienia. Po wejÅ›ciu do Å›rodka porozmawiaj z naczelnikiem Montagiem. Znajdź sposób na uwolnienie więźniów.","Use o passe que o Mourel te deu para entrar na prisão. Após entrar, fale com o Carcereiro Montag. Encontre uma maneira de libertar os prisioneiros.",,"FoloseÈ™te parola pe care È›i-a dat-o Mourel pentru a intra în închisoare. Odată înăuntru, vorbeÈ™te cu Directorul Montag. GăseÈ™te o cale să eliberezi prizonierii.","ИÑпользуй пропуÑк, полученный у Морела, чтобы пройти в тюрьму. Когда ты будешь внутри, поговори Ñ Ñ‚ÑŽÑ€ÐµÐ¼Ñ‰Ð¸ÐºÐ¾Ð¼ Монтагом. Ðайди ÑпоÑоб оÑвободить пленников.",,Använd passet som Mourel gav dig för att komma in i fängelset. När du väl är inne pratar du med fängelsedirektör Montag. Hitta ett sätt att befria fÃ¥ngarna.,Hapishaneye girmek için Mourel'in size verdiÄŸi kartı kullanın. İçeri girdikten sonra Müdür Montag ile konuÅŸun. Mahkûmları serbest bırakmanın bir yolunu bulun. -Use the Warden's key to get into the prison cell blocks and find a way to free the prisoners.,TXT_ILOG1010,MAP05,,,"Použij dozorÄího klÃ­Ä k dostání se do vÄ›zeňského bloku s celami a najdi způsob, jak vysvobodit vÄ›znÄ›.",Brug fængselsdirektørens nøgle til at komme ind i fængselscelleblokkene og find en mÃ¥de at befri fangerne pÃ¥.,"Benutze den Schlüssel des Direktors um in die Zellenblöcke zu kommen und finde heraus, wie man die Gefangenen befreien kann.",,,Usa la llave del Carcelero para entrar en los bloques de celdas y encuentra una forma de liberar a los prisioneros.,Usa la llave del Carcelero para entrar a los bloques de celdas y encuentra una forma de liberar a los prisioneros.,Käytä vankilanjohtajan avainta päästäksesi vankilan selliosastoille ja keksi keino vapauttaa vangit.,Utilisé la clé du Gardien pour entrer dans les blocs de cellules et trouve un moyen de libérer les prisonners.,,"Használd a börtön igazgató kulcsát, hogy bejuss a cella blokkba, és szabadítsd ki a foglyokat.",Usa la chiave del direttore per entrare nella sezione delle celle e trovare un modo per liberare i prigionieri.,ワーデンã‹ã‚‰éµã‚’奪ã£ã¦å†…部ã«å…¥ã‚Œã€‚,"ê°„ìˆ˜ìž¥ì˜ ì—´ì‡ ë¥¼ ì¨ì„œ ê°ì˜¥ 안으로 진입하고, 수ê°ìžë“¤ì„ 풀어줄 ë°©ë²•ì„ ì°¾ì•„ë´.",Gebruik de sleutel van de bewaker om in de celblokken van de gevangenis te komen en een manier te vinden om de gevangenen te bevrijden.,Bruk fengselsdirektørens nøkkel for Ã¥ komme deg inn i fengselsblokkene og finn en mÃ¥te Ã¥ befri fangene pÃ¥.,"Użyj klucza naczelnika, aby dostać siÄ™ do bloków wiÄ™ziennych i znajdź sposób na uwolnienie więźniów.",Use a chave do Carcereiro para entrar nos blocos de celas e ache uma maneira de libertar os prisioneiros.,,FoloseÈ™te cheia pe care È›i-a dat-o Guvernatorul È™i găseÈ™te o cale să eliberezi prizonierii.,"ИÑпользуй ключ тюремщика, чтобы пройти к камерам, и найди ÑпоÑоб оÑвободить пленников.",,Använd fängelsedirektörens nyckel för att ta dig in i fängelsets cellblock och hitta ett sätt att befria fÃ¥ngarna.,Hapishane hücre bloklarına girmek için Müdürün anahtarını kullanın ve mahkûmları serbest bırakmanın bir yolunu bulun. -"Destroy the power crystal that runs the power grid which drives the Order's shields. Go visit Worner, a spy we recruited in the warehouse of the power station. Don't forget to visit the medic and the weapons trainer before you go.",TXT_ILOG1011,MAP03: After accepting Macil's second mission.,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi navÅ¡tívit Wornera, Å¡pióna, kterého jsme najali ve skladu elektrárny. Nezapomeň navÅ¡tívit zdravotníka a uÄitele stÅ™elby, než půjdeÅ¡.","Ødelæg energikrystallen, der driver det strømnet, som driver Ordenens skjolde. Besøg Worner, en spion, som vi rekrutterede i lageret pÃ¥ kraftværket. Glem ikke at besøge lægen og vÃ¥bentræneren, før du gÃ¥r.","Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Suche Worner auf, einen Spion, den wir im Lagerhaus des Kraftwerks rekrutiert haben. Vergiß nicht, den Sanitäter und den Waffentrainer aufzusuchen, bevor du gehst.",,,"Destruye el cristal de poder que impulsa la red eléctrica que alimenta los escudos de La Orden. Busca a Worner, un espía que reclutamos en el almacén de la central eléctrica. Recuerda visitar al médico y al entrenador de armas.",,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Mene tapaamaan Worneria, vakoojaa, jonka värväsimme voimalaitoksen varastolta. Älä unohda vierailla lääkintämiehen ja asekouluttajan luona, ennen kuin lähdet.","Détruis le cristal qui alimente la grille énergétique des boucliers de l'Ordre. Va voir Worner, un espion que nous avons recruté dans l'entrepôt de la centrale éléctrique. N'oublie pas d'aller voir le maître d'armes et le médecin avant d'y aller.",,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Keresd fel az erÅ‘műtÅ‘l betoborzott kémünket, Wornert. Ne felejtsd el meglátogatni a szanitécet és a fegyvermestert mielÅ‘tt nekiindulsz.","Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Trova Worner, una spia che abbiamo reclutato nel magazzino della centrale energetica. Non scordarti di passare dal medico e dall'addestratore di armi prima di andare.","é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 +Use the Warden's key to get into the prison cell blocks and find a way to free the prisoners.,TXT_ILOG1010,MAP05,,,"Použij dozorÄího klÃ­Ä k dostání se do vÄ›zeňského bloku s celami a najdi způsob, jak vysvobodit vÄ›znÄ›.",Brug fængselsdirektørens nøgle til at komme ind i fængselscelleblokkene og find en mÃ¥de at befri fangerne pÃ¥.,"Benutze den Schlüssel des Direktors um in die Zellenblöcke zu kommen und finde heraus, wie man die Gefangenen befreien kann.",,Uzu la Ålosilon de la provoso por atingi la ĉelaron kaj trovu manieron liberigi la kaptitojn.,Usa la llave del carcelero para llegar a la galería y encuentra una forma de liberar a los prisioneros.,,Käytä vankilanjohtajan avainta päästäksesi vankilan selliosastoille ja keksi keino vapauttaa vangit.,Utilisé la clé du Gardien pour entrer dans les blocs de cellules et trouve un moyen de libérer les prisonners.,,"Használd a börtön igazgató kulcsát, hogy bejuss a cella blokkba, és szabadítsd ki a foglyokat.",Usa la chiave del direttore per entrare nella sezione delle celle e trovare un modo per liberare i prigionieri.,ワーデンã‹ã‚‰éµã‚’奪ã£ã¦å†…部ã«å…¥ã‚Œã€‚,"ê°„ìˆ˜ìž¥ì˜ ì—´ì‡ ë¥¼ ì¨ì„œ ê°ì˜¥ 안으로 진입하고, 수ê°ìžë“¤ì„ 풀어줄 ë°©ë²•ì„ ì°¾ì•„ë´.",Gebruik de sleutel van de bewaker om in de celblokken van de gevangenis te komen en een manier te vinden om de gevangenen te bevrijden.,Bruk fengselsdirektørens nøkkel for Ã¥ komme deg inn i fengselsblokkene og finn en mÃ¥te Ã¥ befri fangene pÃ¥.,"Użyj klucza naczelnika, aby dostać siÄ™ do bloków wiÄ™ziennych i znajdź sposób na uwolnienie więźniów.",Use a chave do Carcereiro para entrar nos blocos de celas e ache uma maneira de libertar os prisioneiros.,,FoloseÈ™te cheia pe care È›i-a dat-o Guvernatorul È™i găseÈ™te o cale să eliberezi prizonierii.,"ИÑпользуй ключ тюремщика, чтобы пройти к камерам, и найди ÑпоÑоб оÑвободить пленников.",,Använd fängelsedirektörens nyckel för att ta dig in i fängelsets cellblock och hitta ett sätt att befria fÃ¥ngarna.,Hapishane hücre bloklarına girmek için Müdürün anahtarını kullanın ve mahkûmları serbest bırakmanın bir yolunu bulun. +"Destroy the power crystal that runs the power grid which drives the Order's shields. Go visit Worner, a spy we recruited in the warehouse of the power station. Don't forget to visit the medic and the weapons trainer before you go.",TXT_ILOG1011,MAP03: After accepting Macil's second mission.,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi navÅ¡tívit Wornera, Å¡pióna, kterého jsme najali ve skladu elektrárny. Nezapomeň navÅ¡tívit zdravotníka a uÄitele stÅ™elby, než půjdeÅ¡.","Ødelæg energikrystallen, der driver det strømnet, som driver Ordenens skjolde. Besøg Worner, en spion, som vi rekrutterede i lageret pÃ¥ kraftværket. Glem ikke at besøge lægen og vÃ¥bentræneren, før du gÃ¥r.","Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Suche Worner auf, einen Spion, den wir im Lagerhaus des Kraftwerks rekrutiert haben. Vergiß nicht, den Sanitäter und den Waffentrainer aufzusuchen, bevor du gehst.",,"Parolu kun la kuracisto kaj la armila trejnisto. Renkontu nian spionon Worner en la magazeno (Warehouse) de la centralo (Power Station). Detruu la energian kristalon, kiu impulsas la elektrizan sistemon, kiu tenas la Åildon de la kastelo.","Ve con el médico y el entrenador de armas. Busca a Worner, un espía nuestro, en el almacén (Warehouse) de la central eléctrica (Power Station). Destruye el cristal de poder que impulsa la red eléctrica que mantiene los escudos del castillo.",,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Mene tapaamaan Worneria, vakoojaa, jonka värväsimme voimalaitoksen varastolta. Älä unohda vierailla lääkintämiehen ja asekouluttajan luona, ennen kuin lähdet.","Détruis le cristal qui alimente la grille énergétique des boucliers de l'Ordre. Va voir Worner, un espion que nous avons recruté dans l'entrepôt de la centrale éléctrique. N'oublie pas d'aller voir le maître d'armes et le médecin avant d'y aller.",,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Keresd fel az erÅ‘műtÅ‘l betoborzott kémünket, Wornert. Ne felejtsd el meglátogatni a szanitécet és a fegyvermestert mielÅ‘tt nekiindulsz.","Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Trova Worner, una spia che abbiamo reclutato nel magazzino della centrale energetica. Non scordarti di passare dal medico e dall'addestratore di armi prima di andare.","é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 発電所ã®å€‰åº«ã«æ½œã‚‰ã›ãŸã‚¹ãƒ‘イã€ãƒ¯ãƒ¼ãƒŠãƒ¼ã‚’訪ã­ã‚‹ã€‚ ãã®å‰ã«ãƒ¡ãƒ‡ã‚£ãƒƒã‚¯ã¨æ­¦å™¨ãƒˆãƒ¬ãƒ¼ãƒŠãƒ¼ã«ä¼šã†ã®ã‚’忘れるãªã€‚","오ë”ì˜ ë°©ì–´ë§‰ì„ ìœ ì§€í•˜ëŠ” ë™ë ¥ ë§ì˜ ì „ë ¥ì›ì¸ 수정체를 파괴하ìž. ìš°ì„ , 발전소 ì°½ê³ ì— ìžˆëŠ” 우리 쪽 첩ìžì¸ 워너ì—게 찾아가 ë³´ìž. 가기 ì „ì— ì˜ë¬´ê´€ì´ëž‘ 무기 ë‹´ë‹¹ê´€ì„ ë§Œë‚˜ëŠ” ê²ƒë„ ìžŠì§€ ë§ê³ !","Vernietig het energiekristal dat het stroomnet dat de schilden van de Orde aandrijft. Ga naar Worner, een spion die we in het magazijn van de centrale hebben gerekruteerd. Vergeet niet om de dokter en de wapentrainer te bezoeken voor je vertrekt.","Ødelegg kraftkrystallen som driver kraftnettet som driver ordenens skjold. Besøk Worner, en spion vi rekrutterte i lageret til kraftstasjonen. Ikke glem Ã¥ besøke medisineren og vÃ¥pentreneren før du gÃ¥r.","Zniszcz krysztaÅ‚ mocy, który zasila sieć energetycznÄ… napÄ™dzajÄ…cÄ… tarcze Zakonu. Idź odwiedzić Wornera, szpiega, którego zwerbowaliÅ›my w magazynie elektrowni. Nie zapomnij przed wyjÅ›ciem odwiedzić medyka i trenera broni.","Destrua o cristal de energia que alimenta a rede elétrica por trás dos escudos da Ordem. Visite o Worner, um espião que recrutamos no depósito da usina de energia. Não se esqueça de visitar o médico e o treinador de armas antes de ir.",,"Distruge cristalul de nergie care alimentează câmpurile Ordinului. Vizitează pe Worner, un spion recrutat în depozitul staÈ›iei. Nu uita să vorbeÈ™ti cu medicul È™i antrenorul de arme înainte.","Уничтожь криÑталл, питающий ÑнергоÑеть, от которой работают щиты Ордена. Поговори Ñ Ð£Ð¾Ñ€Ð½Ñром, шпионом СопротивлениÑ, на Ñкладе ÑлектроÑтанции. Ðе забудь перед уходом поÑетить медика и инÑтруктора по Ñтрельбе.",,"Förstör kraftkristallen som driver kraftnätet som driver ordens sköldar. GÃ¥ och besök Worner, en spion som vi rekryterade i kraftverkets lager. Glöm inte att besöka läkaren och vapentränaren innan du gÃ¥r.",Tarikat'ın kalkanlarını çalıştıran güç ÅŸebekesini çalıştıran güç kristalini yok edin. Güç istasyonunun deposunda iÅŸe aldığımız casus Worner'ı ziyaret edin. Gitmeden önce sıhhiyeciyi ve silah eÄŸitmenini ziyaret etmeyi unutmayın. -Destroy the power crystal that runs the power grid which drives the Order's shields. Use the I.D. to get into the power station. You may want to check out the storeroom above Worner.,TXT_ILOG1012,MAP04: After talking to Worner.,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Použij získanou legitimaci pro přístup do elektrárny. Možná by stálo za to podívat se do skladiÅ¡tní místnosti nad Wornerem.","Ødelæg energikrystallen, der driver det kraftnet, som driver Ordenens skjolde. Brug identifikationskortet til at komme ind i kraftværket. Du kan tjekke lagerrummet over Worner.",Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Benutze die Identitätskarte. um in das Kraftwerk zu gelangen. Du solltest den Abstellraum beim Lagerhaus mal inspizieren.,,,Destruye el cristal de poder que impulsa la red eléctrica que alimenta los escudos de La Orden. Usa la identificación para entrar a la central eléctrica. Quizás quieras revisar el cuarto de almacenamiento encima de Worner.,,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Käytä henkilötunnistetta päästäksesi voimalaitokselle. Saatat haluta vilkaista varastohuonetta Wornerin yläpuolella.",Détruis le cristal qui alimente la grille énergétique des boucliers de l'Ordre. Utilise la carte d'identité pour entrer dans la centrale. Il faudrait que tu aille voir la salle de stockage au dessus de Worner.,,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Használd az igazolványt, hogy bejuss az erÅ‘műbe. Jobban jársz, ha benézel a Worner fölötti tárolószobába.",Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Usa il tesserino d'identificazione per entrare nella centrale energetica. Ti conviene controllare il deposito che si trova sopra Worner.,"é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 +Destroy the power crystal that runs the power grid which drives the Order's shields. Use the I.D. to get into the power station. You may want to check out the storeroom above Worner.,TXT_ILOG1012,MAP04: After talking to Worner.,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Použij získanou legitimaci pro přístup do elektrárny. Možná by stálo za to podívat se do skladiÅ¡tní místnosti nad Wornerem.","Ødelæg energikrystallen, der driver det kraftnet, som driver Ordenens skjolde. Brug identifikationskortet til at komme ind i kraftværket. Du kan tjekke lagerrummet over Worner.",Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Benutze die Identitätskarte. um in das Kraftwerk zu gelangen. Du solltest den Abstellraum beim Lagerhaus mal inspizieren.,,"Uzu la identigilon por eniri la centralon. Detruu la energian kristalon, kiu impulsas la elektrizan sistemon, kiu tenas la Åildon de la kastelo. Vi eble volos esplori la deponejon en la magazeno (Warehouse).",Usa la identificación para entrar en la central eléctrica. Destruye el cristal de poder que impulsa la red eléctrica que mantiene los escudos del castillo. Quizás quieras revisar el cuarto de almacenamiento en el almacén (Warehouse).,Usa la identificación para entrar a la central eléctrica. Destruye el cristal de poder que impulsa la red eléctrica que mantiene los escudos del castillo. Quizás quieras revisar el cuarto de almacenamiento en el almacén (Warehouse).,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Käytä henkilötunnistetta päästäksesi voimalaitokselle. Saatat haluta vilkaista varastohuonetta Wornerin yläpuolella.",Détruis le cristal qui alimente la grille énergétique des boucliers de l'Ordre. Utilise la carte d'identité pour entrer dans la centrale. Il faudrait que tu aille voir la salle de stockage au dessus de Worner.,,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Használd az igazolványt, hogy bejuss az erÅ‘műbe. Jobban jársz, ha benézel a Worner fölötti tárolószobába.",Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Usa il tesserino d'identificazione per entrare nella centrale energetica. Ti conviene controllare il deposito che si trova sopra Worner.,"é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 手ã«å…¥ã‚ŒãŸI.D.を使ã£ã¦ç™ºé›»æ‰€ã«å…¥ã‚‹ã€‚ ワーナーã®è¨€ã†ä¸ŠéšŽã®éƒ¨å±‹ã‚‚余裕ãŒã‚ã£ãŸã‚‰èª¿ã¹ã‚‹ã€‚",오ë”ì˜ ë°©ì–´ë§‰ì„ ìœ ì§€í•˜ëŠ” ë™ë ¥ ë§ì˜ ì „ë ¥ì›ì¸ 수정체를 파괴하ìž. ì‹ ë¶„ì¦ì„ ì´ìš©í•´ì„œ 발전소 안으로 들어가. 그리고 워너 ìœ„ì— ì°½ê³ ë„ í™•ì¸í•˜ëŠ” ê±° 잊지 마!,Vernietig het energiekristal dat het elektriciteitsnet dat de schilden van de Orde aandrijft. Gebruik de I.D. om in de centrale te komen. Je kunt het magazijn boven Worner bekijken.,Ødelegg kraftkrystallen som driver kraftnettet som driver Ordenens skjold. Bruk I.D. for Ã¥ komme inn i kraftstasjonen. Det kan være lurt Ã¥ sjekke ut lagerrommet over Worner.,"Zniszcz krysztaÅ‚ mocy, który zasila sieć energetycznÄ… napÄ™dzajÄ…cÄ… tarcze Zakonu. Użyj I.D., aby dostać siÄ™ do elektrowni. Możesz chcieć sprawdzić magazyn nad Wornerem.",Destrua o cristal de energia que alimenta a rede elétrica por trás dos escudos da Ordem. Use a identificação para infiltrar a usina de energia. Você pode querer dar uma olhada no depósito acima do Worner.,,Distruge critalul energetic care alimentează scuturile Ordinului. FoloseÈ™te cardul pentru a intra în staÈ›ia de alimentare. Ai putea verifica È™i depozitul de deasupra lui Worner.,"Уничтожь криÑталл, питающий ÑнергоÑеть, от которой работают щиты Ордена. Пройди на ÑлектроÑтанцию по удоÑтоверению. Возможно, ты захочешь проверить Ñклад на втором Ñтаже, о котором говорил УорнÑÑ€.",,Förstör kraftkristallen som driver kraftnätet som driver Ordens sköldar. Använd ID-kortet för att ta dig in i kraftstationen. Du kanske vill kolla in förrÃ¥det ovanför Worner.,Tarikat'ın kalkanlarını çalıştıran güç ÅŸebekesini çalıştıran güç kristalini yok edin. Güç istasyonuna girmek için kimliÄŸi kullanın. Worner'ın üstündeki depoyu kontrol etmek isteyebilirsiniz. -Destroy the power crystal that runs the power grid which drives the Order's shields. Go talk to Ketrick in the core area.,TXT_ILOG1013,"MAP04: After talking to ""Mr. Crispy"".",,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi za Ketrickem u jádra.","Ødelæg kraftkrystallen, der driver det kraftnet, som driver Ordenens skjolde. GÃ¥ hen og tal med Ketrick i kerneomrÃ¥det.","Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Suche Worner auf, einen Spion, den wir im Lagerhaus des Kraftwerks rekrutiert haben. Vergiß nicht, den Sanitäter und den Waffentrainer aufzusuchen, bevor du gehst.",,,Destruye el cristal de poder que impulsa la red eléctrica que alimenta los escudos de La Orden. Habla con Ketrick en el área del núcleo.,,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Puhu Ketrickin kanssa reaktorisydämen alueella.",Détruis le cristal qui alimente la grille énergétique des boucliers de l'Ordre. Va parler à Ketrick dans la zone du cÅ“ur.,,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Megleled Ketricket a mag térségben.",Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Vai a parlare con Ketrick nell'area del nucleo.,"é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 +Destroy the power crystal that runs the power grid which drives the Order's shields. Go talk to Ketrick in the core area.,TXT_ILOG1013,"MAP04: After talking to ""Mr. Crispy"".",,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi za Ketrickem u jádra.","Ødelæg kraftkrystallen, der driver det kraftnet, som driver Ordenens skjolde. GÃ¥ hen og tal med Ketrick i kerneomrÃ¥det.","Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Suche Worner auf, einen Spion, den wir im Lagerhaus des Kraftwerks rekrutiert haben. Vergiß nicht, den Sanitäter und den Waffentrainer aufzusuchen, bevor du gehst.",,"Iru al Ketrick en la kerno. Detruu la energian kristalon, kiu impulsas la elektrizan sistemon, kiu tenas la Åildon de la kastelo.",Busca a Ketrick en el área del núcleo. Destruye el cristal de poder que impulsa la red eléctrica que mantiene los escudos del castillo.,,"Tuhoa kaupungin sähköverkon voimanlähteenä toimiva voimakristalli, jonka voimaa Veljeskunnan kilvet käyttävät. Puhu Ketrickin kanssa reaktorisydämen alueella.",Détruis le cristal qui alimente la grille énergétique des boucliers de l'Ordre. Va parler à Ketrick dans la zone du cÅ“ur.,,"Semmisítsd meg az erÅ‘ kristályt, ami a Rend külsÅ‘ védÅ‘pajzsát hajtja az elektromos hálózaton keresztül. Megleled Ketricket a mag térségben.",Distruggi il cristallo che fornisce energia agli scudi dell'Ordine. Vai a parlare con Ketrick nell'area del nucleo.,"é€é›»ç¶²ã‚’å‹•ã‹ã—ã¦ã„るパワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 コアエリアã§ã‚±ãƒˆãƒªãƒƒã‚¯ã¨è©±ã™ã€‚",오ë”ì˜ ë°©ì–´ë§‰ì„ ìœ ì§€í•˜ëŠ” ë™ë ¥ ë§ì˜ ì „ë ¥ì›ì¸ 수정체를 파괴하ìž. ì¤‘ì‹¬ë¶€ì— ìžˆëŠ” 케트릭과 대화해.,Vernietig het energiekristal dat het elektriciteitsnet dat de schilden van de Orde aandrijft. Ga in het kerngebied met Ketrick praten.,Ødelegg kraftkrystallen som driver kraftnettet som driver Ordenens skjold. Snakk med Ketrick i kjerneomrÃ¥det.,"Zniszcz krysztaÅ‚ mocy, który uruchamia sieć energetycznÄ… napÄ™dzajÄ…cÄ… tarcze Zakonu. Idź porozmawiać z Ketrickiem w obszarze rdzenia.",Destrua o cristal de energia que alimenta a rede elétrica por trás dos escudos da Ordem. Fale com o Ketrick na área do núcleo.,,Distruge cristalul de energie care alimentează scuturile Ordinului. VorbeÈ™te cu Ketrick în zona nucleului.,"Уничтожь криÑталл, питающий ÑнергоÑеть Ордена и их щиты. Поговори Ñ ÐšÐµÑ‚Ñ€Ð¸ÐºÐ¾Ð¼ возле реактора.",,Förstör kraftkristallen som driver kraftnätet som driver ordens sköldar. GÃ¥ och prata med Ketrick i kärnomrÃ¥det.,Tarikat'ın kalkanlarını çalıştıran güç ÅŸebekesini çalıştıran güç kristalini yok edin. Çekirdek alanda Ketrick ile konuÅŸ. -"Destroy the power crystal. Go talk to Ketrick, bring the walkway up using the switches, then use this id for the elevator.",TXT_ILOG1014,MAP04: After talking to Sammis.,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi si promluvit s Ketrickem, vyzvedni schodiÅ¡tÄ› pomocí tlaÄítek a pak ve výtahu použij tuhle kartu.","Ødelæg kraftkrystallen. GÃ¥ hen og tal med Ketrick, bring gangbroen op ved hjælp af kontakterne, og brug derefter dette id til elevatoren.",Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Gehe zu Ketrick im Reaktorkern.,,,"Destruye el cristal de poder. Habla con Ketrick, alza la pasarela usando los interruptores, luego usa esta identificación para el ascensor.",,Tuhoa voimakristalli. Mene puhumaan Ketrickin kanssa. Tuo kulkusilta vivuilla ylös ja sitten käytä tätä tunnistetta hissiin.,"Détruis le cristal. Va parler à Ketrick. Fais monter la coursive en utilisant les boutons, puis utilise ta carte d'identité pour accéder à l'ascenseur.",,"Semmisítsd meg az erÅ‘ kristályt. Menj és beszélj Kedrickkel, a kapcsolóval emeld meg a hidat, aztán használd az igazolványt a lifthez.","Distruggi il cristallo. Parla con Ketrick, fai alzare la passerella con i pulsanti, e poi usa questo tesserino per l'ascensore.","パワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 +"Destroy the power crystal. Go talk to Ketrick, bring the walkway up using the switches, then use this id for the elevator.",TXT_ILOG1014,MAP04: After talking to Sammis.,,,"ZniÄ energetický krystal, který napájí elektrickou síť, kterou jsou pohánÄ›né Å¡títy Řádu. Jdi si promluvit s Ketrickem, vyzvedni schodiÅ¡tÄ› pomocí tlaÄítek a pak ve výtahu použij tuhle kartu.","Ødelæg kraftkrystallen. GÃ¥ hen og tal med Ketrick, bring gangbroen op ved hjælp af kontakterne, og brug derefter dette id til elevatoren.",Zerstöre den Energiekristall der die Energieversorgung für die Kraftschilde kontrolliert. Gehe zu Ketrick im Reaktorkern.,,Levu la ponton per la Åaltiloj antaÅ­ Sammis kaj tiam ekagigu la lifton per lia identigilo. Iru al Ketrick kaj detruu la energian kristalon.,Usa los interruptores frente a Sammis para alzar la pasarela y luego su identificación con el ascensor. Busca a Ketrick y destruye el cristal de poder.,,Tuhoa voimakristalli. Mene puhumaan Ketrickin kanssa. Tuo kulkusilta vivuilla ylös ja sitten käytä tätä tunnistetta hissiin.,"Détruis le cristal. Va parler à Ketrick. Fais monter la coursive en utilisant les boutons, puis utilise ta carte d'identité pour accéder à l'ascenseur.",,"Semmisítsd meg az erÅ‘ kristályt. Menj és beszélj Kedrickkel, a kapcsolóval emeld meg a hidat, aztán használd az igazolványt a lifthez.","Distruggi il cristallo. Parla con Ketrick, fai alzare la passerella con i pulsanti, e poi usa questo tesserino per l'ascensore.","パワークリスタルを破壊ã—ã¦ã‚ªãƒ¼ãƒ€ãƒ¼ã®ã‚·ãƒ¼ãƒ«ãƒ‰ã‚’æ­¢ã‚る。 ケトリックã¨è©±ã—ã€ã‚¹ã‚¤ãƒƒãƒã‚’å‹•ã‹ã—通路を上ã’ã€ã‚¨ãƒ¬ãƒ™ãƒ¼ã‚¿ãƒ¼ã«IDを使ã†ã€‚",수정체를 파괴해. 케트릭과 대화하고 나서 스위치를 눌러서 진입로를 ìž‘ë™ì‹œì¼œ. 그리고 ì´ ì‹ ë¶„ì¦ì„ ì´ìš©í•´ì„œ 승강기를 올ë¼íƒ€.,"Vernietig het energiekristal. Ga met Ketrick praten, breng de gang naar boven met behulp van de schakelaars, gebruik dan deze id voor de lift.","Ødelegg kraftkrystallen. Snakk med Ketrick, fÃ¥ gangbroen opp ved hjelp av bryterne, og bruk denne id-en til heisen.","Zniszcz krysztaÅ‚ mocy. Idź porozmawiać z Ketrickiem, podnieÅ› chodnik używajÄ…c przełączników, a nastÄ™pnie użyj tego id do windy.","Destrua o cristal de energia. Fale com o Ketrick, faça a passarela subir usando os interruptores e depois use esta identificação para o elevador.",,"Distruge cristalul energetic. VorbeÈ™te cu Ketrick, redă pasarela folosind butoanele, apoi foloseÈ™te cardul pentru lift.","Уничтожь криÑталл. Поговори Ñ ÐšÐµÑ‚Ñ€Ð¸ÐºÐ¾Ð¼, пройди наверх Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ переключателей, затем иÑпользуй удоÑтоверение Ð´Ð»Ñ Ð´Ð¾Ñтупа к лифту.",,"Förstör kraftkristallen. GÃ¥ och prata med Ketrick, ta upp gÃ¥ngvägen med hjälp av brytarna och använd sedan denna id för hissen.","Güç kristalini yok et. Ketrick ile konuÅŸun, anahtarları kullanarak geçidi yukarı getirin, sonra asansör için bu kimliÄŸi kullanın." Find the town entrance that the Order has guarded. Open the door and bring the guard's uniform back to Weran.,TXT_ILOG1015,,,,"Najdi vchod do mÄ›sta, který Řád hlídá. OtevÅ™i dveÅ™e a pÅ™ines hlídaÄovu uniformu Weranovi.","Find byens indgang, som Ordenen har bevogtet. Ã…bn døren og bring vagtens uniform tilbage til Weran.","Finde den Ausgang zur Stadt, der vom Orden bewacht wird. Öffne dir Tür und bringe die Uniform des Wächters zu Weran.",,,Encuentra la entrada al pueblo que La Orden tiene guardada. Abre la puerta y tráele de vuelta a Weran el uniforme del guardia.,,Löydä Veljeskunnan vartioima kaupungin sisäänkäynti. Avaa ovi ja tuo vartijan univormu Weranille.,Trouve l'entrée de la ville que L'Ordre garde fermée. Ouvre-la et amène l'uniforme du garde à Weran.,,"Keresd meg a város bejáratot, amit a Rend Å‘rzött. Nyisd ki az ajtót, és vidd vissza az uniformist Weranhoz.",Trova l'entrata verso la città che l'Ordine ha chiuso. Apri l'ingresso e riporta l'uniforme della guardia a Weran.,"オーダーãŒå°éŽ–ã—ã¦ã„る町ã¸ã®å…¥ã‚Šå£ã‚’見ã¤ã‘る。 ドアを開ã‘ã€ã‚¬ãƒ¼ãƒ‰ã®åˆ¶æœã‚’ã‚¦ã‚§ãƒ©ãƒ³ã«æŒã£ã¦å¸°ã‚‹ã€‚",오ë”ì˜ ê²½ë¹„ê°€ 지키고 있는 마ì„ì˜ ìž…êµ¬ë¥¼ 찾아. ë¬¸ì„ ì—° ë’¤ ê·¸ ê²½ë¹„ì˜ ì „íˆ¬ë³µì„ ì±™ê²¨ì„œ 워렌ì—게 전해줘.,Zoek de stadstoegang die de Orde heeft bewaakt. Open de deur en breng het uniform van de bewaker terug naar Weran.,Finn inngangen til byen som Ordenen har bevoktet. Ã…pne døren og ta med vaktens uniform tilbake til Weran.,"Znajdź wejÅ›cie do miasta, którego pilnuje Zakon. Otwórz drzwi i przynieÅ› Weranowi mundur strażnika.",Encontre a entrada da cidade que a Ordem está vigiando. Abra a porta e leve o uniforme do guarda de volta para o Weran.,,GăseÈ™te intrarea oraÈ™ului pe care Ordinul a păzit-o. Deschide uÈ™a È™i du uniforma paznicului la Weran.,"Ðайди выход в город, охранÑемый Орденом. Открой дверь и принеÑи униформу Ñтражника УÑрану.",,Hitta stadens ingÃ¥ng som ordern har bevakat. Öppna dörren och ta med dig vaktuniformen tillbaka till Weran.,Tarikatın koruduÄŸu kasaba giriÅŸini bulun. Kapıyı açın ve muhafız üniformasını Weran'a geri getirin. @@ -12542,8 +12549,8 @@ Security Comple,TXT_SPEAKER_SECURITY_COMPLE,"Intended to appear in MAP19, but do Computer Tech,TXT_SPEAKER_COMPUTER_TECH,,,,PoÄítaÄový technik,,Computertechniker,,Komputilisto,Técnico de sistemas,,Tietokoneteknikko,Technicien Informatique,,Számítógép Tech,Tecnico Informatico,機械技術者,컴퓨터 기술ìž,Computertechniek,Datatekniker,Technik Komputerowy,Técnico de Informática,,Tehnician Calculatoare,Компьютерный техник,,Datortekniker,Bilgisayar Teknolojisi MacGuffin,TXT_SPEAKER_MACGUFFIN,,,,,,,,Makgufino,,,,,,,,マクガフィン,맥거핀,,,,,,,Макгаффин,,, Arion,TXT_SPEAKER_ARION,,,,,,,,,,,,,,,,アリオン,아리온,,,,,,,Ðрион,,,Aron -Dock Worker,TXT_SPEAKER_DOCK_WORKER,,,,Přístavní dÄ›lník,Dokarbejder,Dockarbeiter,,Stivisto,Estibador,,Ahtaaja,Docker,,Dokk munkás,Operaio Portuale,港湾労åƒè€…,항구 ì¼ê¾¼,Dokwerker,Havnearbeider,Pracownik Doku,Operário da Doca,,Muncitor Docuri,Рабочий дока,,Hamnarbetare,Liman İşçisi -Irale,TXT_SPEAKER_IRALE,,,,,,,,,,,,,,,,イラール,ì´ë¡¤ë¦¬,,,,,,,ИрÑйл,,,İrale +Dock Worker,TXT_SPEAKER_DOCK_WORKER,This is a compound word. Some languages express this in one word.,,,Přístavní dÄ›lník,Dokarbejder,Dockarbeiter,,Stivisto,Estibador,,Ahtaaja,Docker,,Dokk munkás,Stivatore,港湾労åƒè€…,항구 ì¼ê¾¼,Dokwerker,Havnearbeider,Pracownik Doku,Estivador,,Muncitor Docuri,Рабочий дока,,Hamnarbetare,Liman İşçisi +Irale,TXT_SPEAKER_IRALE,,,,,,,,Iralo,,,,,,,,イラール,ì´ë¡¤ë¦¬,,,,,,,ИрÑйл,,,İrale Core Guard,TXT_SPEAKER_CORE_GUARD,,,,Stráž jádra,Kernevagt,Wache,,Gardisto de la kerno,Guardia del núcleo,,Ytimenvartija,Garde du cÅ“ur,,Mag År,Guardia del Cuore,コア ガード,코어 경비병,Kernwacht,Kjernevakt,Strażnik Rdzenia,Guarda do Núcleo,,Gardian Nucleu,Страж Ñдра,,KärnvÃ¥rdsvakt,Çekirdek Koruma Sewer Guard,TXT_SPEAKER_SEWER_GUARD,,,,Kanální stráž,Kloakvagt,Kanalisationswächter,,Gardisto de la kloako,Guardia de las alcantarillas,,Viemärinvartija,Garde des égouts,,Kanális År,Guardia della Fogna,下水é“ガード,í•˜ìˆ˜ë„ ë³´ì´ˆë³‘,Rioolwacht,Kloakkvakt,Strażnik Kanałów,Guarda do Esgoto,,Gardian Canal,Страж канализации,,Avloppsvakt,Kanalizasyon Görevlisi Technician,TXT_SPEAKER_TECHNICIAN,,,,Technik,Tekniker,Techniker,,Teknikisto,Técnico,,Teknikko,Technicien,,Technikus,Tecnico,技術者,기술ìž,Technicus,Tekniker,Technik,Técnico,,Tehnician,Техник,,Tekniker,Teknisyen From 538f62a556cd7b2e6815e4c67f2d366671accec8 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Sat, 22 Feb 2025 13:22:41 -0500 Subject: [PATCH 024/384] - renormalize normals on scaled models --- wadsrc/static/shaders/glsl/main.vp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wadsrc/static/shaders/glsl/main.vp b/wadsrc/static/shaders/glsl/main.vp index f718d73bc..f800fea1b 100644 --- a/wadsrc/static/shaders/glsl/main.vp +++ b/wadsrc/static/shaders/glsl/main.vp @@ -94,8 +94,8 @@ void main() ClipDistance4 = worldcoord.y - ((uSplitBottomPlane.w + uSplitBottomPlane.x * worldcoord.x + uSplitBottomPlane.y * worldcoord.z) * uSplitBottomPlane.z); } - vWorldNormal = NormalModelMatrix * vec4(normalize(bones.Normal), 1.0); - vEyeNormal = NormalViewMatrix * vec4(normalize(vWorldNormal.xyz), 1.0); + vWorldNormal = vec4(normalize((NormalModelMatrix * vec4(normalize(bones.Normal), 1.0)).xyz), 1.0); + vEyeNormal = vec4(normalize((NormalViewMatrix * vec4(normalize(vWorldNormal.xyz), 1.0)).xyz), 1.0); #endif #ifdef SPHEREMAP From e5cf79fecb325d3620fdec7a3422a453329559d9 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Tue, 25 Feb 2025 12:55:15 -0500 Subject: [PATCH 025/384] - don't load idres24.wad in multiplayer games by default; allow override with cvar setting --- src/d_iwad.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/d_iwad.cpp b/src/d_iwad.cpp index 6ce60d30b..195e39aa5 100644 --- a/src/d_iwad.cpp +++ b/src/d_iwad.cpp @@ -59,6 +59,8 @@ EXTERN_CVAR(Bool, autoloadbrightmaps) EXTERN_CVAR(Bool, autoloadwidescreen) EXTERN_CVAR(String, language) +CVAR(Int, i_loadsupportwad, 1, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // 0=never, 1=singleplayer only, 2=always + bool foundprio = false; // global to prevent iwad box from appearing //========================================================================== @@ -842,11 +844,16 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char if(info.SupportWAD.IsNotEmpty()) { - FString supportWAD = IWADPathFileSearch(info.SupportWAD); + bool wantsnetgame = (Args->CheckParm("-join") || Args->CheckParm("-host")); - if(supportWAD.IsNotEmpty()) + if ((wantsnetgame && i_loadsupportwad == 1) || (i_loadsupportwad == 2)) { - D_AddFile(wadfiles, supportWAD.GetChars(), true, -1, GameConfig); + FString supportWAD = IWADPathFileSearch(info.SupportWAD); + + if(supportWAD.IsNotEmpty()) + { + D_AddFile(wadfiles, supportWAD.GetChars(), true, -1, GameConfig); + } } } From 52a54521ed43d2495cab3e194e7f8d55bca799b1 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Tue, 25 Feb 2025 12:59:23 -0500 Subject: [PATCH 026/384] - fix erroneous check in previous commit --- src/d_iwad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d_iwad.cpp b/src/d_iwad.cpp index 195e39aa5..589b2fbb4 100644 --- a/src/d_iwad.cpp +++ b/src/d_iwad.cpp @@ -846,7 +846,7 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char { bool wantsnetgame = (Args->CheckParm("-join") || Args->CheckParm("-host")); - if ((wantsnetgame && i_loadsupportwad == 1) || (i_loadsupportwad == 2)) + if ((!wantsnetgame && i_loadsupportwad == 1) || (i_loadsupportwad == 2)) { FString supportWAD = IWADPathFileSearch(info.SupportWAD); From b0d6f143f6c01e267a878c149b228613593aa7cf Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Thu, 27 Feb 2025 12:56:44 -0500 Subject: [PATCH 027/384] - fix classic doom.doom filter --- src/d_main.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 34ae5b369..875f892bf 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -3161,9 +3161,10 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw lfi.gameTypeFilter.push_back(LumpFilterIWAD.GetChars()); // Workaround for old Doom filter names. - if (LumpFilterIWAD.Compare("doom.id.doom") == 0) + if (LumpFilterIWAD.IndexOf("doom.id.doom") >= 0) { - lfi.gameTypeFilter.push_back("doom.doom"); + FString NewFilterName = (FString)"doom.doom" + LumpFilterIWAD.Mid(12); // "doom.id.doom" is 12 characters + lfi.gameTypeFilter.push_back(NewFilterName.GetChars()); } From f1b5ba09e0fb001e7a6d1771ef68abfb15d2931d Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson <18584402+madame-rachelle@users.noreply.github.com> Date: Fri, 28 Feb 2025 02:38:42 -0500 Subject: [PATCH 028/384] Update README.md update copyright year --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7268877ea..8cb527d82 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## GZDoom is a modder-friendly OpenGL and Vulkan source port based on the DOOM engine -Copyright (c) 1998-2023 ZDoom + GZDoom teams, and contributors +Copyright (c) 1998-2025 ZDoom + GZDoom teams, and contributors Doom Source (c) 1997 id Software, Raven Software, and contributors From 6b8736fb304eed5cd7b5121aca74e1699e6da777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 2 Mar 2025 14:45:00 -0300 Subject: [PATCH 029/384] rework how vector local type restrictions are managed --- src/common/scripting/backend/codegen.cpp | 13 ++++++------- src/common/scripting/core/types.cpp | 4 ++++ src/common/scripting/core/types.h | 5 +++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/common/scripting/backend/codegen.cpp b/src/common/scripting/backend/codegen.cpp index 1ce7d6acf..b9f1aa54c 100644 --- a/src/common/scripting/backend/codegen.cpp +++ b/src/common/scripting/backend/codegen.cpp @@ -12748,16 +12748,15 @@ ExpEmit FxFunctionPtrCast::Emit(VMFunctionBuilder *build) FxLocalVariableDeclaration::FxLocalVariableDeclaration(PType *type, FName name, FxExpression *initval, int varflags, const FScriptPosition &p) :FxExpression(EFX_LocalVariableDeclaration, p) { - // Local FVector isn't different from Vector - if (type == TypeFVector2) type = TypeVector2; - else if (type == TypeFVector3) type = TypeVector3; - else if (type == TypeFVector4) type = TypeVector4; - else if (type == TypeFQuaternion) type = TypeQuaternion; + if(type != type->GetLocalType()) + { + ScriptPosition.Message(MSG_WARNING, "Type '%s' not allowed in local variables, changing to '%s'", type->DescriptiveName(), type->GetLocalType()->DescriptiveName()); + } - ValueType = type; + ValueType = type->GetLocalType(); VarFlags = varflags; Name = name; - RegCount = type->RegCount; + RegCount = ValueType->RegCount; Init = initval; clearExpr = nullptr; } diff --git a/src/common/scripting/core/types.cpp b/src/common/scripting/core/types.cpp index d99f7eb13..6b15cb735 100644 --- a/src/common/scripting/core/types.cpp +++ b/src/common/scripting/core/types.cpp @@ -401,6 +401,7 @@ void PType::StaticInit() TypeFVector2->RegType = REGT_FLOAT; TypeFVector2->RegCount = 2; TypeFVector2->isOrdered = true; + TypeFVector2->SetLocalType(TypeVector2); TypeFVector3 = new PStruct(NAME_FVector3, nullptr); TypeFVector3->AddField(NAME_X, TypeFloat32); @@ -415,6 +416,7 @@ void PType::StaticInit() TypeFVector3->RegType = REGT_FLOAT; TypeFVector3->RegCount = 3; TypeFVector3->isOrdered = true; + TypeFVector3->SetLocalType(TypeVector3); TypeFVector4 = new PStruct(NAME_FVector4, nullptr); TypeFVector4->AddField(NAME_X, TypeFloat32); @@ -431,6 +433,7 @@ void PType::StaticInit() TypeFVector4->RegType = REGT_FLOAT; TypeFVector4->RegCount = 4; TypeFVector4->isOrdered = true; + TypeFVector4->SetLocalType(TypeVector4); TypeQuaternion = new PStruct(NAME_Quat, nullptr); @@ -464,6 +467,7 @@ void PType::StaticInit() TypeFQuaternion->RegType = REGT_FLOAT; TypeFQuaternion->RegCount = 4; TypeFQuaternion->isOrdered = true; + TypeFQuaternion->SetLocalType(TypeQuaternion); Namespaces.GlobalNamespace->Symbols.AddSymbol(Create(NAME_sByte, TypeSInt8)); diff --git a/src/common/scripting/core/types.h b/src/common/scripting/core/types.h index 69d73bd77..2568367e6 100644 --- a/src/common/scripting/core/types.h +++ b/src/common/scripting/core/types.h @@ -110,6 +110,11 @@ public: EScopeFlags ScopeFlags = (EScopeFlags)0; bool SizeKnown = true; + PType * LocalType = nullptr; + + PType * SetLocalType(PType * LocalType) { this->LocalType = LocalType; return this; } + PType * GetLocalType() { return LocalType ? LocalType : this; } + PType(unsigned int size = 1, unsigned int align = 1); virtual ~PType(); virtual bool isNumeric() { return false; } From e8b7a30a796412546e4e34605273ac1a5e880fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 2 Mar 2025 15:18:43 -0300 Subject: [PATCH 030/384] add better descriptive name for vectors/quats --- src/common/scripting/core/types.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/common/scripting/core/types.cpp b/src/common/scripting/core/types.cpp index 6b15cb735..20ce7e05d 100644 --- a/src/common/scripting/core/types.cpp +++ b/src/common/scripting/core/types.cpp @@ -359,6 +359,7 @@ void PType::StaticInit() TypeVector2->RegType = REGT_FLOAT; TypeVector2->RegCount = 2; TypeVector2->isOrdered = true; + TypeVector2->mDescriptiveName = "Vector2"; TypeVector3 = new PStruct(NAME_Vector3, nullptr); TypeVector3->AddField(NAME_X, TypeFloat64); @@ -373,6 +374,7 @@ void PType::StaticInit() TypeVector3->RegType = REGT_FLOAT; TypeVector3->RegCount = 3; TypeVector3->isOrdered = true; + TypeVector3->mDescriptiveName = "Vector3"; TypeVector4 = new PStruct(NAME_Vector4, nullptr); TypeVector4->AddField(NAME_X, TypeFloat64); @@ -389,6 +391,7 @@ void PType::StaticInit() TypeVector4->RegType = REGT_FLOAT; TypeVector4->RegCount = 4; TypeVector4->isOrdered = true; + TypeVector4->mDescriptiveName = "Vector4"; TypeFVector2 = new PStruct(NAME_FVector2, nullptr); @@ -401,6 +404,7 @@ void PType::StaticInit() TypeFVector2->RegType = REGT_FLOAT; TypeFVector2->RegCount = 2; TypeFVector2->isOrdered = true; + TypeFVector2->mDescriptiveName = "FVector2"; TypeFVector2->SetLocalType(TypeVector2); TypeFVector3 = new PStruct(NAME_FVector3, nullptr); @@ -416,6 +420,7 @@ void PType::StaticInit() TypeFVector3->RegType = REGT_FLOAT; TypeFVector3->RegCount = 3; TypeFVector3->isOrdered = true; + TypeFVector3->mDescriptiveName = "FVector3"; TypeFVector3->SetLocalType(TypeVector3); TypeFVector4 = new PStruct(NAME_FVector4, nullptr); @@ -433,6 +438,7 @@ void PType::StaticInit() TypeFVector4->RegType = REGT_FLOAT; TypeFVector4->RegCount = 4; TypeFVector4->isOrdered = true; + TypeFVector4->mDescriptiveName = "FVector4"; TypeFVector4->SetLocalType(TypeVector4); @@ -450,6 +456,7 @@ void PType::StaticInit() TypeQuaternion->moveOp = OP_MOVEV4; TypeQuaternion->RegType = REGT_FLOAT; TypeQuaternion->RegCount = 4; + TypeQuaternion->mDescriptiveName = "Quat"; TypeQuaternion->isOrdered = true; TypeFQuaternion = new PStruct(NAME_FQuat, nullptr); @@ -467,6 +474,7 @@ void PType::StaticInit() TypeFQuaternion->RegType = REGT_FLOAT; TypeFQuaternion->RegCount = 4; TypeFQuaternion->isOrdered = true; + TypeFQuaternion->mDescriptiveName = "FQuat"; TypeFQuaternion->SetLocalType(TypeQuaternion); From 35c44c7e21f230000a31db51acfb7916a2683599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 2 Mar 2025 15:19:43 -0300 Subject: [PATCH 031/384] don't allow backing types of string/array/map/etc to be referenced as actual types --- src/common/scripting/core/types.h | 2 + src/common/scripting/frontend/zcc-parse.lemon | 1 + src/common/scripting/frontend/zcc_compile.cpp | 29 +++++++-- src/common/scripting/frontend/zcc_compile.h | 2 +- wadsrc/static/zscript/engine/base.zs | 6 +- wadsrc/static/zscript/engine/dynarrays.zs | 16 ++--- wadsrc/static/zscript/engine/maps.zs | 64 +++++++++---------- 7 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/common/scripting/core/types.h b/src/common/scripting/core/types.h index 2568367e6..5dc8fd182 100644 --- a/src/common/scripting/core/types.h +++ b/src/common/scripting/core/types.h @@ -110,6 +110,8 @@ public: EScopeFlags ScopeFlags = (EScopeFlags)0; bool SizeKnown = true; + bool TypeInternal = false; + PType * LocalType = nullptr; PType * SetLocalType(PType * LocalType) { this->LocalType = LocalType; return this; } diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index ab46782aa..c6781db12 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -432,6 +432,7 @@ struct_flags(X) ::= struct_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; } struct_flags(X) ::= struct_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; } struct_flags(X) ::= struct_flags(A) CLEARSCOPE. { X.Flags = A.Flags | ZCC_ClearScope; } struct_flags(X) ::= struct_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; } +struct_flags(X) ::= struct_flags(A) INTERNAL. { X.Flags = A.Flags | ZCC_Internal; } struct_flags(X) ::= struct_flags(A) VERSION LPAREN STRCONST(C) RPAREN. { X.Flags = A.Flags | ZCC_Version; X.Version = C.String->GetChars(); } opt_struct_body(X) ::= . { X = NULL; } diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index 8a432e8c1..dfe9dfa13 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -729,6 +729,8 @@ void ZCCCompiler::CreateStructTypes() syms = &OutNamespace->Symbols; } + + if (s->NodeName() == NAME__ && fileSystem.GetFileContainer(Lump) == 0) { // This is just a container for syntactic purposes. @@ -743,11 +745,17 @@ void ZCCCompiler::CreateStructTypes() { s->strct->Type = NewStruct(s->NodeName(), outer, false, AST.FileNo); } + if (s->strct->Flags & ZCC_Version) { s->strct->Type->mVersion = s->strct->Version; } + if (s->strct->Flags & ZCC_Internal) + { + s->strct->Type->TypeInternal = true; + } + auto &sf = s->Type()->ScopeFlags; if (mVersion >= MakeVersion(2, 4, 0)) { @@ -809,6 +817,12 @@ void ZCCCompiler::CreateClassTypes() PClass *parent; auto ParentName = c->cls->ParentName; + if (c->cls->Flags & ZCC_Internal) + { + Error(c->cls, "'Internal' not allowed for classes"); + } + + // The parent exists, we may create a type for this class if (ParentName != nullptr && ParentName->SiblingNext == ParentName) { parent = PClass::FindClass(ParentName->Id); @@ -845,7 +859,6 @@ void ZCCCompiler::CreateClassTypes() Error(c->cls, "Class '%s' cannot extend sealed class '%s'", FName(c->NodeName()).GetChars(), parent->TypeName.GetChars()); } - // The parent exists, we may create a type for this class if (c->cls->Flags & ZCC_Native) { // If this is a native class, its own type must also already exist and not be a runtime class. @@ -1868,7 +1881,7 @@ PType *ZCCCompiler::DetermineType(PType *outertype, ZCC_TreeNode *field, FName n { Error(field, "%s: @ not allowed for user scripts", name.GetChars()); } - retval = ResolveUserType(btype, btype->UserType, outertype? &outertype->Symbols : nullptr, true); + retval = ResolveUserType(outertype, btype, btype->UserType, outertype? &outertype->Symbols : nullptr, true); break; case ZCC_UserType: @@ -1898,7 +1911,7 @@ PType *ZCCCompiler::DetermineType(PType *outertype, ZCC_TreeNode *field, FName n default: - retval = ResolveUserType(btype, btype->UserType, outertype ? &outertype->Symbols : nullptr, false); + retval = ResolveUserType(outertype, btype, btype->UserType, outertype ? &outertype->Symbols : nullptr, false); break; } break; @@ -2153,7 +2166,7 @@ PType *ZCCCompiler::DetermineType(PType *outertype, ZCC_TreeNode *field, FName n * @param nativetype Distinguishes between searching for a native type or a user type. * @returns the PType found for this user type */ -PType *ZCCCompiler::ResolveUserType(ZCC_BasicType *type, ZCC_Identifier *id, PSymbolTable *symt, bool nativetype) +PType *ZCCCompiler::ResolveUserType(PType *outertype, ZCC_BasicType *type, ZCC_Identifier *id, PSymbolTable *symt, bool nativetype) { // Check the symbol table for the identifier. PSymbol *sym = nullptr; @@ -2170,10 +2183,18 @@ PType *ZCCCompiler::ResolveUserType(ZCC_BasicType *type, ZCC_Identifier *id, PSy return TypeError; } + //only allow references to internal types inside internal types + if (ptype->TypeInternal && !outertype->TypeInternal) + { + Error(type, "Type %s not accessible", FName(type->UserType->Id).GetChars()); + return TypeError; + } + if (id->SiblingNext != type->UserType) { assert(id->SiblingNext->NodeType == AST_Identifier); ptype = ResolveUserType( + outertype, type, static_cast(id->SiblingNext), &ptype->Symbols, diff --git a/src/common/scripting/frontend/zcc_compile.h b/src/common/scripting/frontend/zcc_compile.h index 19e9f8ff0..bf1407189 100644 --- a/src/common/scripting/frontend/zcc_compile.h +++ b/src/common/scripting/frontend/zcc_compile.h @@ -134,7 +134,7 @@ protected: FString FlagsToString(uint32_t flags); PType *DetermineType(PType *outertype, ZCC_TreeNode *field, FName name, ZCC_Type *ztype, bool allowarraytypes, bool formember); PType *ResolveArraySize(PType *baseType, ZCC_Expression *arraysize, PContainerType *cls, bool *nosize); - PType *ResolveUserType(ZCC_BasicType *type, ZCC_Identifier *id, PSymbolTable *sym, bool nativetype); + PType *ResolveUserType(PType *outertype, ZCC_BasicType *type, ZCC_Identifier *id, PSymbolTable *sym, bool nativetype); static FString UserTypeName(ZCC_BasicType *type); TArray OrderStructs(); void AddStruct(TArray &new_order, ZCC_StructWork *struct_def); diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index fa212ba38..14cedaed4 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -189,7 +189,7 @@ struct Vector3 } */ -struct _ native // These are the global variables, the struct is only here to avoid extending the parser for this. +struct _ native internal // These are the global variables, the struct is only here to avoid extending the parser for this. { native readonly Array AllClasses; native internal readonly Map AllServices; @@ -903,7 +903,7 @@ enum EmptyTokenType // Although String is a builtin type, this is a convenient way to attach methods to it. // All of these methods are available on strings -struct StringStruct native +struct StringStruct native internal { native static vararg String Format(String fmt, ...); native vararg void AppendFormat(String fmt, ...); @@ -952,7 +952,7 @@ struct Translation version("2.4") } // Convenient way to attach functions to Quat -struct QuatStruct native +struct QuatStruct native internal { native static Quat SLerp(Quat from, Quat to, double t); native static Quat NLerp(Quat from, Quat to, double t); diff --git a/wadsrc/static/zscript/engine/dynarrays.zs b/wadsrc/static/zscript/engine/dynarrays.zs index f9e6a680d..28f36b313 100644 --- a/wadsrc/static/zscript/engine/dynarrays.zs +++ b/wadsrc/static/zscript/engine/dynarrays.zs @@ -1,7 +1,7 @@ // The VM uses 7 integral data types, so for dynamic array support we need one specific set of functions for each of these types. // Do not use these structs directly, they are incomplete and only needed to create prototypes for the needed functions. -struct DynArray_I8 native +struct DynArray_I8 native internal { native readonly int Size; @@ -21,7 +21,7 @@ struct DynArray_I8 native native void Clear (); } -struct DynArray_I16 native +struct DynArray_I16 native internal { native readonly int Size; @@ -41,7 +41,7 @@ struct DynArray_I16 native native void Clear (); } -struct DynArray_I32 native +struct DynArray_I32 native internal { native readonly int Size; @@ -62,7 +62,7 @@ struct DynArray_I32 native native void Clear (); } -struct DynArray_F32 native +struct DynArray_F32 native internal { native readonly int Size; @@ -82,7 +82,7 @@ struct DynArray_F32 native native void Clear (); } -struct DynArray_F64 native +struct DynArray_F64 native internal { native readonly int Size; @@ -102,7 +102,7 @@ struct DynArray_F64 native native void Clear (); } -struct DynArray_Ptr native +struct DynArray_Ptr native internal { native readonly int Size; @@ -122,7 +122,7 @@ struct DynArray_Ptr native native void Clear (); } -struct DynArray_Obj native +struct DynArray_Obj native internal { native readonly int Size; @@ -142,7 +142,7 @@ struct DynArray_Obj native native void Clear (); } -struct DynArray_String native +struct DynArray_String native internal { native readonly int Size; diff --git a/wadsrc/static/zscript/engine/maps.zs b/wadsrc/static/zscript/engine/maps.zs index b871edf05..0e71c3b05 100644 --- a/wadsrc/static/zscript/engine/maps.zs +++ b/wadsrc/static/zscript/engine/maps.zs @@ -1,5 +1,5 @@ -struct Map_I32_I8 native +struct Map_I32_I8 native internal { native void Copy(Map_I32_I8 other); native void Move(Map_I32_I8 other); @@ -18,7 +18,7 @@ struct Map_I32_I8 native native void Remove(int key); } -struct MapIterator_I32_I8 native +struct MapIterator_I32_I8 native internal { native bool Init(Map_I32_I8 other); native bool ReInit(); @@ -31,7 +31,7 @@ struct MapIterator_I32_I8 native native void SetValue(int value); } -struct Map_I32_I16 native +struct Map_I32_I16 native internal { native void Copy(Map_I32_I16 other); native void Move(Map_I32_I16 other); @@ -50,7 +50,7 @@ struct Map_I32_I16 native native void Remove(int key); } -struct MapIterator_I32_I16 native +struct MapIterator_I32_I16 native internal { native bool Init(Map_I32_I16 other); native bool ReInit(); @@ -63,7 +63,7 @@ struct MapIterator_I32_I16 native native void SetValue(int value); } -struct Map_I32_I32 native +struct Map_I32_I32 native internal { native void Copy(Map_I32_I32 other); native void Move(Map_I32_I32 other); @@ -82,7 +82,7 @@ struct Map_I32_I32 native native void Remove(int key); } -struct MapIterator_I32_I32 native +struct MapIterator_I32_I32 native internal { native bool Init(Map_I32_I32 other); native bool ReInit(); @@ -95,7 +95,7 @@ struct MapIterator_I32_I32 native native void SetValue(int value); } -struct Map_I32_F32 native +struct Map_I32_F32 native internal { native void Copy(Map_I32_F32 other); native void Move(Map_I32_F32 other); @@ -114,7 +114,7 @@ struct Map_I32_F32 native native void Remove(int key); } -struct MapIterator_I32_F32 native +struct MapIterator_I32_F32 native internal { native bool Init(Map_I32_F32 other); native bool ReInit(); @@ -127,7 +127,7 @@ struct MapIterator_I32_F32 native native void SetValue(double value); } -struct Map_I32_F64 native +struct Map_I32_F64 native internal { native void Copy(Map_I32_F64 other); native void Move(Map_I32_F64 other); @@ -146,7 +146,7 @@ struct Map_I32_F64 native native void Remove(int key); } -struct MapIterator_I32_F64 native +struct MapIterator_I32_F64 native internal { native bool Init(Map_I32_F64 other); native bool ReInit(); @@ -159,7 +159,7 @@ struct MapIterator_I32_F64 native native void SetValue(double value); } -struct Map_I32_Obj native +struct Map_I32_Obj native internal { native void Copy(Map_I32_Obj other); native void Move(Map_I32_Obj other); @@ -178,7 +178,7 @@ struct Map_I32_Obj native native void Remove(int key); } -struct MapIterator_I32_Obj native +struct MapIterator_I32_Obj native internal { native bool Init(Map_I32_Obj other); native bool ReInit(); @@ -191,7 +191,7 @@ struct MapIterator_I32_Obj native native void SetValue(Object value); } -struct Map_I32_Ptr native +struct Map_I32_Ptr native internal { native void Copy(Map_I32_Ptr other); native void Move(Map_I32_Ptr other); @@ -210,7 +210,7 @@ struct Map_I32_Ptr native native void Remove(int key); } -struct MapIterator_I32_Ptr native +struct MapIterator_I32_Ptr native internal { native bool Init(Map_I32_Ptr other); native bool Next(); @@ -220,7 +220,7 @@ struct MapIterator_I32_Ptr native native void SetValue(voidptr value); } -struct Map_I32_Str native +struct Map_I32_Str native internal { native void Copy(Map_I32_Str other); native void Move(Map_I32_Str other); @@ -239,7 +239,7 @@ struct Map_I32_Str native native void Remove(int key); } -struct MapIterator_I32_Str native +struct MapIterator_I32_Str native internal { native bool Init(Map_I32_Str other); native bool ReInit(); @@ -254,7 +254,7 @@ struct MapIterator_I32_Str native // --------------- -struct Map_Str_I8 native +struct Map_Str_I8 native internal { native void Copy(Map_Str_I8 other); native void Move(Map_Str_I8 other); @@ -273,7 +273,7 @@ struct Map_Str_I8 native native void Remove(String key); } -struct MapIterator_Str_I8 native +struct MapIterator_Str_I8 native internal { native bool Init(Map_Str_I8 other); native bool ReInit(); @@ -286,7 +286,7 @@ struct MapIterator_Str_I8 native native void SetValue(int value); } -struct Map_Str_I16 native +struct Map_Str_I16 native internal { native void Copy(Map_Str_I16 other); native void Move(Map_Str_I16 other); @@ -305,7 +305,7 @@ struct Map_Str_I16 native native void Remove(String key); } -struct MapIterator_Str_I16 native +struct MapIterator_Str_I16 native internal { native bool Init(Map_Str_I16 other); native bool ReInit(); @@ -318,7 +318,7 @@ struct MapIterator_Str_I16 native native void SetValue(int value); } -struct Map_Str_I32 native +struct Map_Str_I32 native internal { native void Copy(Map_Str_I32 other); native void Move(Map_Str_I32 other); @@ -337,7 +337,7 @@ struct Map_Str_I32 native native void Remove(String key); } -struct MapIterator_Str_I32 native +struct MapIterator_Str_I32 native internal { native bool Init(Map_Str_I32 other); native bool ReInit(); @@ -350,7 +350,7 @@ struct MapIterator_Str_I32 native native void SetValue(int value); } -struct Map_Str_F32 native +struct Map_Str_F32 native internal { native void Copy(Map_Str_F32 other); native void Move(Map_Str_F32 other); @@ -369,7 +369,7 @@ struct Map_Str_F32 native native void Remove(String key); } -struct MapIterator_Str_F32 native +struct MapIterator_Str_F32 native internal { native bool Init(Map_Str_F32 other); native bool ReInit(); @@ -382,7 +382,7 @@ struct MapIterator_Str_F32 native native void SetValue(double value); } -struct Map_Str_F64 native +struct Map_Str_F64 native internal { native void Copy(Map_Str_F64 other); native void Move(Map_Str_F64 other); @@ -401,7 +401,7 @@ struct Map_Str_F64 native native void Remove(String key); } -struct MapIterator_Str_F64 native +struct MapIterator_Str_F64 native internal { native bool Init(Map_Str_F64 other); native bool ReInit(); @@ -414,7 +414,7 @@ struct MapIterator_Str_F64 native native void SetValue(double value); } -struct Map_Str_Obj native +struct Map_Str_Obj native internal { native void Copy(Map_Str_Obj other); native void Move(Map_Str_Obj other); @@ -433,7 +433,7 @@ struct Map_Str_Obj native native void Remove(String key); } -struct MapIterator_Str_Obj native +struct MapIterator_Str_Obj native internal { native bool Init(Map_Str_Obj other); native bool ReInit(); @@ -446,7 +446,7 @@ struct MapIterator_Str_Obj native native void SetValue(Object value); } -struct Map_Str_Ptr native +struct Map_Str_Ptr native internal { native void Copy(Map_Str_Ptr other); native void Move(Map_Str_Ptr other); @@ -465,7 +465,7 @@ struct Map_Str_Ptr native native void Remove(String key); } -struct MapIterator_Str_Ptr native +struct MapIterator_Str_Ptr native internal { native bool Init(Map_Str_Ptr other); native bool ReInit(); @@ -478,7 +478,7 @@ struct MapIterator_Str_Ptr native native void SetValue(voidptr value); } -struct Map_Str_Str native +struct Map_Str_Str native internal { native void Copy(Map_Str_Str other); native void Move(Map_Str_Str other); @@ -497,7 +497,7 @@ struct Map_Str_Str native native void Remove(String key); } -struct MapIterator_Str_Str native +struct MapIterator_Str_Str native internal { native bool Init(Map_Str_Str other); native bool ReInit(); From 93c8af32ca8844d736d4ad66a3dd3eb4d91a75ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 2 Mar 2025 16:57:26 -0300 Subject: [PATCH 032/384] allow deprecation of classes/structs, deprecate Dictionary --- src/common/scripting/core/types.h | 2 + src/common/scripting/frontend/zcc-parse.lemon | 54 +++++++++++++------ src/common/scripting/frontend/zcc_compile.cpp | 25 ++++++++- src/common/scripting/frontend/zcc_parser.h | 1 + wadsrc/static/zscript/engine/dictionary.zs | 4 +- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/common/scripting/core/types.h b/src/common/scripting/core/types.h index 5dc8fd182..380ae88d4 100644 --- a/src/common/scripting/core/types.h +++ b/src/common/scripting/core/types.h @@ -111,6 +111,8 @@ public: bool SizeKnown = true; bool TypeInternal = false; + bool TypeDeprecated = false; // mVersion is deprecation version, not minimum version + FString mDeprecationMessage; PType * LocalType = nullptr; diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index c6781db12..eed755355 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -80,6 +80,7 @@ static void SetNodeLine(ZCC_TreeNode *name, int line) ZCC_Identifier *Replaces; ZCC_Identifier *Sealed; VersionInfo Version; + FString *DeprecationMessage; }; struct StateOpts { @@ -232,6 +233,7 @@ class_head(X) ::= EXTEND CLASS(T) IDENTIFIER(A). head->Version = {0, 0}; head->Type = nullptr; head->Symbol = nullptr; + head->DeprecationMessage = nullptr; X = head; } @@ -247,6 +249,7 @@ class_head(X) ::= CLASS(T) IDENTIFIER(A) class_ancestry(B) class_flags(C). head->Version = C.Version; head->Type = nullptr; head->Symbol = nullptr; + head->DeprecationMessage = C.DeprecationMessage; X = head; } @@ -255,15 +258,24 @@ class_ancestry(X) ::= . { X = NULL; } class_ancestry(X) ::= COLON dottable_id(A). { X = A; /*X-overwrites-A*/ } %type class_flags{ClassFlagsBlock} -class_flags(X) ::= . { X.Flags = 0; X.Replaces = NULL; X.Version = {0,0}; X.Sealed = NULL; } -class_flags(X) ::= class_flags(A) ABSTRACT. { X.Flags = A.Flags | ZCC_Abstract; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; } -class_flags(X) ::= class_flags(A) FINAL. { X.Flags = A.Flags | ZCC_Final; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed;} -class_flags(X) ::= class_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; } -class_flags(X) ::= class_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; } -class_flags(X) ::= class_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; } -class_flags(X) ::= class_flags(A) REPLACES dottable_id(B). { X.Flags = A.Flags; X.Replaces = B; X.Version = A.Version; X.Sealed = A.Sealed; } -class_flags(X) ::= class_flags(A) VERSION LPAREN STRCONST(C) RPAREN. { X.Flags = A.Flags | ZCC_Version; X.Replaces = A.Replaces; X.Version = C.String->GetChars(); X.Sealed = A.Sealed; } -class_flags(X) ::= class_flags(A) SEALED LPAREN states_opt(B) RPAREN. { X.Flags = A.Flags | ZCC_Sealed; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = B; } +class_flags(X) ::= . { X.Flags = 0; X.Replaces = NULL; X.Version = {0,0}; X.Sealed = NULL; X.DeprecationMessage = NULL; } +class_flags(X) ::= class_flags(A) ABSTRACT. { X.Flags = A.Flags | ZCC_Abstract; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) FINAL. { X.Flags = A.Flags | ZCC_Final; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) REPLACES dottable_id(B). { X.Flags = A.Flags; X.Replaces = B; X.Version = A.Version; X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) VERSION LPAREN STRCONST(C) RPAREN. { X.Flags = A.Flags | ZCC_Version; X.Replaces = A.Replaces; X.Version = C.String->GetChars(); X.Sealed = A.Sealed; X.DeprecationMessage = A.DeprecationMessage; } +class_flags(X) ::= class_flags(A) SEALED LPAREN states_opt(B) RPAREN. { X.Flags = A.Flags | ZCC_Sealed; X.Replaces = A.Replaces; X.Version = A.Version; X.Sealed = B; X.DeprecationMessage = A.DeprecationMessage; } + +class_flags(X) ::= class_flags(A) DEPRECATED LPAREN STRCONST(C) opt_deprecation_message(D) RPAREN. +{ + X.Flags = A.Flags | ZCC_Deprecated; + X.Replaces = A.Replaces; + X.Version = C.String->GetChars(); + X.Sealed = A.Sealed; + X.DeprecationMessage = D.String; +} /*----- Dottable Identifier -----*/ // This can be either a single identifier or two identifiers connected by a . @@ -412,6 +424,7 @@ struct_def(X) ::= STRUCT(T) IDENTIFIER(A) struct_flags(S) LBRACE opt_struct_body def->Symbol = nullptr; def->Version = S.Version; def->Flags = S.Flags; + def->DeprecationMessage = S.DeprecationMessage; X = def; } @@ -422,18 +435,27 @@ struct_def(X) ::= EXTEND STRUCT(T) IDENTIFIER(A) LBRACE opt_struct_body(B) RBRAC def->Body = B; def->Type = nullptr; def->Symbol = nullptr; + def->Version = {0, 0}; def->Flags = ZCC_Extension; + def->DeprecationMessage = nullptr; X = def; } %type struct_flags{ClassFlagsBlock} -struct_flags(X) ::= . { X.Flags = 0; X.Version = {0, 0}; } -struct_flags(X) ::= struct_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; } -struct_flags(X) ::= struct_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; } -struct_flags(X) ::= struct_flags(A) CLEARSCOPE. { X.Flags = A.Flags | ZCC_ClearScope; } -struct_flags(X) ::= struct_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; } -struct_flags(X) ::= struct_flags(A) INTERNAL. { X.Flags = A.Flags | ZCC_Internal; } -struct_flags(X) ::= struct_flags(A) VERSION LPAREN STRCONST(C) RPAREN. { X.Flags = A.Flags | ZCC_Version; X.Version = C.String->GetChars(); } +struct_flags(X) ::= . { X.Flags = 0; X.Version = {0, 0}; X.DeprecationMessage = NULL; } +struct_flags(X) ::= struct_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) CLEARSCOPE. { X.Flags = A.Flags | ZCC_ClearScope; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) INTERNAL. { X.Flags = A.Flags | ZCC_Internal; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) VERSION LPAREN STRCONST(C) RPAREN. { X.Flags = A.Flags | ZCC_Version; X.Version = C.String->GetChars(); X.DeprecationMessage = A.DeprecationMessage; } + +struct_flags(X) ::= struct_flags(A) DEPRECATED LPAREN STRCONST(C) opt_deprecation_message(D) RPAREN. +{ + X.Flags = A.Flags | ZCC_Deprecated; + X.Version = C.String->GetChars(); + X.DeprecationMessage = D.String; +} opt_struct_body(X) ::= . { X = NULL; } opt_struct_body(X) ::= struct_body(X). diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index dfe9dfa13..fb49cf39a 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -751,6 +751,13 @@ void ZCCCompiler::CreateStructTypes() s->strct->Type->mVersion = s->strct->Version; } + if (s->strct->Flags & ZCC_Deprecated) + { + s->strct->Type->mVersion = s->strct->Version; + s->strct->Type->TypeDeprecated = true; + s->strct->Type->mDeprecationMessage = s->strct->DeprecationMessage ? *s->strct->DeprecationMessage : ""; + } + if (s->strct->Flags & ZCC_Internal) { s->strct->Type->TypeInternal = true; @@ -919,6 +926,13 @@ void ZCCCompiler::CreateClassTypes() { c->Type()->mVersion = c->cls->Version; } + + if (c->cls->Flags & ZCC_Deprecated) + { + c->Type()->mVersion = c->cls->Version; + c->Type()->TypeDeprecated = true; + c->Type()->mDeprecationMessage = c->cls->DeprecationMessage ? *c->cls->DeprecationMessage : ""; + } if (c->cls->Flags & ZCC_Final) @@ -2177,7 +2191,16 @@ PType *ZCCCompiler::ResolveUserType(PType *outertype, ZCC_BasicType *type, ZCC_I if (sym != nullptr && sym->IsKindOf(RUNTIME_CLASS(PSymbolType))) { auto ptype = static_cast(sym)->Type; - if (ptype->mVersion > mVersion) + + if (ptype->TypeDeprecated) + { + if(ptype->mVersion <= mVersion && !outertype->TypeDeprecated && fileSystem.GetFileContainer(Lump) > 0) + { + Warn(type, "Type %s is deprecated since ZScript version %d.%d.%d%s%s", + FName(type->UserType->Id).GetChars(), mVersion.major, mVersion.minor, mVersion.revision, ptype->mDeprecationMessage.IsEmpty() ? "" : ": ", ptype->mDeprecationMessage.GetChars()); + } + } + else if (ptype->mVersion > mVersion) { Error(type, "Type %s not accessible to ZScript version %d.%d.%d", FName(type->UserType->Id).GetChars(), mVersion.major, mVersion.minor, mVersion.revision); return TypeError; diff --git a/src/common/scripting/frontend/zcc_parser.h b/src/common/scripting/frontend/zcc_parser.h index 47ab2d65f..d12d7b87e 100644 --- a/src/common/scripting/frontend/zcc_parser.h +++ b/src/common/scripting/frontend/zcc_parser.h @@ -243,6 +243,7 @@ struct ZCC_Struct : ZCC_NamedNode ZCC_TreeNode *Body; PContainerType *Type; VersionInfo Version; + FString *DeprecationMessage; }; struct ZCC_Property : ZCC_NamedNode diff --git a/wadsrc/static/zscript/engine/dictionary.zs b/wadsrc/static/zscript/engine/dictionary.zs index 0bd48cd69..6c778492f 100644 --- a/wadsrc/static/zscript/engine/dictionary.zs +++ b/wadsrc/static/zscript/engine/dictionary.zs @@ -5,7 +5,7 @@ * * @note keys are case-sensitive. */ -class Dictionary +class Dictionary deprecated("4.15", "Use Map instead") { native static Dictionary Create(); @@ -38,7 +38,7 @@ class Dictionary * DictionaryIterator is not serializable. To make DictionaryIterator a class * member, use `transient` keyword. */ -class DictionaryIterator +class DictionaryIterator deprecated("4.15", "Use Map instead") { native static DictionaryIterator Create(Dictionary dict); From 02523b1f90d8c3d971d0949c9b9efbde1b28896b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 2 Mar 2025 16:58:37 -0300 Subject: [PATCH 033/384] restrict internal structs to gzdoom.pk3 --- src/common/scripting/frontend/zcc_compile.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index fb49cf39a..2eff4dab0 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -760,7 +760,14 @@ void ZCCCompiler::CreateStructTypes() if (s->strct->Flags & ZCC_Internal) { - s->strct->Type->TypeInternal = true; + if(fileSystem.GetFileContainer(Lump) == 0) + { + s->strct->Type->TypeInternal = true; + } + else + { + Error(s->strct, "Internal structs are only allowed in the root pk3"); + } } auto &sf = s->Type()->ScopeFlags; From 7685553af8d3f451789a25209684ee0b8df3770f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Mon, 3 Mar 2025 08:15:10 -0300 Subject: [PATCH 034/384] 4.14.1 accepts 4.15, bump version to 4.15.1 --- src/common/engine/sc_man_scanner.re | 2 +- src/common/scripting/frontend/zcc_compile.cpp | 2 +- src/version.h | 4 ++-- wadsrc/static/zscript.txt | 2 +- wadsrc/static/zscript/actors/actor.zs | 16 ++++++++-------- wadsrc/static/zscript/engine/dictionary.zs | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/common/engine/sc_man_scanner.re b/src/common/engine/sc_man_scanner.re index a1283778e..5a0d88fb0 100644 --- a/src/common/engine/sc_man_scanner.re +++ b/src/common/engine/sc_man_scanner.re @@ -177,7 +177,7 @@ std2: /* Other keywords from UnrealScript */ 'abstract' { RET(TK_Abstract); } 'foreach' { RET(ParseVersion >= MakeVersion(4, 10, 0)? TK_ForEach : TK_Identifier); } - 'unsafe' { RET(ParseVersion >= MakeVersion(4, 15, 0)? TK_Unsafe : TK_Identifier); } + 'unsafe' { RET(ParseVersion >= MakeVersion(4, 15, 1)? TK_Unsafe : TK_Identifier); } 'true' { RET(TK_True); } 'false' { RET(TK_False); } 'none' { RET(TK_None); } diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index 2eff4dab0..492498f4b 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -2497,7 +2497,7 @@ void ZCCCompiler::CompileFunction(ZCC_StructWork *c, ZCC_FuncDeclarator *f, bool if (f->Flags & ZCC_Override) varflags |= VARF_Override; if (f->Flags & ZCC_Abstract) varflags |= VARF_Abstract; if (f->Flags & ZCC_VarArg) varflags |= VARF_VarArg; - if (f->Flags & ZCC_FuncConst) varflags |= (mVersion >= MakeVersion(4, 15, 0) ? VARF_ReadOnly | VARF_SafeConst : VARF_ReadOnly); // FuncConst method is internally marked as VARF_ReadOnly + if (f->Flags & ZCC_FuncConst) varflags |= (mVersion >= MakeVersion(4, 15, 1) ? VARF_ReadOnly | VARF_SafeConst : VARF_ReadOnly); // FuncConst method is internally marked as VARF_ReadOnly if (f->Flags & ZCC_FuncConstUnsafe) varflags |= VARF_ReadOnly; if (mVersion >= MakeVersion(2, 4, 0)) diff --git a/src/version.h b/src/version.h index 58e7927a1..7a04c37dc 100644 --- a/src/version.h +++ b/src/version.h @@ -50,12 +50,12 @@ const char *GetVersionString(); // These are for content versioning. #define VER_MAJOR 4 #define VER_MINOR 15 -#define VER_REVISION 0 +#define VER_REVISION 1 // This should always refer to the GZDoom version a derived port is based on and not reflect the derived port's version number! #define ENG_MAJOR 4 #define ENG_MINOR 15 -#define ENG_REVISION 0 +#define ENG_REVISION 1 // Version identifier for network games. // Bump it every time you do a release unless you're certain you diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt index b3565f8a7..54939e70a 100644 --- a/wadsrc/static/zscript.txt +++ b/wadsrc/static/zscript.txt @@ -1,4 +1,4 @@ -version "4.15" +version "4.15.1" // Generic engine code #include "zscript/engine/base.zs" diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 898185701..c27dbf8ba 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -73,7 +73,7 @@ class ViewPosition native native readonly int Flags; } -class Behavior native play abstract version("4.15") +class Behavior native play abstract version("4.15.1") { native readonly Actor Owner; native readonly LevelLocals Level; @@ -84,7 +84,7 @@ class Behavior native play abstract version("4.15") virtual void Tick() {} } -class BehaviorIterator native abstract final version("4.15") +class BehaviorIterator native abstract final version("4.15.1") { native static BehaviorIterator CreateFrom(Actor mobj, class type = null); native static BehaviorIterator Create(class type = null, class ownerType = null); @@ -521,12 +521,12 @@ class Actor : Thinker native return sin(fb * (180./32)) * 8; } - native version("4.15") clearscope Behavior FindBehavior(class type) const; - native version("4.15") bool RemoveBehavior(class type); - native version("4.15") Behavior AddBehavior(class type); - native version("4.15") void TickBehaviors(); - native version("4.15") void ClearBehaviors(class type = null); - native version("4.15") void MoveBehaviors(Actor from); + native version("4.15.1") clearscope Behavior FindBehavior(class type) const; + native version("4.15.1") bool RemoveBehavior(class type); + native version("4.15.1") Behavior AddBehavior(class type); + native version("4.15.1") void TickBehaviors(); + native version("4.15.1") void ClearBehaviors(class type = null); + native version("4.15.1") void MoveBehaviors(Actor from); native clearscope bool isFrozen() const; virtual native void BeginPlay(); diff --git a/wadsrc/static/zscript/engine/dictionary.zs b/wadsrc/static/zscript/engine/dictionary.zs index 6c778492f..322d62b18 100644 --- a/wadsrc/static/zscript/engine/dictionary.zs +++ b/wadsrc/static/zscript/engine/dictionary.zs @@ -5,7 +5,7 @@ * * @note keys are case-sensitive. */ -class Dictionary deprecated("4.15", "Use Map instead") +class Dictionary deprecated("4.15.1", "Use Map instead") { native static Dictionary Create(); @@ -38,7 +38,7 @@ class Dictionary deprecated("4.15", "Use Map instead") * DictionaryIterator is not serializable. To make DictionaryIterator a class * member, use `transient` keyword. */ -class DictionaryIterator deprecated("4.15", "Use Map instead") +class DictionaryIterator deprecated("4.15.1", "Use Map instead") { native static DictionaryIterator Create(Dictionary dict); From 0d963166f1756883b4dc9d44205b9e7a4c53b0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 4 Mar 2025 08:36:13 -0300 Subject: [PATCH 035/384] Allow `>>` in parser for aggregate types makes stuff like Array> parse properly (bit hacky but can't do much better without restructuring the scanner/lexer) --- src/common/engine/sc_man.h | 5 ++ src/common/scripting/frontend/zcc-parse.lemon | 84 +++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/common/engine/sc_man.h b/src/common/engine/sc_man.h index 5873523a1..d90da6474 100644 --- a/src/common/engine/sc_man.h +++ b/src/common/engine/sc_man.h @@ -85,6 +85,11 @@ public: ParseVersion = ver; } + bool CheckParseVersion(VersionInfo ver) + { + return ParseVersion >= ver; + } + void SetCMode(bool cmode); void SetNoOctals(bool cmode) { NoOctals = cmode; } void SetNoFatalErrors(bool cmode) { NoFatalErrors = cmode; } diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index eed755355..85e3a762b 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -980,6 +980,7 @@ type_name(X) ::= DOT dottable_id(A). /* Aggregate types */ %type aggregate_type {ZCC_Type *} +%type aggregate_type_pre {ZCC_Type *} %type type {ZCC_Type *} %type type_list {ZCC_Type *} %type type_list_or_void {ZCC_Type *} @@ -988,30 +989,92 @@ type_name(X) ::= DOT dottable_id(A). %type array_size{ZCC_Expression *} %type array_size_expr{ZCC_Expression *} -aggregate_type(X) ::= MAP(T) LT type_or_array(A) COMMA type_or_array(B) GT. /* ZSMap */ +aggregate_type_pre(X) ::= MAP(T) LT type_or_array(A) COMMA type_or_array(B). /* ZSMap */ { NEW_AST_NODE(MapType,map,T); map->KeyType = A; map->ValueType = B; X = map; + X->ArraySize = NULL; } -aggregate_type(X) ::= MAPITERATOR(T) LT type_or_array(A) COMMA type_or_array(B) GT. /* ZSMapIterator */ +aggregate_type_pre(X) ::= MAPITERATOR(T) LT type_or_array(A) COMMA type_or_array(B). /* ZSMapIterator */ { NEW_AST_NODE(MapIteratorType,map_it,T); map_it->KeyType = A; map_it->ValueType = B; X = map_it; + X->ArraySize = NULL; } -aggregate_type(X) ::= ARRAY(T) LT type_or_array(A) GT. /* TArray */ +aggregate_type_pre(X) ::= ARRAY(T) LT type_or_array(A). /* TArray */ { NEW_AST_NODE(DynArrayType,arr,T); arr->ElementType = A; X = arr; + X->ArraySize = NULL; } -aggregate_type(X) ::= func_ptr_type(A). { X = A; /*X-overwrites-A*/ } +aggregate_type_pre(X) ::= func_ptr_type(A). { X = A; /*X-overwrites-A*/ X->ArraySize = NULL; } + +aggregate_type_pre(X) ::= CLASS(T) LT dottable_id(A). /* class */ +{ + NEW_AST_NODE(ClassType,cls,T); + cls->Restriction = A; + X = cls; + X->ArraySize = NULL; +} + +aggregate_type(X) ::= aggregate_type_pre(A) GT. { X = A; X->ArraySize = NULL; } + +aggregate_type(X) ::= MAP(T) LT type_or_array(A) COMMA aggregate_type_pre(B) RSH. /* ZSMap> */ +{ + if(!stat->sc->CheckParseVersion(MakeVersion(4, 15, 1))) + { + stat->sc->ScriptMessage(">> without space in parametrized types only supported for zscript >= 4.15.1"); + FScriptPosition::ErrorCounter++; + } + NEW_AST_NODE(MapType,map,T); + map->KeyType = A; + map->ValueType = B; + X = map; + X->ArraySize = NULL; +} + +aggregate_type(X) ::= MAPITERATOR(T) LT type_or_array(A) COMMA aggregate_type_pre(B) RSH. /* ZSMapIterator> */ +{ + if(!stat->sc->CheckParseVersion(MakeVersion(4, 15, 1))) + { + stat->sc->ScriptMessage(">> without space in parametrized types only supported for zscript >= 4.15.1"); + FScriptPosition::ErrorCounter++; + } + NEW_AST_NODE(MapIteratorType,map_it,T); + map_it->KeyType = A; + map_it->ValueType = B; + X = map_it; + X->ArraySize = NULL; +} + +aggregate_type(X) ::= ARRAY(T) LT aggregate_type_pre(A) RSH. /* TArray> */ +{ + if(!stat->sc->CheckParseVersion(MakeVersion(4, 15, 1))) + { + stat->sc->ScriptMessage(">> without space in parametrized types only supported for zscript >= 4.15.1"); + FScriptPosition::ErrorCounter++; + } + NEW_AST_NODE(DynArrayType,arr,T); + arr->ElementType = A; + X = arr; + X->ArraySize = NULL; +} + +aggregate_type(X) ::= CLASS(T). /* class */ +{ + NEW_AST_NODE(ClassType,cls,T); + cls->Restriction = NULL; + X = cls; + X->ArraySize = NULL; +} %type func_ptr_type {ZCC_FuncPtrType *} %type func_ptr_params {ZCC_FuncPtrParamDecl *} @@ -1025,7 +1088,7 @@ fn_ptr_flag(X) ::= CLEARSCOPE. { X.Int = ZCC_ClearScope; } //fn_ptr_flag(X) ::= VIRTUALSCOPE. { X.Int = ZCC_VirtualScope; } //virtual scope not allowed -func_ptr_type(X) ::= FNTYPE(T) LT fn_ptr_flag(F) type_list_or_void(A) LPAREN func_ptr_params(B) RPAREN GT. /* Function<...(...)> */ +func_ptr_type(X) ::= FNTYPE(T) LT fn_ptr_flag(F) type_list_or_void(A) LPAREN func_ptr_params(B) RPAREN. /* Function<...(...)> */ { NEW_AST_NODE(FuncPtrType,fn_ptr,T); fn_ptr->RetType = A; @@ -1034,7 +1097,7 @@ func_ptr_type(X) ::= FNTYPE(T) LT fn_ptr_flag(F) type_list_or_void(A) LPAREN fun X = fn_ptr; } -func_ptr_type(X) ::= FNTYPE(T) LT VOID GT. /* Function */ +func_ptr_type(X) ::= FNTYPE(T) LT VOID. /* Function */ { NEW_AST_NODE(FuncPtrType,fn_ptr,T); fn_ptr->RetType = nullptr; @@ -1076,15 +1139,6 @@ func_ptr_param(X) ::= func_param_flags(A) type(B) AND. X = parm; } -aggregate_type(X) ::= CLASS(T) class_restrictor(A). /* class */ -{ - NEW_AST_NODE(ClassType,cls,T); - cls->Restriction = A; - X = cls; -} -class_restrictor(X) ::= . { X = NULL; } -class_restrictor(X) ::= LT dottable_id(A) GT. { X = A; /*X-overwrites-A*/ } - type(X) ::= type_name(A). { X = A; /*X-overwrites-A*/ X->ArraySize = NULL; } type(X) ::= aggregate_type(A). { X = A; /*X-overwrites-A*/ X->ArraySize = NULL; } From 909d211137b0cc7a09423c560069fccd1af992a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 4 Mar 2025 08:59:03 -0300 Subject: [PATCH 036/384] rename vm internal structs to make room for compilation-unit-internal structs/classes --- src/common/scripting/core/types.h | 2 +- src/common/scripting/frontend/zcc-parse.lemon | 12 ++-- src/common/scripting/frontend/zcc_compile.cpp | 6 +- src/common/scripting/frontend/zcc_parser.h | 1 + wadsrc/static/zscript/engine/base.zs | 6 +- wadsrc/static/zscript/engine/dynarrays.zs | 16 ++--- wadsrc/static/zscript/engine/maps.zs | 64 +++++++++---------- 7 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/common/scripting/core/types.h b/src/common/scripting/core/types.h index 380ae88d4..e68612fcf 100644 --- a/src/common/scripting/core/types.h +++ b/src/common/scripting/core/types.h @@ -110,7 +110,7 @@ public: EScopeFlags ScopeFlags = (EScopeFlags)0; bool SizeKnown = true; - bool TypeInternal = false; + bool VMInternalStruct = false; bool TypeDeprecated = false; // mVersion is deprecation version, not minimum version FString mDeprecationMessage; diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index 85e3a762b..2affe274b 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -442,12 +442,12 @@ struct_def(X) ::= EXTEND STRUCT(T) IDENTIFIER(A) LBRACE opt_struct_body(B) RBRAC } %type struct_flags{ClassFlagsBlock} -struct_flags(X) ::= . { X.Flags = 0; X.Version = {0, 0}; X.DeprecationMessage = NULL; } -struct_flags(X) ::= struct_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } -struct_flags(X) ::= struct_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } -struct_flags(X) ::= struct_flags(A) CLEARSCOPE. { X.Flags = A.Flags | ZCC_ClearScope; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } -struct_flags(X) ::= struct_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } -struct_flags(X) ::= struct_flags(A) INTERNAL. { X.Flags = A.Flags | ZCC_Internal; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= . { X.Flags = 0; X.Version = {0, 0}; X.DeprecationMessage = NULL; } +struct_flags(X) ::= struct_flags(A) UI. { X.Flags = A.Flags | ZCC_UIFlag; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) PLAY. { X.Flags = A.Flags | ZCC_Play; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) CLEARSCOPE. { X.Flags = A.Flags | ZCC_ClearScope; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) NATIVE. { X.Flags = A.Flags | ZCC_Native; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } +struct_flags(X) ::= struct_flags(A) UNSAFE LPAREN INTERNAL RPAREN. { X.Flags = A.Flags | ZCC_VMInternalStruct; X.Version = A.Version; X.DeprecationMessage = A.DeprecationMessage; } struct_flags(X) ::= struct_flags(A) VERSION LPAREN STRCONST(C) RPAREN. { X.Flags = A.Flags | ZCC_Version; X.Version = C.String->GetChars(); X.DeprecationMessage = A.DeprecationMessage; } struct_flags(X) ::= struct_flags(A) DEPRECATED LPAREN STRCONST(C) opt_deprecation_message(D) RPAREN. diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index 492498f4b..006219d0e 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -758,11 +758,11 @@ void ZCCCompiler::CreateStructTypes() s->strct->Type->mDeprecationMessage = s->strct->DeprecationMessage ? *s->strct->DeprecationMessage : ""; } - if (s->strct->Flags & ZCC_Internal) + if (s->strct->Flags & ZCC_VMInternalStruct) { if(fileSystem.GetFileContainer(Lump) == 0) { - s->strct->Type->TypeInternal = true; + s->strct->Type->VMInternalStruct = true; } else { @@ -2214,7 +2214,7 @@ PType *ZCCCompiler::ResolveUserType(PType *outertype, ZCC_BasicType *type, ZCC_I } //only allow references to internal types inside internal types - if (ptype->TypeInternal && !outertype->TypeInternal) + if (ptype->VMInternalStruct && !outertype->VMInternalStruct) { Error(type, "Type %s not accessible", FName(type->UserType->Id).GetChars()); return TypeError; diff --git a/src/common/scripting/frontend/zcc_parser.h b/src/common/scripting/frontend/zcc_parser.h index d12d7b87e..7596553a5 100644 --- a/src/common/scripting/frontend/zcc_parser.h +++ b/src/common/scripting/frontend/zcc_parser.h @@ -67,6 +67,7 @@ enum ZCC_Sealed = 1 << 23, ZCC_FuncConstUnsafe = 1 << 24, ZCC_UnsafeClearScope = 1 << 25, + ZCC_VMInternalStruct = 1 << 26, }; // Function parameter modifiers diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index 14cedaed4..33c14eef6 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -189,7 +189,7 @@ struct Vector3 } */ -struct _ native internal // These are the global variables, the struct is only here to avoid extending the parser for this. +struct _ native unsafe(internal) // These are the global variables, the struct is only here to avoid extending the parser for this. { native readonly Array AllClasses; native internal readonly Map AllServices; @@ -903,7 +903,7 @@ enum EmptyTokenType // Although String is a builtin type, this is a convenient way to attach methods to it. // All of these methods are available on strings -struct StringStruct native internal +struct StringStruct native unsafe(internal) { native static vararg String Format(String fmt, ...); native vararg void AppendFormat(String fmt, ...); @@ -952,7 +952,7 @@ struct Translation version("2.4") } // Convenient way to attach functions to Quat -struct QuatStruct native internal +struct QuatStruct native unsafe(internal) { native static Quat SLerp(Quat from, Quat to, double t); native static Quat NLerp(Quat from, Quat to, double t); diff --git a/wadsrc/static/zscript/engine/dynarrays.zs b/wadsrc/static/zscript/engine/dynarrays.zs index 28f36b313..d21c70e1a 100644 --- a/wadsrc/static/zscript/engine/dynarrays.zs +++ b/wadsrc/static/zscript/engine/dynarrays.zs @@ -1,7 +1,7 @@ // The VM uses 7 integral data types, so for dynamic array support we need one specific set of functions for each of these types. // Do not use these structs directly, they are incomplete and only needed to create prototypes for the needed functions. -struct DynArray_I8 native internal +struct DynArray_I8 native unsafe(internal) { native readonly int Size; @@ -21,7 +21,7 @@ struct DynArray_I8 native internal native void Clear (); } -struct DynArray_I16 native internal +struct DynArray_I16 native unsafe(internal) { native readonly int Size; @@ -41,7 +41,7 @@ struct DynArray_I16 native internal native void Clear (); } -struct DynArray_I32 native internal +struct DynArray_I32 native unsafe(internal) { native readonly int Size; @@ -62,7 +62,7 @@ struct DynArray_I32 native internal native void Clear (); } -struct DynArray_F32 native internal +struct DynArray_F32 native unsafe(internal) { native readonly int Size; @@ -82,7 +82,7 @@ struct DynArray_F32 native internal native void Clear (); } -struct DynArray_F64 native internal +struct DynArray_F64 native unsafe(internal) { native readonly int Size; @@ -102,7 +102,7 @@ struct DynArray_F64 native internal native void Clear (); } -struct DynArray_Ptr native internal +struct DynArray_Ptr native unsafe(internal) { native readonly int Size; @@ -122,7 +122,7 @@ struct DynArray_Ptr native internal native void Clear (); } -struct DynArray_Obj native internal +struct DynArray_Obj native unsafe(internal) { native readonly int Size; @@ -142,7 +142,7 @@ struct DynArray_Obj native internal native void Clear (); } -struct DynArray_String native internal +struct DynArray_String native unsafe(internal) { native readonly int Size; diff --git a/wadsrc/static/zscript/engine/maps.zs b/wadsrc/static/zscript/engine/maps.zs index 0e71c3b05..dbc2f4dad 100644 --- a/wadsrc/static/zscript/engine/maps.zs +++ b/wadsrc/static/zscript/engine/maps.zs @@ -1,5 +1,5 @@ -struct Map_I32_I8 native internal +struct Map_I32_I8 native unsafe(internal) { native void Copy(Map_I32_I8 other); native void Move(Map_I32_I8 other); @@ -18,7 +18,7 @@ struct Map_I32_I8 native internal native void Remove(int key); } -struct MapIterator_I32_I8 native internal +struct MapIterator_I32_I8 native unsafe(internal) { native bool Init(Map_I32_I8 other); native bool ReInit(); @@ -31,7 +31,7 @@ struct MapIterator_I32_I8 native internal native void SetValue(int value); } -struct Map_I32_I16 native internal +struct Map_I32_I16 native unsafe(internal) { native void Copy(Map_I32_I16 other); native void Move(Map_I32_I16 other); @@ -50,7 +50,7 @@ struct Map_I32_I16 native internal native void Remove(int key); } -struct MapIterator_I32_I16 native internal +struct MapIterator_I32_I16 native unsafe(internal) { native bool Init(Map_I32_I16 other); native bool ReInit(); @@ -63,7 +63,7 @@ struct MapIterator_I32_I16 native internal native void SetValue(int value); } -struct Map_I32_I32 native internal +struct Map_I32_I32 native unsafe(internal) { native void Copy(Map_I32_I32 other); native void Move(Map_I32_I32 other); @@ -82,7 +82,7 @@ struct Map_I32_I32 native internal native void Remove(int key); } -struct MapIterator_I32_I32 native internal +struct MapIterator_I32_I32 native unsafe(internal) { native bool Init(Map_I32_I32 other); native bool ReInit(); @@ -95,7 +95,7 @@ struct MapIterator_I32_I32 native internal native void SetValue(int value); } -struct Map_I32_F32 native internal +struct Map_I32_F32 native unsafe(internal) { native void Copy(Map_I32_F32 other); native void Move(Map_I32_F32 other); @@ -114,7 +114,7 @@ struct Map_I32_F32 native internal native void Remove(int key); } -struct MapIterator_I32_F32 native internal +struct MapIterator_I32_F32 native unsafe(internal) { native bool Init(Map_I32_F32 other); native bool ReInit(); @@ -127,7 +127,7 @@ struct MapIterator_I32_F32 native internal native void SetValue(double value); } -struct Map_I32_F64 native internal +struct Map_I32_F64 native unsafe(internal) { native void Copy(Map_I32_F64 other); native void Move(Map_I32_F64 other); @@ -146,7 +146,7 @@ struct Map_I32_F64 native internal native void Remove(int key); } -struct MapIterator_I32_F64 native internal +struct MapIterator_I32_F64 native unsafe(internal) { native bool Init(Map_I32_F64 other); native bool ReInit(); @@ -159,7 +159,7 @@ struct MapIterator_I32_F64 native internal native void SetValue(double value); } -struct Map_I32_Obj native internal +struct Map_I32_Obj native unsafe(internal) { native void Copy(Map_I32_Obj other); native void Move(Map_I32_Obj other); @@ -178,7 +178,7 @@ struct Map_I32_Obj native internal native void Remove(int key); } -struct MapIterator_I32_Obj native internal +struct MapIterator_I32_Obj native unsafe(internal) { native bool Init(Map_I32_Obj other); native bool ReInit(); @@ -191,7 +191,7 @@ struct MapIterator_I32_Obj native internal native void SetValue(Object value); } -struct Map_I32_Ptr native internal +struct Map_I32_Ptr native unsafe(internal) { native void Copy(Map_I32_Ptr other); native void Move(Map_I32_Ptr other); @@ -210,7 +210,7 @@ struct Map_I32_Ptr native internal native void Remove(int key); } -struct MapIterator_I32_Ptr native internal +struct MapIterator_I32_Ptr native unsafe(internal) { native bool Init(Map_I32_Ptr other); native bool Next(); @@ -220,7 +220,7 @@ struct MapIterator_I32_Ptr native internal native void SetValue(voidptr value); } -struct Map_I32_Str native internal +struct Map_I32_Str native unsafe(internal) { native void Copy(Map_I32_Str other); native void Move(Map_I32_Str other); @@ -239,7 +239,7 @@ struct Map_I32_Str native internal native void Remove(int key); } -struct MapIterator_I32_Str native internal +struct MapIterator_I32_Str native unsafe(internal) { native bool Init(Map_I32_Str other); native bool ReInit(); @@ -254,7 +254,7 @@ struct MapIterator_I32_Str native internal // --------------- -struct Map_Str_I8 native internal +struct Map_Str_I8 native unsafe(internal) { native void Copy(Map_Str_I8 other); native void Move(Map_Str_I8 other); @@ -273,7 +273,7 @@ struct Map_Str_I8 native internal native void Remove(String key); } -struct MapIterator_Str_I8 native internal +struct MapIterator_Str_I8 native unsafe(internal) { native bool Init(Map_Str_I8 other); native bool ReInit(); @@ -286,7 +286,7 @@ struct MapIterator_Str_I8 native internal native void SetValue(int value); } -struct Map_Str_I16 native internal +struct Map_Str_I16 native unsafe(internal) { native void Copy(Map_Str_I16 other); native void Move(Map_Str_I16 other); @@ -305,7 +305,7 @@ struct Map_Str_I16 native internal native void Remove(String key); } -struct MapIterator_Str_I16 native internal +struct MapIterator_Str_I16 native unsafe(internal) { native bool Init(Map_Str_I16 other); native bool ReInit(); @@ -318,7 +318,7 @@ struct MapIterator_Str_I16 native internal native void SetValue(int value); } -struct Map_Str_I32 native internal +struct Map_Str_I32 native unsafe(internal) { native void Copy(Map_Str_I32 other); native void Move(Map_Str_I32 other); @@ -337,7 +337,7 @@ struct Map_Str_I32 native internal native void Remove(String key); } -struct MapIterator_Str_I32 native internal +struct MapIterator_Str_I32 native unsafe(internal) { native bool Init(Map_Str_I32 other); native bool ReInit(); @@ -350,7 +350,7 @@ struct MapIterator_Str_I32 native internal native void SetValue(int value); } -struct Map_Str_F32 native internal +struct Map_Str_F32 native unsafe(internal) { native void Copy(Map_Str_F32 other); native void Move(Map_Str_F32 other); @@ -369,7 +369,7 @@ struct Map_Str_F32 native internal native void Remove(String key); } -struct MapIterator_Str_F32 native internal +struct MapIterator_Str_F32 native unsafe(internal) { native bool Init(Map_Str_F32 other); native bool ReInit(); @@ -382,7 +382,7 @@ struct MapIterator_Str_F32 native internal native void SetValue(double value); } -struct Map_Str_F64 native internal +struct Map_Str_F64 native unsafe(internal) { native void Copy(Map_Str_F64 other); native void Move(Map_Str_F64 other); @@ -401,7 +401,7 @@ struct Map_Str_F64 native internal native void Remove(String key); } -struct MapIterator_Str_F64 native internal +struct MapIterator_Str_F64 native unsafe(internal) { native bool Init(Map_Str_F64 other); native bool ReInit(); @@ -414,7 +414,7 @@ struct MapIterator_Str_F64 native internal native void SetValue(double value); } -struct Map_Str_Obj native internal +struct Map_Str_Obj native unsafe(internal) { native void Copy(Map_Str_Obj other); native void Move(Map_Str_Obj other); @@ -433,7 +433,7 @@ struct Map_Str_Obj native internal native void Remove(String key); } -struct MapIterator_Str_Obj native internal +struct MapIterator_Str_Obj native unsafe(internal) { native bool Init(Map_Str_Obj other); native bool ReInit(); @@ -446,7 +446,7 @@ struct MapIterator_Str_Obj native internal native void SetValue(Object value); } -struct Map_Str_Ptr native internal +struct Map_Str_Ptr native unsafe(internal) { native void Copy(Map_Str_Ptr other); native void Move(Map_Str_Ptr other); @@ -465,7 +465,7 @@ struct Map_Str_Ptr native internal native void Remove(String key); } -struct MapIterator_Str_Ptr native internal +struct MapIterator_Str_Ptr native unsafe(internal) { native bool Init(Map_Str_Ptr other); native bool ReInit(); @@ -478,7 +478,7 @@ struct MapIterator_Str_Ptr native internal native void SetValue(voidptr value); } -struct Map_Str_Str native internal +struct Map_Str_Str native unsafe(internal) { native void Copy(Map_Str_Str other); native void Move(Map_Str_Str other); @@ -497,7 +497,7 @@ struct Map_Str_Str native internal native void Remove(String key); } -struct MapIterator_Str_Str native internal +struct MapIterator_Str_Str native unsafe(internal) { native bool Init(Map_Str_Str other); native bool ReInit(); From a09dba6b8b25c3b523b8a4cad290dc9e1f6b0923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 4 Mar 2025 10:25:02 -0300 Subject: [PATCH 037/384] fix crash if chat key is pressed during the loading screen --- src/ct_chat.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ct_chat.cpp b/src/ct_chat.cpp index a12bb9f20..b6d96aa4e 100644 --- a/src/ct_chat.cpp +++ b/src/ct_chat.cpp @@ -251,14 +251,18 @@ void CT_Drawer (void) { // [MK] allow the status bar to take over chat prompt drawing bool skip = false; - IFVIRTUALPTR(StatusBar, DBaseStatusBar, DrawChat) + + if(StatusBar) { - FString txt = ChatQueue; - VMValue params[] = { (DObject*)StatusBar, &txt }; - int rv; - VMReturn ret(&rv); - VMCall(func, params, countof(params), &ret, 1); - if (!!rv) return; + IFVIRTUALPTR(StatusBar, DBaseStatusBar, DrawChat) + { + FString txt = ChatQueue; + VMValue params[] = { (DObject*)StatusBar, &txt }; + int rv; + VMReturn ret(&rv); + VMCall(func, params, countof(params), &ret, 1); + if (!!rv) return; + } } FStringf prompt("%s ", GStrings.GetString("TXT_SAY")); @@ -269,8 +273,8 @@ void CT_Drawer (void) scalex = 1; int scale = active_con_scale(drawer); int screen_width = twod->GetWidth() / scale; - int screen_height= twod->GetHeight() / scale; - int st_y = StatusBar->GetTopOfStatusbar() / scale; + int screen_height = twod->GetHeight() / scale; + int st_y = StatusBar ? (StatusBar->GetTopOfStatusbar() / scale) : screen_height; y += ((twod->GetHeight() == viewheight && viewactive) || gamestate != GS_LEVEL) ? screen_height : st_y; From abfd91e8f18502a4caad9153d385193856b80af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 4 Mar 2025 10:37:33 -0300 Subject: [PATCH 038/384] stop game from getting stuck in chat mode if the main menu is open --- src/g_game.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/g_game.cpp b/src/g_game.cpp index 84a410fe2..786bf0629 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -1000,6 +1000,8 @@ bool G_Responder (event_t *ev) if (gameaction == ga_nothing && (demoplayback || gamestate == GS_DEMOSCREEN || gamestate == GS_TITLELEVEL)) { + if (chatmodeon) chatmodeon = 0; + const char *cmd = Bindings.GetBind (ev->data1); if (ev->type == EV_KeyDown) From 94be307225bbf9488b8519b9dfae7ed89ca827af Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sun, 24 Nov 2024 18:26:06 -0500 Subject: [PATCH 039/384] Netcode Overhaul Rewrote netcode to be more stable and functional. Packet-server mode has been restrategized and will now be the default netmode when playing with 3+ non-LAN players. TryRunTics has been cleaned up with older tick control behavior removed to account for the rewritten renderer. The main thread is now more consistent when playing online to prevent potential slow downs and lock ups. Load barriers are better accounted for to prevent spikes on level transition. Improvements to chat and lobby systems including a force start game button. Added a suite of new host options such as kicking and controlling who can pause the game. Max players increased from 8 to 16 since the new code can now handle it. Note: Demo functionality is untested. This will be rewritten at a later time alongside improvements to GZDoom's playback features (e.g. freecam mode). --- src/common/console/c_dispatch.cpp | 16 + src/common/console/c_dispatch.h | 2 + src/common/engine/i_net.cpp | 365 +- src/common/engine/i_net.h | 93 +- src/common/engine/st_start.h | 2 + src/common/platform/posix/cocoa/st_console.h | 1 + src/common/platform/posix/cocoa/st_console.mm | 5 + src/common/platform/posix/cocoa/st_start.mm | 5 + src/common/platform/posix/i_system.h | 1 - src/common/platform/posix/sdl/st_start.cpp | 6 + src/common/platform/win32/i_mainwindow.cpp | 5 + src/common/platform/win32/i_mainwindow.h | 1 + src/common/platform/win32/i_system.h | 1 - src/common/platform/win32/st_start.cpp | 5 + src/common/widgets/netstartwindow.cpp | 28 +- src/common/widgets/netstartwindow.h | 4 + src/ct_chat.cpp | 159 +- src/d_event.h | 1 + src/d_main.cpp | 35 +- src/d_net.cpp | 3978 +++++++++-------- src/d_net.h | 172 +- src/d_protocol.cpp | 577 ++- src/d_protocol.h | 25 +- src/doomdef.h | 2 +- src/g_game.cpp | 242 +- src/g_level.cpp | 18 +- src/g_statusbar/sbar.h | 4 + src/g_statusbar/shared_sbar.cpp | 25 +- src/hu_scores.cpp | 209 +- src/hu_stuff.h | 4 +- src/p_tick.cpp | 35 + src/playsim/bots/b_bot.h | 18 +- src/playsim/bots/b_func.cpp | 8 +- src/playsim/bots/b_move.cpp | 18 +- src/playsim/bots/b_think.cpp | 37 +- src/playsim/d_player.h | 2 +- src/playsim/p_mobj.cpp | 10 +- src/playsim/p_pspr.cpp | 2 +- src/playsim/p_things.cpp | 14 +- src/playsim/p_user.cpp | 34 +- src/serializer_doom.h | 2 - src/version.h | 5 - wadsrc/static/menudef.txt | 18 +- wadsrc/static/zscript/actors/player/player.zs | 8 +- wadsrc/static/zscript/constants.zs | 2 +- 45 files changed, 3327 insertions(+), 2877 deletions(-) diff --git a/src/common/console/c_dispatch.cpp b/src/common/console/c_dispatch.cpp index 8b6d738a0..c4a364a61 100644 --- a/src/common/console/c_dispatch.cpp +++ b/src/common/console/c_dispatch.cpp @@ -190,6 +190,22 @@ static const char *KeyConfCommands[] = // CODE -------------------------------------------------------------------- +bool C_IsValidInt(const char* arg, int& value, int base) +{ + char* end_read; + value = std::strtol(arg, &end_read, base); + ptrdiff_t chars_read = end_read - arg; + return chars_read == strlen(arg); +} + +bool C_IsValidFloat(const char* arg, double& value) +{ + char* end_read; + value = std::strtod(arg, &end_read); + ptrdiff_t chars_read = end_read - arg; + return chars_read == strlen(arg); +} + void C_DoCommand (const char *cmd, int keynum) { FConsoleCommand *com; diff --git a/src/common/console/c_dispatch.h b/src/common/console/c_dispatch.h index e8b2bfdf0..13b2503e5 100644 --- a/src/common/console/c_dispatch.h +++ b/src/common/console/c_dispatch.h @@ -74,6 +74,8 @@ void C_ClearDelayedCommands(); // Process a single console command. Does not handle wait. void C_DoCommand (const char *cmd, int keynum=0); +bool C_IsValidInt(const char* arg, int& value, int base = 10); +bool C_IsValidFloat(const char* arg, double& value); FExecList *C_ParseExecFile(const char *file, FExecList *source); void C_SearchForPullins(FExecList *exec, const char *file, class FCommandLine &args); diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 0a67062a0..e85e38789 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -75,6 +75,7 @@ #include "cmdlib.h" #include "printf.h" #include "i_interface.h" +#include "c_cvars.h" #include "i_net.h" @@ -104,18 +105,31 @@ typedef int SOCKET; typedef int socklen_t; #endif +constexpr int MaxPlayers = 16; // TODO: This needs to be put in some kind of unified header later +constexpr size_t MaxPasswordSize = 256u; + bool netgame, multiplayer; int consoleplayer; // i.e. myconnectindex in Build. doomcom_t doomcom; +CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE) +{ + if (strlen(self) + 1 > MaxPasswordSize) + { + self = ""; + Printf(TEXTCOLOR_RED "Password cannot be greater than 255 characters\n"); + } +} + +FClientStack NetworkClients; + // // NETWORKING // static u_short DOOMPORT = (IPPORT_USERRESERVED + 29); static SOCKET mysocket = INVALID_SOCKET; -static sockaddr_in sendaddress[MAXNETNODES]; -static uint8_t sendplayer[MAXNETNODES]; +static sockaddr_in sendaddress[MaxPlayers]; #ifdef __WIN32__ const char *neterror (void); @@ -134,12 +148,20 @@ enum PRE_ALLHEREACK, // Sent from guest to host to acknowledge PRE_ALLHEREACK receipt PRE_GO, // Sent from host to guest to continue game startup PRE_IN_PROGRESS, // Sent from host to guest if the game has already started + PRE_WRONG_PASSWORD, // Sent from host to guest if their provided password was wrong }; // Set PreGamePacket.fake to this so that the game rejects any pregame packets // after it starts. This translates to NCMD_SETUP|NCMD_MULTI. #define PRE_FAKE 0x30 +struct PreGameConnectPacket +{ + uint8_t Fake; + uint8_t Message; + char Password[MaxPasswordSize]; +}; + struct PreGamePacket { uint8_t Fake; @@ -154,17 +176,16 @@ struct PreGamePacket { uint32_t address; uint16_t port; - uint8_t player; - uint8_t pad; - } machines[MAXNETNODES]; + uint16_t pad; + } machines[MaxPlayers]; }; uint8_t TransmitBuffer[TRANSMIT_SIZE]; FString GetPlayerName(int num) { - if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(sendplayer[num]); - else return FStringf("Player %d", sendplayer[num] + 1); + if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(num); + else return FStringf("Player %d", num + 1); } // @@ -200,22 +221,34 @@ void BindToLocalPort (SOCKET s, u_short port) I_FatalError ("BindToPort: %s", neterror ()); } +void I_ClearNode(int node) +{ + memset(&sendaddress[node], 0, sizeof(sendaddress[node])); +} + +static bool I_ShouldStartNetGame() +{ + if (doomcom.consoleplayer != 0) + return false; + + return StartWindow->ShouldStartNet(); +} + int FindNode (const sockaddr_in *address) { - int i; + int i = 0; // find remote node number - for (i = 0; isin_addr.s_addr == sendaddress[i].sin_addr.s_addr && address->sin_port == sendaddress[i].sin_port) + { break; - - if (i == doomcom.numnodes) - { - // packet is not from one of the players (new game broadcast?) - i = -1; + } } - return i; + + return (i == doomcom.numplayers) ? -1 : i; } // @@ -249,8 +282,8 @@ void PacketSend (void) { // Printf("send %lu/%d\n", size, doomcom.datalength); c = sendto(mysocket, (char *)TransmitBuffer, size, - 0, (sockaddr *)&sendaddress[doomcom.remotenode], - sizeof(sendaddress[doomcom.remotenode])); + 0, (sockaddr *)&sendaddress[doomcom.remoteplayer], + sizeof(sendaddress[doomcom.remoteplayer])); } else { @@ -262,8 +295,8 @@ void PacketSend (void) { // Printf("send %d\n", doomcom.datalength); c = sendto(mysocket, (char *)doomcom.data, doomcom.datalength, - 0, (sockaddr *)&sendaddress[doomcom.remotenode], - sizeof(sendaddress[doomcom.remotenode])); + 0, (sockaddr *)&sendaddress[doomcom.remoteplayer], + sizeof(sendaddress[doomcom.remoteplayer])); } } // if (c == -1) @@ -315,7 +348,7 @@ void PacketGet (void) } else { - doomcom.remotenode = -1; // no packet + doomcom.remoteplayer = -1; // no packet return; } } @@ -331,7 +364,7 @@ void PacketGet (void) { Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars()); // Pretend no packet - doomcom.remotenode = -1; + doomcom.remoteplayer = -1; return; } c = msgsize + 1; @@ -350,11 +383,11 @@ void PacketGet (void) uint8_t msg[] = { PRE_FAKE, PRE_IN_PROGRESS }; PreSend(msg, 2, &fromaddress); } - doomcom.remotenode = -1; + doomcom.remoteplayer = -1; return; } - doomcom.remotenode = node; + doomcom.remoteplayer = node; doomcom.datalength = (short)c; } @@ -373,22 +406,6 @@ sockaddr_in *PreGet (void *buffer, int bufferlen, bool noabort) int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK || (noabort && err == WSAECONNRESET)) return NULL; // no packet - - if (doomcom.consoleplayer == 0) - { - int node = FindNode(&fromaddress); - I_NetMessage("Got unexpected disconnect."); - doomcom.numnodes--; - for (; node < doomcom.numnodes; ++node) - sendaddress[node] = sendaddress[node + 1]; - - // Let remaining guests know that somebody left. - SendConAck(doomcom.numnodes, doomcom.numplayers); - } - else - { - I_NetError("The host disbanded the game unexpectedly"); - } } return &fromaddress; } @@ -439,7 +456,7 @@ void BuildAddress (sockaddr_in *address, const char *name) if (!isnamed) { address->sin_addr.s_addr = inet_addr (target.GetChars()); - Printf ("Node number %d, address %s\n", doomcom.numnodes, target.GetChars()); + Printf ("Node number %d, address %s\n", doomcom.numplayers, target.GetChars()); } else { @@ -448,7 +465,7 @@ void BuildAddress (sockaddr_in *address, const char *name) I_FatalError ("gethostbyname: couldn't find %s\n%s", target.GetChars(), neterror()); address->sin_addr.s_addr = *(int *)hostentry->h_addr_list[0]; Printf ("Node number %d, hostname %s\n", - doomcom.numnodes, hostentry->h_name); + doomcom.numplayers, hostentry->h_name); } } @@ -489,17 +506,17 @@ void StartNetwork (bool autoPort) #endif } -void SendAbort (void) +void SendAbort (int connected) { uint8_t dis[2] = { PRE_FAKE, PRE_DISCONNECT }; int i, j; - if (doomcom.numnodes > 1) + if (connected > 1) { - if (consoleplayer == 0) + if (doomcom.consoleplayer == 0) { // The host needs to let everyone know - for (i = 1; i < doomcom.numnodes; ++i) + for (i = 1; i < connected; ++i) { for (j = 4; j > 0; --j) { @@ -512,7 +529,7 @@ void SendAbort (void) // Guests only need to let the host know. for (i = 4; i > 0; --i) { - PreSend (dis, 2, &sendaddress[1]); + PreSend (dis, 2, &sendaddress[0]); } } } @@ -526,17 +543,17 @@ void SendConAck (int num_connected, int num_needed) packet.Message = PRE_CONACK; packet.NumNodes = num_needed; packet.NumPresent = num_connected; - for (int node = 1; node < doomcom.numnodes; ++node) + for (int node = 1; node < num_connected; ++node) { PreSend (&packet, 4, &sendaddress[node]); } - I_NetProgress (doomcom.numnodes); + I_NetProgress (num_connected); } bool Host_CheckForConnects (void *userdata) { - PreGamePacket packet; - int numplayers = (int)(intptr_t)userdata; + PreGameConnectPacket packet; + int* connectedPlayers = (int*)userdata; sockaddr_in *from; int node; @@ -550,7 +567,7 @@ bool Host_CheckForConnects (void *userdata) { case PRE_CONNECT: node = FindNode (from); - if (doomcom.numnodes == numplayers) + if (doomcom.numplayers == *connectedPlayers) { if (node == -1) { @@ -566,13 +583,21 @@ bool Host_CheckForConnects (void *userdata) { if (node == -1) { - node = doomcom.numnodes++; + if (strlen(net_password) > 0 && strcmp(net_password, packet.Password)) + { + packet.Message = PRE_WRONG_PASSWORD; + PreSend(&packet, 2, from); + break; + } + + node = *connectedPlayers; + ++*connectedPlayers; sendaddress[node] = *from; I_NetMessage ("Got connect from node %d.", node); } // Let the new guest (and everyone else) know we got their message. - SendConAck (doomcom.numnodes, numplayers); + SendConAck (*connectedPlayers, doomcom.numplayers); } break; @@ -580,27 +605,25 @@ bool Host_CheckForConnects (void *userdata) node = FindNode (from); if (node >= 0) { + I_ClearNode(node); I_NetMessage ("Got disconnect from node %d.", node); - doomcom.numnodes--; - while (node < doomcom.numnodes) + --*connectedPlayers; + while (node < *connectedPlayers) { sendaddress[node] = sendaddress[node+1]; - node++; + ++node; } // Let remaining guests know that somebody left. - SendConAck (doomcom.numnodes, numplayers); + SendConAck (*connectedPlayers, doomcom.numplayers); } break; - - case PRE_KEEPALIVE: - break; } } - if (doomcom.numnodes < numplayers) + if (*connectedPlayers < doomcom.numplayers && !I_ShouldStartNetGame()) { // Send message to everyone as a keepalive - SendConAck(doomcom.numnodes, numplayers); + SendConAck(*connectedPlayers, doomcom.numplayers); return false; } @@ -614,53 +637,53 @@ bool Host_CheckForConnects (void *userdata) node = FindNode (from); if (node >= 0) { - doomcom.numnodes--; - while (node < doomcom.numnodes) + I_ClearNode(node); + --*connectedPlayers; + while (node < *connectedPlayers) { sendaddress[node] = sendaddress[node+1]; - node++; + ++node; } // Let remaining guests know that somebody left. - SendConAck (doomcom.numnodes, numplayers); + SendConAck (*connectedPlayers, doomcom.numplayers); } break; } } - return doomcom.numnodes >= numplayers; + + // TODO: This will need a much better solution later. + if (I_ShouldStartNetGame()) + doomcom.numplayers = *connectedPlayers; + + return *connectedPlayers >= doomcom.numplayers; } bool Host_SendAllHere (void *userdata) { - int *gotack = (int *)userdata; // ackcount is at gotack[MAXNETNODES] + int* gotack = (int*)userdata; + const int mask = (1 << doomcom.numplayers) - 1; PreGamePacket packet; int node; sockaddr_in *from; - // Send out address information to all guests. Guests that have already - // acknowledged receipt effectively get just a heartbeat packet. + // Send out address information to all guests. This is will be the final order + // of the send addresses so each guest will need to rearrange their own in order + // to keep node -> player numbers synchronized. packet.Fake = PRE_FAKE; packet.Message = PRE_ALLHERE; - for (node = 1; node < doomcom.numnodes; node++) + for (node = 1; node < doomcom.numplayers; node++) { int machine, spot = 0; packet.ConsoleNum = node; - if (!gotack[node]) + if (!((*gotack) & (1 << node))) { - for (spot = 0, machine = 1; machine < doomcom.numnodes; machine++) + for (spot = 0; spot < doomcom.numplayers; spot++) { - if (node != machine) - { - packet.machines[spot].address = sendaddress[machine].sin_addr.s_addr; - packet.machines[spot].port = sendaddress[machine].sin_port; - packet.machines[spot].player = node; - - spot++; // fixes problem of new address replacing existing address in - // array; it's supposed to increment the index before getting - // and storing in the packet the next address. - } + packet.machines[spot].address = sendaddress[spot].sin_addr.s_addr; + packet.machines[spot].port = sendaddress[spot].sin_port; } - packet.NumNodes = doomcom.numnodes - 2; + packet.NumNodes = doomcom.numplayers; } else { @@ -672,23 +695,36 @@ bool Host_SendAllHere (void *userdata) // Check for replies. while ( (from = PreGet (&packet, sizeof(packet), false)) ) { - if (packet.Fake == PRE_FAKE && packet.Message == PRE_ALLHEREACK) + if (packet.Fake != PRE_FAKE) + continue; + + if (packet.Message == PRE_ALLHEREACK) { node = FindNode (from); if (node >= 0) + *gotack |= 1 << node; + } + else if (packet.Message == PRE_CONNECT) + { + // If someone is still trying to connect, let them know it's either too + // late or that they need to move on to the next stage. + node = FindNode(from); + if (node == -1) { - if (!gotack[node]) - { - gotack[node] = true; - gotack[MAXNETNODES]++; - } + packet.Message = PRE_ALLFULL; + PreSend(&packet, 2, from); + } + else + { + packet.Message = PRE_CONACK; + packet.NumNodes = packet.NumPresent = doomcom.numplayers; + PreSend(&packet, 4, from); } - PreSend (&packet, 2, from); } } // If everybody has replied, then this loop can end. - return gotack[MAXNETNODES] == doomcom.numnodes - 1; + return ((*gotack) & mask) == mask; } bool HostGame (int i) @@ -696,24 +732,24 @@ bool HostGame (int i) PreGamePacket packet; int numplayers; int node; - int gotack[MAXNETNODES+1]; if ((i == Args->NumArgs() - 1) || !(numplayers = atoi (Args->GetArg(i+1)))) { // No player count specified, assume 2 numplayers = 2; } - if (numplayers > MAXNETNODES) + if (numplayers > MaxPlayers) { - I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MAXNETNODES); + I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MaxPlayers); } if (numplayers == 1) { // Special case: Only 1 player, so don't bother starting the network + NetworkClients += 0; netgame = false; multiplayer = true; doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes = 1; + doomcom.numplayers = 1; doomcom.consoleplayer = 0; return true; } @@ -723,27 +759,41 @@ bool HostGame (int i) // [JC] - this computer is starting the game, therefore it should // be the Net Arbitrator. doomcom.consoleplayer = 0; - Printf ("Console player number: %d\n", doomcom.consoleplayer); - - doomcom.numnodes = 1; + doomcom.numplayers = numplayers; I_NetInit ("Hosting game", numplayers); - // Wait for numplayers-1 different connections - if (!I_NetLoop (Host_CheckForConnects, (void *)(intptr_t)numplayers)) + // Wait for the lobby to be full. + int connectedPlayers = 1; + if (!I_NetLoop (Host_CheckForConnects, (void *)&connectedPlayers)) { - SendAbort(); + SendAbort(connectedPlayers); + throw CExitEvent(0); return false; } + // If the player force started with only themselves in the lobby, start the game + // immediately. + if (doomcom.numplayers <= 1) + { + NetworkClients += 0; + netgame = false; + multiplayer = true; + doomcom.id = DOOMCOM_ID; + doomcom.numplayers = 1; + I_NetDone(); + return true; + } + // Now inform everyone of all machines involved in the game - memset (gotack, 0, sizeof(gotack)); + int gotack = 1; I_NetMessage ("Sending all here."); I_NetInit ("Done waiting", 1); - if (!I_NetLoop (Host_SendAllHere, (void *)gotack)) + if (!I_NetLoop (Host_SendAllHere, (void *)&gotack)) { - SendAbort(); + SendAbort(doomcom.numplayers); + throw CExitEvent(0); return false; } @@ -751,7 +801,7 @@ bool HostGame (int i) I_NetMessage ("Go"); packet.Fake = PRE_FAKE; packet.Message = PRE_GO; - for (node = 1; node < doomcom.numnodes; node++) + for (node = 1; node < doomcom.numplayers; node++) { // If we send the packets eight times to each guest, // hopefully at least one of them will get through. @@ -761,16 +811,10 @@ bool HostGame (int i) } } - I_NetMessage ("Total players: %d", doomcom.numnodes); + I_NetMessage ("Total players: %d", doomcom.numplayers); doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes; - // On the host, each player's number is the same as its node number - for (i = 0; i < doomcom.numnodes; ++i) - { - sendplayer[i] = i; - } return true; } @@ -782,16 +826,18 @@ bool Guest_ContactHost (void *userdata) { sockaddr_in *from; PreGamePacket packet; + PreGameConnectPacket sendPacket; // Let the host know we are here. - packet.Fake = PRE_FAKE; - packet.Message = PRE_CONNECT; - PreSend (&packet, 2, &sendaddress[1]); + sendPacket.Fake = PRE_FAKE; + sendPacket.Message = PRE_CONNECT; + memcpy(sendPacket.Password, net_password, strlen(net_password) + 1); + PreSend (&sendPacket, sizeof(sendPacket), &sendaddress[0]); // Listen for a reply. while ( (from = PreGet (&packet, sizeof(packet), true)) ) { - if (packet.Fake == PRE_FAKE && FindNode(from) == 1) + if (packet.Fake == PRE_FAKE && !FindNode(from)) { if (packet.Message == PRE_CONACK) { @@ -812,6 +858,10 @@ bool Guest_ContactHost (void *userdata) { I_NetError("The game was already started."); } + else if (packet.Message == PRE_WRONG_PASSWORD) + { + I_NetError("Invalid password."); + } } } @@ -828,7 +878,7 @@ bool Guest_WaitForOthers (void *userdata) while ( (from = PreGet (&packet, sizeof(packet), false)) ) { - if (packet.Fake != PRE_FAKE || FindNode(from) != 1) + if (packet.Fake != PRE_FAKE || FindNode(from)) { continue; } @@ -839,31 +889,35 @@ bool Guest_WaitForOthers (void *userdata) break; case PRE_ALLHERE: - if (doomcom.numnodes == 2) + if (doomcom.consoleplayer == -1) { - int node; - - doomcom.numnodes = packet.NumNodes + 2; - sendplayer[0] = packet.ConsoleNum; // My player number + doomcom.numplayers = packet.NumNodes; doomcom.consoleplayer = packet.ConsoleNum; + // Don't use the address sent from the host for our own machine. + sendaddress[doomcom.consoleplayer] = sendaddress[1]; + I_NetMessage ("Console player number: %d", doomcom.consoleplayer); - for (node = 0; node < packet.NumNodes; node++) + + for (int node = 1; node < doomcom.numplayers; ++node) { - sendaddress[node+2].sin_addr.s_addr = packet.machines[node].address; - sendaddress[node+2].sin_port = packet.machines[node].port; - sendplayer[node+2] = packet.machines[node].player; + if (node == doomcom.consoleplayer) + continue; + + sendaddress[node].sin_addr.s_addr = packet.machines[node].address; + sendaddress[node].sin_port = packet.machines[node].port; // [JC] - fixes problem of games not starting due to // no address family being assigned to nodes stored in // sendaddress[] from the All Here packet. - sendaddress[node+2].sin_family = AF_INET; + sendaddress[node].sin_family = AF_INET; } + + I_NetMessage("Received All Here, sending ACK."); } - I_NetMessage ("Received All Here, sending ACK."); packet.Fake = PRE_FAKE; packet.Message = PRE_ALLHEREACK; - PreSend (&packet, 2, &sendaddress[1]); + PreSend (&packet, 2, &sendaddress[0]); break; case PRE_GO: @@ -876,10 +930,6 @@ bool Guest_WaitForOthers (void *userdata) } } - packet.Fake = PRE_FAKE; - packet.Message = PRE_KEEPALIVE; - PreSend(&packet, 2, &sendaddress[1]); - return false; } @@ -892,33 +942,32 @@ bool JoinGame (int i) StartNetwork (true); - // Host is always node 1 - BuildAddress (&sendaddress[1], Args->GetArg(i+1)); - sendplayer[1] = 0; - doomcom.numnodes = 2; + // Host is always node 0 + BuildAddress (&sendaddress[0], Args->GetArg(i+1)); + doomcom.numplayers = 2; doomcom.consoleplayer = -1; - // Let host know we are here I_NetInit ("Contacting host", 0); - if (!I_NetLoop (Guest_ContactHost, NULL)) + if (!I_NetLoop (Guest_ContactHost, nullptr)) { - SendAbort(); + SendAbort(2); + throw CExitEvent(0); return false; } // Wait for everyone else to connect - if (!I_NetLoop (Guest_WaitForOthers, 0)) + if (!I_NetLoop (Guest_WaitForOthers, nullptr)) { - SendAbort(); + SendAbort(2); + throw CExitEvent(0); return false; } - I_NetMessage ("Total players: %d", doomcom.numnodes); + I_NetMessage ("Total players: %d", doomcom.numplayers); doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes; return true; } @@ -954,19 +1003,20 @@ static int PrivateNetOf(in_addr in) static bool NodesOnSameNetwork() { - int net1; + if (doomcom.consoleplayer != 0) + return false; - net1 = PrivateNetOf(sendaddress[1].sin_addr); + const int firstClient = PrivateNetOf(sendaddress[1].sin_addr); // Printf("net1 = %08x\n", net1); - if (net1 == 0) + if (firstClient == 0) { return false; } - for (int i = 2; i < doomcom.numnodes; ++i) + for (int i = 2; i < doomcom.numplayers; ++i) { - int net = PrivateNetOf(sendaddress[i].sin_addr); + const int net = PrivateNetOf(sendaddress[i].sin_addr); // Printf("Net[%d] = %08x\n", i, net); - if (net != net1) + if (net != firstClient) { return false; } @@ -1004,6 +1054,8 @@ int I_InitNetwork (void) Printf ("using alternate port %i\n", DOOMPORT); } + net_password = Args->CheckValue("-password"); + // parse network game options, // player 1: -host // player x: -join @@ -1018,19 +1070,22 @@ int I_InitNetwork (void) else { // single player game + NetworkClients += 0; netgame = false; multiplayer = false; doomcom.id = DOOMCOM_ID; - doomcom.numplayers = doomcom.numnodes = 1; + doomcom.ticdup = 1; + doomcom.numplayers = 1; doomcom.consoleplayer = 0; return false; } - if (doomcom.numnodes < 3) + + if (doomcom.numplayers < 3) { // Packet server mode with only two players is effectively the same as // peer-to-peer but with some slightly larger packets. return false; } - return doomcom.numnodes > 3 || !NodesOnSameNetwork(); + return !NodesOnSameNetwork(); } @@ -1070,7 +1125,7 @@ void I_NetMessage(const char* text, ...) void I_NetError(const char* error) { - doomcom.numnodes = 0; + doomcom.numplayers = 0; StartWindow->NetClose(); I_FatalError("%s", error); } diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h index 15720374d..6fbf6c51e 100644 --- a/src/common/engine/i_net.h +++ b/src/common/engine/i_net.h @@ -5,6 +5,7 @@ // Called by D_DoomMain. int I_InitNetwork (void); +void I_ClearNode(int node); void I_NetCmd (void); void I_NetMessage(const char*, ...); void I_NetError(const char* error); @@ -15,74 +16,70 @@ void I_NetDone(); enum ENetConstants { - MAXNETNODES = 8, // max computers in a game DOOMCOM_ID = 0x12345678, - BACKUPTICS = 36, // number of tics to remember - MAXTICDUP = 5, - LOCALCMDTICS =(BACKUPTICS*MAXTICDUP), + BACKUPTICS = 35 * 5, // Remember up to 5 seconds of data. + MAXTICDUP = 3, + MAXSENDTICS = 35 * 1, // Only send up to 1 second of data at a time. + LOCALCMDTICS = (BACKUPTICS*MAXTICDUP), MAX_MSGLEN = 14000, CMD_SEND = 1, CMD_GET = 2, }; -// [RH] -// New generic packet structure: -// -// Header: -// One byte with following flags. -// One byte with starttic -// One byte with master's maketic (master -> slave only!) -// If NCMD_RETRANSMIT set, one byte with retransmitfrom -// If NCMD_XTICS set, one byte with number of tics (minus 3, so theoretically up to 258 tics in one packet) -// If NCMD_QUITTERS, one byte with number of players followed by one byte with each player's consolenum -// If NCMD_MULTI, one byte with number of players followed by one byte with each player's consolenum -// - The first player's consolenum is not included in this list, because it always matches the sender -// -// For each tic: -// Two bytes with consistancy check, followed by tic data -// -// Setup packets are different, and are described just before D_ArbitrateNetStart(). - enum ENCMD { - NCMD_EXIT = 0x80, - NCMD_RETRANSMIT = 0x40, - NCMD_SETUP = 0x20, - NCMD_MULTI = 0x10, // multiple players in this packet - NCMD_QUITTERS = 0x08, // one or more players just quit (packet server only) - NCMD_COMPRESSED = 0x04, // remainder of packet is compressed + NCMD_EXIT = 0x80, // Client has left the game + NCMD_RETRANSMIT = 0x40, // + NCMD_SETUP = 0x20, // Guest is letting the host know who it is + NCMD_LEVELREADY = 0x10, // After loading a level, guests send this over to the host who then sends it back after all are received + NCMD_QUITTERS = 0x08, // Client is getting info about one or more players quitting (packet server only) + NCMD_COMPRESSED = 0x04, // Remainder of packet is compressed + NCMD_LATENCYACK = 0x02, // A latency packet was just read, so let the sender know. + NCMD_LATENCY = 0x01, // Latency packet, used for measuring RTT. - NCMD_XTICS = 0x03, // packet contains >2 tics - NCMD_2TICS = 0x02, // packet contains 2 tics - NCMD_1TICS = 0x01, // packet contains 1 tic - NCMD_0TICS = 0x00, // packet contains 0 tics + NCMD_USERINFO = NCMD_SETUP + 1, // Guest is getting another client's user info + NCMD_GAMEINFO = NCMD_SETUP + 2, // Guest is getting the state of the game from the host + NCMD_GAMEREADY = NCMD_SETUP + 3, // Host has verified the game is ready to be started }; +struct FClientStack : public TArray +{ + inline bool InGame(int i) const { return Find(i) < Size(); } + + void operator+=(const int i) + { + if (!InGame(i)) + SortedInsert(i); + } + + void operator-=(const int i) + { + Delete(Find(i)); + } +}; + +extern FClientStack NetworkClients; + // // Network packet data. // struct doomcom_t { - uint32_t id; // should be DOOMCOM_ID - int16_t intnum; // DOOM executes an int to execute commands - -// communication between DOOM and the driver - int16_t command; // CMD_SEND or CMD_GET - int16_t remotenode; // dest for send, set by get (-1 = no packet). - int16_t datalength; // bytes in data to be sent - -// info common to all nodes - int16_t numnodes; // console is always node 0. - int16_t ticdup; // 1 = no duplication, 2-5 = dup for slow nets - -// info specific to this node - int16_t consoleplayer; + // info common to all nodes + uint32_t id; // should be DOOMCOM_ID + int16_t ticdup; // 1 = no duplication, 2-3 = dup for slow nets int16_t numplayers; -// packet data to be sent - uint8_t data[MAX_MSGLEN]; + // info specific to this node + int16_t consoleplayer; + // communication between DOOM and the driver + int16_t command; // CMD_SEND or CMD_GET + int16_t remoteplayer; // dest for send, set by get (-1 = no packet). + // packet data to be sent + int16_t datalength; // bytes in data to be sent + uint8_t data[MAX_MSGLEN]; }; extern doomcom_t doomcom; diff --git a/src/common/engine/st_start.h b/src/common/engine/st_start.h index b09991d04..07bc61469 100644 --- a/src/common/engine/st_start.h +++ b/src/common/engine/st_start.h @@ -56,6 +56,7 @@ public: virtual void NetProgress(int count) {} virtual void NetDone() {} virtual void NetClose() {} + virtual bool ShouldStartNet() { return false; } virtual bool NetLoop(bool (*timer_callback)(void *), void *userdata) { return false; } virtual void AppendStatusLine(const char* status) {} virtual void LoadingStatus(const char* message, int colors) {} @@ -76,6 +77,7 @@ public: void NetMessage(const char* format, ...); // cover for printf void NetDone(); void NetClose(); + bool ShouldStartNet() override; bool NetLoop(bool (*timer_callback)(void*), void* userdata); protected: int NetMaxPos, NetCurPos; diff --git a/src/common/platform/posix/cocoa/st_console.h b/src/common/platform/posix/cocoa/st_console.h index 91f77d124..8b35989f8 100644 --- a/src/common/platform/posix/cocoa/st_console.h +++ b/src/common/platform/posix/cocoa/st_console.h @@ -67,6 +67,7 @@ public: void NetProgress(int count); void NetDone(); void NetClose(); + bool ShouldStartNet(); private: NSWindow* m_window; diff --git a/src/common/platform/posix/cocoa/st_console.mm b/src/common/platform/posix/cocoa/st_console.mm index 95d0ee11e..ce352ca72 100644 --- a/src/common/platform/posix/cocoa/st_console.mm +++ b/src/common/platform/posix/cocoa/st_console.mm @@ -536,3 +536,8 @@ void FConsoleWindow::NetClose() { // TODO: Implement this } + +bool FConsoleWindow::ShouldStartNet() +{ + return false; +} diff --git a/src/common/platform/posix/cocoa/st_start.mm b/src/common/platform/posix/cocoa/st_start.mm index 2cd73104d..948e9f207 100644 --- a/src/common/platform/posix/cocoa/st_start.mm +++ b/src/common/platform/posix/cocoa/st_start.mm @@ -115,6 +115,11 @@ void FBasicStartupScreen::NetClose() FConsoleWindow::GetInstance().NetClose(); } +bool FBasicStartupScreen::ShouldStartNet() +{ + return FConsoleWindow::GetInstance().ShouldStartNet(); +} + bool FBasicStartupScreen::NetLoop(bool (*timerCallback)(void*), void* const userData) { while (true) diff --git a/src/common/platform/posix/i_system.h b/src/common/platform/posix/i_system.h index 5573e26d3..da99cf9e0 100644 --- a/src/common/platform/posix/i_system.h +++ b/src/common/platform/posix/i_system.h @@ -13,7 +13,6 @@ #include "tarray.h" #include "zstring.h" -struct ticcmd_t; struct WadStuff; #ifndef SHARE_DIR diff --git a/src/common/platform/posix/sdl/st_start.cpp b/src/common/platform/posix/sdl/st_start.cpp index 5bd88684b..5efd4200f 100644 --- a/src/common/platform/posix/sdl/st_start.cpp +++ b/src/common/platform/posix/sdl/st_start.cpp @@ -58,6 +58,7 @@ class FTTYStartupScreen : public FStartupScreen void NetProgress(int count); void NetDone(); void NetClose(); + bool ShouldStartNet(); bool NetLoop(bool (*timer_callback)(void *), void *userdata); protected: bool DidNetInit; @@ -243,6 +244,11 @@ void FTTYStartupScreen::NetClose() // TODO: Implement this } +bool FTTYStartupScreen::ShouldStartNet() +{ + return false; +} + //=========================================================================== // // FTTYStartupScreen :: NetLoop diff --git a/src/common/platform/win32/i_mainwindow.cpp b/src/common/platform/win32/i_mainwindow.cpp index 5bdd3569c..be7d0f796 100644 --- a/src/common/platform/win32/i_mainwindow.cpp +++ b/src/common/platform/win32/i_mainwindow.cpp @@ -133,6 +133,11 @@ void MainWindow::CloseNetStartPane() NetStartWindow::NetClose(); } +bool MainWindow::ShouldStartNetGame() +{ + return NetStartWindow::ShouldStartNetGame(); +} + void MainWindow::SetNetStartProgress(int pos) { NetStartWindow::SetNetStartProgress(pos); diff --git a/src/common/platform/win32/i_mainwindow.h b/src/common/platform/win32/i_mainwindow.h index 83567135c..e2056c674 100644 --- a/src/common/platform/win32/i_mainwindow.h +++ b/src/common/platform/win32/i_mainwindow.h @@ -30,6 +30,7 @@ public: bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); void HideNetStartPane(); void CloseNetStartPane(); + bool ShouldStartNetGame(); void SetWindowTitle(const char* caption); diff --git a/src/common/platform/win32/i_system.h b/src/common/platform/win32/i_system.h index b70efb78b..f83b3f8c1 100644 --- a/src/common/platform/win32/i_system.h +++ b/src/common/platform/win32/i_system.h @@ -8,7 +8,6 @@ #include "zstring.h" #include "utf8.h" -struct ticcmd_t; struct WadStuff; // [RH] Detects the OS the game is running under. diff --git a/src/common/platform/win32/st_start.cpp b/src/common/platform/win32/st_start.cpp index fceeb3ced..6fb5a5f95 100644 --- a/src/common/platform/win32/st_start.cpp +++ b/src/common/platform/win32/st_start.cpp @@ -206,3 +206,8 @@ void FBasicStartupScreen::NetClose() { mainwindow.CloseNetStartPane(); } + +bool FBasicStartupScreen::ShouldStartNet() +{ + return mainwindow.ShouldStartNetGame(); +} diff --git a/src/common/widgets/netstartwindow.cpp b/src/common/widgets/netstartwindow.cpp index 887f9cc31..1a8845961 100644 --- a/src/common/widgets/netstartwindow.cpp +++ b/src/common/widgets/netstartwindow.cpp @@ -54,13 +54,6 @@ bool NetStartWindow::RunMessageLoop(bool (*newtimer_callback)(void*), void* newu if (Instance->CallbackException) std::rethrow_exception(Instance->CallbackException); - // Even though the comment in FBasicStartupScreen::NetLoop says we should return false, the old code actually throws an exception! - // This effectively also means the function always returns true... - if (!Instance->exitreason) - { - throw CExitEvent(0); - } - return Instance->exitreason; } @@ -70,6 +63,14 @@ void NetStartWindow::NetClose() Instance->OnClose(); } +bool NetStartWindow::ShouldStartNetGame() +{ + if (Instance != nullptr) + return Instance->shouldstart; + + return false; +} + NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) { SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); @@ -81,13 +82,16 @@ NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) MessageLabel = new TextLabel(this); ProgressLabel = new TextLabel(this); AbortButton = new PushButton(this); + ForceStartButton = new PushButton(this); MessageLabel->SetTextAlignment(TextLabelAlignment::Center); ProgressLabel->SetTextAlignment(TextLabelAlignment::Center); AbortButton->OnClick = [=]() { OnClose(); }; + ForceStartButton->OnClick = [=]() { ForceStart(); }; - AbortButton->SetText("Abort Network Game"); + AbortButton->SetText("Abort"); + ForceStartButton->SetText("Start Game"); CallbackTimer = new Timer(this); CallbackTimer->FuncExpired = [=]() { OnCallbackTimerExpired(); }; @@ -117,6 +121,11 @@ void NetStartWindow::OnClose() DisplayWindow::ExitLoop(); } +void NetStartWindow::ForceStart() +{ + shouldstart = true; +} + void NetStartWindow::OnGeometryChanged() { double w = GetWidth(); @@ -132,7 +141,8 @@ void NetStartWindow::OnGeometryChanged() y += labelheight; y = GetHeight() - 15.0 - AbortButton->GetPreferredHeight(); - AbortButton->SetFrameGeometry((w - 200.0) * 0.5, y, 200.0, AbortButton->GetPreferredHeight()); + AbortButton->SetFrameGeometry((w + 10.0) * 0.5, y, 100.0, AbortButton->GetPreferredHeight()); + ForceStartButton->SetFrameGeometry((w - 210.0) * 0.5, y, 100.0, ForceStartButton->GetPreferredHeight()); } void NetStartWindow::OnCallbackTimerExpired() diff --git a/src/common/widgets/netstartwindow.h b/src/common/widgets/netstartwindow.h index dcc9d688e..b4189af8a 100644 --- a/src/common/widgets/netstartwindow.h +++ b/src/common/widgets/netstartwindow.h @@ -15,6 +15,7 @@ public: static void SetNetStartProgress(int pos); static bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); static void NetClose(); + static bool ShouldStartNetGame(); private: NetStartWindow(); @@ -25,6 +26,7 @@ private: protected: void OnClose() override; void OnGeometryChanged() override; + virtual void ForceStart(); private: void OnCallbackTimerExpired(); @@ -32,6 +34,7 @@ private: TextLabel* MessageLabel = nullptr; TextLabel* ProgressLabel = nullptr; PushButton* AbortButton = nullptr; + PushButton* ForceStartButton = nullptr; Timer* CallbackTimer = nullptr; @@ -41,6 +44,7 @@ private: void* userdata = nullptr; bool exitreason = false; + bool shouldstart = false; std::exception_ptr CallbackException; diff --git a/src/ct_chat.cpp b/src/ct_chat.cpp index b6d96aa4e..20636e859 100644 --- a/src/ct_chat.cpp +++ b/src/ct_chat.cpp @@ -55,6 +55,7 @@ enum EXTERN_CVAR (Bool, sb_cooperative_enable) EXTERN_CVAR (Bool, sb_deathmatch_enable) EXTERN_CVAR (Bool, sb_teamdeathmatch_enable) +EXTERN_CVAR (Int, cl_showchat) int active_con_scaletext(); @@ -73,8 +74,16 @@ static void CT_BackSpace (); static void ShoveChatStr (const char *str, uint8_t who); static bool DoSubstitution (FString &out, const char *in); -static TArray ChatQueue; +constexpr int MessageLimit = 2; // Clamp the amount of messages you can send in a brief period +constexpr uint64_t MessageThrottleTime = 1000u; // Time in ms that spam messages will be tracked. +constexpr uint64_t SpamCoolDown = 3000u; +static TArray ChatQueue; +static uint64_t ChatThrottle = 0u; +static int ChatSpamCount = 0; +static uint64_t ChatCoolDown = 0u; // Spam limiter + +CVAR (Int, net_chatslowmode, 0, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR (String, chatmacro1, "I'm ready to kick butt!", CVAR_ARCHIVE) CVAR (String, chatmacro2, "I'm OK.", CVAR_ARCHIVE) CVAR (String, chatmacro3, "I'm not looking too good!", CVAR_ARCHIVE) @@ -265,7 +274,7 @@ void CT_Drawer (void) } } - FStringf prompt("%s ", GStrings.GetString("TXT_SAY")); + FStringf prompt("%s ", chatmodeon == 2 && deathmatch && teamplay ? GStrings.GetString("TXT_SAYTEAM") : GStrings.GetString("TXT_SAY")); int x, scalex, y, promptwidth; y = (viewactive || gamestate != GS_LEVEL) ? -displayfont->GetHeight()-2 : -displayfont->GetHeight() - 22; @@ -365,6 +374,24 @@ static void ShoveChatStr (const char *str, uint8_t who) if (str == NULL || str[0] == '\0') return; + if (netgame) + { + const uint64_t time = I_msTime(); + if (time >= ChatThrottle) + { + ChatSpamCount = 0; + } + else if (++ChatSpamCount >= MessageLimit) + { + ChatSpamCount = 0; + ChatCoolDown = time + SpamCoolDown; + } + + ChatThrottle = time + MessageThrottleTime; + if (net_chatslowmode > 0) + ChatCoolDown = max(time + net_chatslowmode * 1000, ChatCoolDown); + } + FString substBuff; if (str[0] == '/' && @@ -508,13 +535,40 @@ static bool DoSubstitution (FString &out, const char *in) CCMD (messagemode) { - if (menuactive == MENU_Off) + if (menuactive != MENU_Off) + return; + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) { - buttonMap.ResetButtonStates(); - chatmodeon = 1; - C_HideConsole (); - CT_ClearChatMessage (); + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; } + + if (multiplayer && deathmatch) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + return; + } + + chatmodeon = 1; + } + else + { + if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); + return; + } + + chatmodeon = 2; + } + + buttonMap.ResetButtonStates(); + C_HideConsole (); + CT_ClearChatMessage (); } CCMD (say) @@ -522,22 +576,75 @@ CCMD (say) if (argv.argc() == 1) { Printf ("Usage: say \n"); + return; + } + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) + { + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; + } + + // If not in a DM lobby, route it to team chat instead (helps improve chat + // filtering). + if (multiplayer && deathmatch) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + } + else + { + ShoveChatStr(argv[1], 0); + } + } + else if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); } else { - ShoveChatStr (argv[1], 0); + ShoveChatStr(argv[1], 1); } } CCMD (messagemode2) { - if (menuactive == MENU_Off) + if (menuactive != MENU_Off) + return; + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) { - buttonMap.ResetButtonStates(); - chatmodeon = 2; - C_HideConsole (); - CT_ClearChatMessage (); + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; } + + if (multiplayer && deathmatch && !teamplay) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + return; + } + + chatmodeon = 1; + } + else + { + if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); + return; + } + + chatmodeon = 2; + } + + buttonMap.ResetButtonStates(); + C_HideConsole(); + CT_ClearChatMessage(); } CCMD (say_team) @@ -545,6 +652,32 @@ CCMD (say_team) if (argv.argc() == 1) { Printf ("Usage: say_team \n"); + return; + } + + const uint64_t time = I_msTime(); + if (ChatCoolDown > time) + { + Printf("You must wait %d more seconds before being able to chat again\n", int((ChatCoolDown - time) * 0.001)); + return; + } + + // If in a DM lobby, route it to global chat instead (helps + // improve chat filtering). + if (multiplayer && deathmatch && !teamplay) + { + if (cl_showchat < CHAT_GLOBAL) + { + Printf("Global chat is currently disabled\n"); + } + else + { + ShoveChatStr(argv[1], 0); + } + } + else if (cl_showchat < CHAT_TEAM_ONLY) + { + Printf("Team chat is currently disabled\n"); } else { diff --git a/src/d_event.h b/src/d_event.h index 409dcb62b..56ae9b07e 100644 --- a/src/d_event.h +++ b/src/d_event.h @@ -99,6 +99,7 @@ enum gameaction_t : int ga_intro, ga_intermission, ga_titleloop, + ga_mapwarp, }; extern gameaction_t gameaction; diff --git a/src/d_main.cpp b/src/d_main.cpp index 875f892bf..9b1f9c530 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -167,7 +167,7 @@ void CloseWidgetResources(); bool D_CheckNetGame (); void D_ProcessEvents (); -void G_BuildTiccmd (ticcmd_t* cmd); +void G_BuildTiccmd (usercmd_t* cmd); void D_DoAdvanceDemo (); void D_LoadWadSettings (); void ParseGLDefs(); @@ -874,7 +874,6 @@ static void DrawRateStuff() static void DrawOverlays() { - NetUpdate (); C_DrawConsole (); M_Drawer (); DrawRateStuff(); @@ -904,6 +903,8 @@ void D_Display () int wipe_type; sector_t *viewsec; + GC::CheckGC(); + if (nodrawers || screen == NULL) return; // for comparative timing / profiling @@ -960,7 +961,7 @@ void D_Display () } // [RH] Allow temporarily disabling wipes - if (NoWipe || !CanWipe()) + if (netgame || NoWipe || !CanWipe()) { if (NoWipe > 0) NoWipe--; wipestart = nullptr; @@ -1153,7 +1154,6 @@ void D_Display () } else { - NetUpdate(); // send out any new accumulation PerformWipe(wipestart, screen->WipeEndScreen(), wipe_type, false, DrawOverlays); } cycles.Unclock(); @@ -1177,7 +1177,6 @@ void D_ErrorCleanup () Net_ClearBuffers (); G_NewInit (); M_ClearMenus (); - singletics = false; playeringame[0] = 1; players[0].playerstate = PST_LIVE; gameaction = ga_fullconsole; @@ -1224,28 +1223,7 @@ void D_DoomLoop () } I_SetFrameTime(); - // process one or more tics - if (singletics) - { - I_StartTic (); - D_ProcessEvents (); - G_BuildTiccmd (&netcmds[consoleplayer][maketic%BACKUPTICS]); - if (advancedemo) - D_DoAdvanceDemo (); - C_Ticker (); - M_Ticker (); - G_Ticker (); - // [RH] Use the consoleplayer's camera to update sounds - S_UpdateSounds (players[consoleplayer].camera); // move positional sounds - gametic++; - maketic++; - GC::CheckGC (); - Net_NewMakeTic (); - } - else - { - TryRunTics (); // will run at least one tic - } + TryRunTics (); // will run at least one tic // Update display, next frame, with current state. I_StartTic (); D_ProcessEvents(); @@ -3466,7 +3444,7 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw // [RH] Run any saved commands from the command line or autoexec.cfg now. gamestate = GS_FULLCONSOLE; - Net_NewMakeTic (); + Net_Initialize(); C_RunDelayedCommands(); gamestate = GS_STARTUP; @@ -3558,6 +3536,7 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw AddCommandString(StoredWarp.GetChars()); StoredWarp = ""; } + gameaction = ga_mapwarp; } else { diff --git a/src/d_net.cpp b/src/d_net.cpp index e1f665eec..1c9ca9202 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -79,969 +79,1208 @@ EXTERN_CVAR (Bool, cl_capfps) EXTERN_CVAR (Bool, vid_vsync) EXTERN_CVAR (Int, vid_maxfps) -//#define SIMULATEERRORS (RAND_MAX/3) -#define SIMULATEERRORS 0 - extern uint8_t *demo_p; // [RH] Special "ticcmds" get recorded in demos extern FString savedescription; extern FString savegamefile; -extern short consistancy[MAXPLAYERS][BACKUPTICS]; - extern bool AppActive; -#define netbuffer (doomcom.data) +void P_ClearLevelInterpolation(); -enum { NET_PeerToPeer, NET_PacketServer }; -uint8_t NetMode = NET_PeerToPeer; +struct FNetGameInfo +{ + uint32_t DetectedPlayers[MAXPLAYERS]; + uint8_t GotSetup[MAXPLAYERS]; +}; +enum ELevelStartStatus +{ + LST_READY, + LST_HOST, + LST_WAITING, +}; - -// // NETWORKING // -// gametic is the tic about to (or currently being) run -// maketic is the tick that hasn't had control made for it yet -// nettics[] has the maketics for all players +// gametic is the tic about to (or currently being) run. +// ClientTic is the tick the client is currently on and building a command for. // -// a gametic cannot be run until nettics[] > gametic for all players -// -#define RESENDCOUNT 10 -#define PL_DRONE 0x80 // bit flag in doomdata->player +// A world tick cannot be ran until CurrentSequence >= gametic for all clients. -ticcmd_t localcmds[LOCALCMDTICS]; +#define NetBuffer (doomcom.data) +ENetMode NetMode = NET_PeerToPeer; +int ClientTic = 0; +usercmd_t LocalCmds[LOCALCMDTICS] = {}; +int LastSentConsistency = 0; // Last consistency we sent out. If < CurrentConsistency, send them out. +int CurrentConsistency = 0; // Last consistency we generated. +FClientNetState ClientStates[MAXPLAYERS] = {}; -FDynamicBuffer NetSpecs[MAXPLAYERS][BACKUPTICS]; -ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS]; -int nettics[MAXNETNODES]; -bool nodeingame[MAXNETNODES]; // set false as nodes leave game -bool nodejustleft[MAXNETNODES]; // set when a node just left -bool remoteresend[MAXNETNODES]; // set when local needs tics -int resendto[MAXNETNODES]; // set when remote needs tics -int resendcount[MAXNETNODES]; +// If we're sending a packet to ourselves, store it here instead. This is the simplest way to execute +// playback as it means in the world running code itself all player commands are built the exact same way +// instead of having to rely on pulling from the correct local buffers. It also ensures all commands are +// executed over the net at the exact same tick. +static size_t LocalNetBufferSize = 0; +static uint8_t LocalNetBuffer[MAX_MSGLEN] = {}; -uint64_t lastrecvtime[MAXPLAYERS]; // [RH] Used for pings -uint64_t currrecvtime[MAXPLAYERS]; -uint64_t lastglobalrecvtime; // Identify the last time a packet was received. -bool hadlate; -int netdelay[MAXNETNODES][BACKUPTICS]; // Used for storing network delay times. -int lastaverage; +static uint8_t CurrentLobbyID = 0u; // Ignore commands not from this lobby (useful when transitioning levels). +static int LastGameUpdate = 0; // Track the last time the game actually ran the world. +static int MutedClients = 0; // Ignore messages from these clients. -int nodeforplayer[MAXPLAYERS]; -int playerfornode[MAXNETNODES]; +static int LevelStartDebug = 0; +static int LevelStartDelay = 0; // While this is > 0, don't start generating packets yet. +static ELevelStartStatus LevelStartStatus = LST_READY; // Listen for when to actually start making tics. +static int LevelStartAck = 0; // Used by the host to determine if everyone has loaded in. -int maketic; -int skiptics; -int ticdup = 1; +static int FullLatencyCycle = MAXSENDTICS * 3; // Give ~3 seconds to gather latency info about clients on boot up. +static int LastLatencyUpdate = 0; // Update average latency every ~1 second. -void D_ProcessEvents (void); -void G_BuildTiccmd (ticcmd_t *cmd); -void D_DoAdvanceDemo (void); +static int EnterTic = 0; +static int LastEnterTic = 0; +static bool bCommandsReset = false; // If true, commands were recently cleared. Don't generate any more tics. -static void SendSetup (uint32_t playersdetected[MAXNETNODES], uint8_t gotsetup[MAXNETNODES], int len); +static int CommandsAhead = 0; // In packet server mode, the host will let us know if we're outpacing them. +static int SkipCommandTimer = 0; // Tracker for when to check for skipping commands. ~0.5 seconds in a row of being ahead will start skipping. +static int SkipCommandAmount = 0; // Amount of commands to skip. Try and batch skip them all at once since we won't be able to get an update until the full RTT. + +void D_ProcessEvents(void); +void G_BuildTiccmd(usercmd_t *cmd); +void D_DoAdvanceDemo(void); + +static void SendSetup(const FNetGameInfo& info, int len); static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always); -int reboundpacket; -uint8_t reboundstore[MAX_MSGLEN]; - -int frameon; -int frameskip[4]; -int oldnettics; -int mastertics; - -static int entertic; -static int oldentertics; - extern bool advancedemo; CVAR(Bool, vid_dontdowait, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) -CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) -CUSTOM_CVAR(Int, net_extratic, 0, CVAR_SERVERINFO | CVAR_NOSAVE) +CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later. +CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) +CVAR(Bool, net_disablepause, false, CVAR_SERVERINFO | CVAR_NOSAVE) + +CVAR(Bool, cl_noboldchat, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CVAR(Bool, cl_nochatsound, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CUSTOM_CVAR(Int, cl_showchat, CHAT_GLOBAL, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) { - if (self < 0) - { - self = 0; - } - else if (self > 2) - { - self = 2; - } + if (self < CHAT_DISABLED) + self = CHAT_DISABLED; + else if (self > CHAT_GLOBAL) + self = CHAT_GLOBAL; } -#ifdef _DEBUG -CVAR(Int, net_fakelatency, 0, 0); - -struct PacketStore +// Used to write out all network events that occured leading up to the next tick. +static struct NetEventData { - int timer; - doomcom_t message; -}; + struct FStream { + uint8_t* Stream; + size_t Used = 0; -static TArray InBuffer; -static TArray OutBuffer; -#endif + FStream() + { + Grow(256); + } -// [RH] Special "ticcmds" get stored in here -static struct TicSpecial + ~FStream() + { + if (Stream != nullptr) + M_Free(Stream); + } + + void Grow(size_t size) + { + Stream = (uint8_t*)M_Realloc(Stream, size); + } + } Streams[BACKUPTICS]; + +private: + size_t CurrentSize = 0; + size_t MaxSize = 256; + int CurrentClientTic = 0; + + // Make more room for special Command. + void GetMoreBytes(size_t newSize) + { + MaxSize = max(MaxSize * 2, newSize + 30); + + DPrintf(DMSG_NOTIFY, "Expanding special size to %zu\n", MaxSize); + + for (auto& stream : Streams) + Streams->Grow(MaxSize); + + CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream + CurrentSize; + } + + void AddBytes(size_t bytes) + { + if (CurrentSize + bytes >= MaxSize) + GetMoreBytes(CurrentSize + bytes); + + CurrentSize += bytes; + } + +public: + uint8_t* CurrentStream = nullptr; + + // Boot up does some faux network events so we need to wait until after + // everything is initialized to actually set up the network stream. + void InitializeEventData() + { + CurrentStream = Streams[0].Stream; + CurrentSize = 0; + } + + void ResetStream() + { + CurrentClientTic = ClientTic / doomcom.ticdup; + CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream; + CurrentSize = 0; + } + + void NewClientTic() + { + const int tic = ClientTic / doomcom.ticdup; + if (CurrentClientTic == tic) + return; + + Streams[CurrentClientTic % BACKUPTICS].Used = CurrentSize; + + CurrentClientTic = tic; + CurrentStream = Streams[tic % BACKUPTICS].Stream; + CurrentSize = 0; + } + + NetEventData& operator<<(uint8_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(1); + WriteInt8(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(int16_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(2); + WriteInt16(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(int32_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(4); + WriteInt32(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(int64_t it) + { + if (CurrentStream != nullptr) + { + AddBytes(8); + WriteInt64(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(float it) + { + if (CurrentStream != nullptr) + { + AddBytes(4); + WriteFloat(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(double it) + { + if (CurrentStream != nullptr) + { + AddBytes(8); + WriteDouble(it, &CurrentStream); + } + return *this; + } + + NetEventData& operator<<(const char *it) + { + if (CurrentStream != nullptr) + { + AddBytes(strlen(it) + 1); + WriteString(it, &CurrentStream); + } + return *this; + } +} NetEvents; + +void Net_ClearBuffers() { - uint8_t *streams[BACKUPTICS]; - size_t used[BACKUPTICS]; - uint8_t *streamptr; - size_t streamoffs; - size_t specialsize; - int lastmaketic; - bool okay; - - TicSpecial () + for (int i = 0; i < MAXPLAYERS; ++i) { - int i; + playeringame[i] = false; + players[i].waiting = players[i].inconsistant = false; - lastmaketic = -1; - specialsize = 256; + auto& state = ClientStates[i]; + state.AverageLatency = state.CurrentLatency = 0u; + memset(state.SentTime, 0, sizeof(state.SentTime)); + memset(state.RecvTime, 0, sizeof(state.RecvTime)); + state.bNewLatency = true; - for (i = 0; i < BACKUPTICS; i++) - streams[i] = NULL; + state.ResendID = 0u; + state.CurrentNetConsistency = state.LastVerifiedConsistency = state.ConsistencyAck = state.ResendConsistencyFrom = -1; + state.CurrentSequence = state.SequenceAck = state.ResendSequenceFrom = -1; + state.Flags = 0; - for (i = 0; i < BACKUPTICS; i++) - { - streams[i] = (uint8_t *)M_Malloc (256); - used[i] = 0; - } - okay = true; + for (int j = 0; j < BACKUPTICS; ++j) + state.Tics[j].Data.SetData(nullptr, 0); } - ~TicSpecial () - { - int i; + doomcom.command = doomcom.datalength = 0; + doomcom.remoteplayer = -1; + doomcom.numplayers = doomcom.ticdup = 1; + consoleplayer = doomcom.consoleplayer = 0; + LocalNetBufferSize = 0; + Net_Arbitrator = 0; - for (i = 0; i < BACKUPTICS; i++) - { - if (streams[i]) - { - M_Free (streams[i]); - streams[i] = NULL; - used[i] = 0; - } - } - okay = false; - } + MutedClients = 0; + CurrentLobbyID = 0u; + NetworkClients.Clear(); + NetMode = NET_PeerToPeer; + netgame = multiplayer = false; + LastSentConsistency = CurrentConsistency = 0; + LastEnterTic = LastGameUpdate = EnterTic; + gametic = ClientTic = 0; + SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; + NetEvents.ResetStream(); - // Make more room for special commands. - void GetMoreSpace (size_t needed) - { - int i; + LevelStartAck = 0; + LevelStartDelay = LevelStartDebug = 0; + LevelStartStatus = LST_READY; - specialsize = max(specialsize * 2, needed + 30); + FullLatencyCycle = MAXSENDTICS * 3; + LastLatencyUpdate = 0; - DPrintf (DMSG_NOTIFY, "Expanding special size to %zu\n", specialsize); - - for (i = 0; i < BACKUPTICS; i++) - streams[i] = (uint8_t *)M_Realloc (streams[i], specialsize); - - streamptr = streams[(maketic/ticdup)%BACKUPTICS] + streamoffs; - } - - void CheckSpace (size_t needed) - { - if (streamoffs + needed >= specialsize) - GetMoreSpace (streamoffs + needed); - - streamoffs += needed; - } - - void NewMakeTic () - { - int mt = maketic / ticdup; - if (lastmaketic != -1) - { - if (lastmaketic == mt) - return; - used[lastmaketic%BACKUPTICS] = streamoffs; - } - - lastmaketic = mt; - streamptr = streams[mt%BACKUPTICS]; - streamoffs = 0; - } - - TicSpecial &operator << (uint8_t it) - { - if (streamptr) - { - CheckSpace (1); - WriteInt8 (it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (int16_t it) - { - if (streamptr) - { - CheckSpace (2); - WriteInt16 (it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (int32_t it) - { - if (streamptr) - { - CheckSpace (4); - WriteInt32 (it, &streamptr); - } - return *this; - } - - TicSpecial& operator << (int64_t it) - { - if (streamptr) - { - CheckSpace(8); - WriteInt64(it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (float it) - { - if (streamptr) - { - CheckSpace (4); - WriteFloat (it, &streamptr); - } - return *this; - } - - TicSpecial& operator << (double it) - { - if (streamptr) - { - CheckSpace(8); - WriteDouble(it, &streamptr); - } - return *this; - } - - TicSpecial &operator << (const char *it) - { - if (streamptr) - { - CheckSpace (strlen (it) + 1); - WriteString (it, &streamptr); - } - return *this; - } - -} specials; - -void Net_ClearBuffers () -{ - int i, j; - - memset (localcmds, 0, sizeof(localcmds)); - memset (netcmds, 0, sizeof(netcmds)); - memset (nettics, 0, sizeof(nettics)); - memset (nodeingame, 0, sizeof(nodeingame)); - memset (nodeforplayer, 0, sizeof(nodeforplayer)); - memset (playerfornode, 0, sizeof(playerfornode)); - memset (remoteresend, 0, sizeof(remoteresend)); - memset (resendto, 0, sizeof(resendto)); - memset (resendcount, 0, sizeof(resendcount)); - memset (lastrecvtime, 0, sizeof(lastrecvtime)); - memset (currrecvtime, 0, sizeof(currrecvtime)); - memset (consistancy, 0, sizeof(consistancy)); - nodeingame[0] = true; - - for (i = 0; i < MAXPLAYERS; i++) - { - for (j = 0; j < BACKUPTICS; j++) - { - NetSpecs[i][j].SetData (NULL, 0); - } - } - - oldentertics = entertic; - gametic = 0; - maketic = 0; - - lastglobalrecvtime = 0; + playeringame[0] = true; + NetworkClients += 0; } -// -// [RH] Rewritten to properly calculate the packet size -// with our variable length commands. -// -int NetbufferSize () +void Net_ResetCommands(bool midTic) { - if (netbuffer[0] & (NCMD_EXIT | NCMD_SETUP)) + bCommandsReset = midTic; + ++CurrentLobbyID; + SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; + + int tic = gametic / doomcom.ticdup; + if (midTic) { - return doomcom.datalength; - } - - int k = 2, count, numtics; - - if (netbuffer[0] & NCMD_RETRANSMIT) - k++; - - if (NetMode == NET_PacketServer && doomcom.remotenode == nodeforplayer[Net_Arbitrator]) - k++; - - numtics = netbuffer[0] & NCMD_XTICS; - if (numtics == 3) - { - numtics += netbuffer[k++]; - } - - if (netbuffer[0] & NCMD_QUITTERS) - { - k += netbuffer[k] + 1; - } - - // Network delay byte - k++; - - if (netbuffer[0] & NCMD_MULTI) - { - count = netbuffer[k]; - k += count; + // If we're mid ticdup cycle, make sure we immediately enter the next one after + // the current tic we're in finishes. + ClientTic = (tic + 1) * doomcom.ticdup; + gametic = (tic * doomcom.ticdup) + (doomcom.ticdup - 1); } else { - count = 1; + ClientTic = gametic = tic * doomcom.ticdup; + --tic; + } + + for (auto client : NetworkClients) + { + auto& state = ClientStates[client]; + state.Flags &= CF_QUIT; + state.CurrentSequence = min(state.CurrentSequence, tic); + state.SequenceAck = min(state.SequenceAck, tic); + if (state.ResendSequenceFrom >= tic) + state.ResendSequenceFrom = -1; + + // Make sure not to run its current command either. + auto& curTic = state.Tics[tic % BACKUPTICS]; + memset(&curTic.Command, 0, sizeof(curTic.Command)); + curTic.Data.SetData(nullptr, 0); } - // Need at least 3 bytes per tic per player - if (doomcom.datalength < k + 3 * count * numtics) + NetEvents.ResetStream(); +} + +void Net_SetWaiting() +{ + if (netgame && !demoplayback && NetworkClients.Size() > 1) + LevelStartStatus = LST_WAITING; +} + +// [RH] Rewritten to properly calculate the packet size +// with our variable length Command. +static int GetNetBufferSize() +{ + if (NetBuffer[0] & NCMD_EXIT) + return 1 + (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator); + // TODO: Need a skipper for this. + if (NetBuffer[0] & NCMD_SETUP) + return doomcom.datalength; + if (NetBuffer[0] & (NCMD_LATENCY | NCMD_LATENCYACK)) + return 2; + + if (NetBuffer[0] & NCMD_LEVELREADY) { - return k + 3 * count * numtics; + int bytes = 2; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + bytes += 2; + + return bytes; } - uint8_t *skipper = &netbuffer[k]; - if ((netbuffer[0] & NCMD_EXIT) == 0) + // Header info + int totalBytes = 10; + if (NetBuffer[0] & NCMD_QUITTERS) + totalBytes += NetBuffer[totalBytes] + 1; + + const int playerCount = NetBuffer[totalBytes++]; + const int numTics = NetBuffer[totalBytes++]; + if (numTics > 0) + totalBytes += 4; + const int ranTics = NetBuffer[totalBytes++]; + if (ranTics > 0) + totalBytes += 4; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + ++totalBytes; + + // Minimum additional packet size per player: + // 1 byte for player number + // If in packet server mode and from the host, 2 bytes for the latency to the host + int padding = 1; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + padding += 2; + if (doomcom.datalength < totalBytes + playerCount * padding) + return totalBytes + playerCount * padding; + + uint8_t* skipper = &NetBuffer[totalBytes]; + for (int p = 0; p < playerCount; ++p) { - while (count-- > 0) + ++skipper; + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + skipper += 2; + + for (int i = 0; i < ranTics; ++i) + skipper += 3; + + for (int i = 0; i < numTics; ++i) { - SkipTicCmd (&skipper, numtics); + ++skipper; + SkipUserCmdMessage(skipper); } } - return int(skipper - netbuffer); + + return int(skipper - NetBuffer); } -// -// -// -int ExpandTics (int low) -{ - int delta; - int mt = maketic / ticdup; - - delta = low - (mt&0xff); - - if (delta >= -64 && delta <= 64) - return (mt&~0xff) + low; - if (delta > 64) - return (mt&~0xff) - 256 + low; - if (delta < -64) - return (mt&~0xff) + 256 + low; - - I_Error ("ExpandTics: strange value %i at maketic %i", low, maketic); - return 0; -} - - - // // HSendPacket // -void HSendPacket (int node, int len) +static void HSendPacket(int client, size_t size) { - if (debugfile && node != 0) - { - int i, k, realretrans; - - if (netbuffer[0] & NCMD_SETUP) - { - fprintf (debugfile,"%i/%i send %i = SETUP [%3i]", gametic, maketic, node, len); - for (i = 0; i < len; i++) - fprintf (debugfile," %2x", ((uint8_t *)netbuffer)[i]); - } - else if (netbuffer[0] & NCMD_EXIT) - { - fprintf (debugfile,"%i/%i send %i = EXIT [%3i]", gametic, maketic, node, len); - for (i = 0; i < len; i++) - fprintf (debugfile," %2x", ((uint8_t *)netbuffer)[i]); - } - else - { - k = 2; - - if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator && - node != 0) - { - k++; - } - - if (netbuffer[0] & NCMD_RETRANSMIT) - realretrans = ExpandTics (netbuffer[k++]); - else - realretrans = -1; - - int numtics = netbuffer[0] & 3; - if (numtics == 3) - numtics += netbuffer[k++]; - - fprintf (debugfile,"%i/%i send %i = (%i + %i, R %i) [%3i]", - gametic, maketic, - node, - ExpandTics(netbuffer[1]), - numtics, realretrans, len); - - for (i = 0; i < len; i++) - fprintf (debugfile, "%c%2x", i==k?'|':' ', ((uint8_t *)netbuffer)[i]); - } - fprintf (debugfile, " [[ "); - for (i = 0; i < doomcom.numnodes; ++i) - { - if (nodeingame[i]) - { - fprintf (debugfile, "%d ", nettics[i]); - } - else - { - fprintf (debugfile, "--- "); - } - } - fprintf (debugfile, "]]\n"); - } - - if (node == 0) - { - memcpy (reboundstore, netbuffer, len); - reboundpacket = len; - return; - } - + // This data already exists locally in the demo file, so don't write it out. if (demoplayback) return; - if (!netgame) - I_Error ("Tried to transmit to another node"); - -#if SIMULATEERRORS - if (rand() < SIMULATEERRORS) + doomcom.remoteplayer = client; + doomcom.datalength = size; + if (client == consoleplayer) { - if (debugfile) - fprintf (debugfile, "Drop!\n"); + memcpy(LocalNetBuffer, NetBuffer, size); + LocalNetBufferSize = size; return; } -#endif + + if (!netgame) + I_Error("Tried to send a packet to a client while offline"); doomcom.command = CMD_SEND; - doomcom.remotenode = node; - doomcom.datalength = len; - -#ifdef _DEBUG - if (net_fakelatency / 2 > 0) - { - PacketStore store; - store.message = doomcom; - store.timer = I_GetTime() + ((net_fakelatency / 2) / (1000 / TICRATE)); - OutBuffer.Push(store); - } - else - I_NetCmd(); - - for (unsigned int i = 0; i < OutBuffer.Size(); i++) - { - if (OutBuffer[i].timer <= I_GetTime()) - { - doomcom = OutBuffer[i].message; - I_NetCmd(); - OutBuffer.Delete(i); - i = -1; - } - } -#else I_NetCmd(); -#endif } -// // HGetPacket // Returns false if no packet is waiting -// -bool HGetPacket (void) +static bool HGetPacket() { - if (reboundpacket) + if (demoplayback) + return false; + + if (LocalNetBufferSize) { - memcpy (netbuffer, reboundstore, reboundpacket); - doomcom.remotenode = 0; - reboundpacket = 0; + memcpy(NetBuffer, LocalNetBuffer, LocalNetBufferSize); + doomcom.datalength = LocalNetBufferSize; + doomcom.remoteplayer = consoleplayer; + LocalNetBufferSize = 0; return true; } if (!netgame) return false; - if (demoplayback) - return false; - doomcom.command = CMD_GET; - I_NetCmd (); + I_NetCmd(); -#ifdef _DEBUG - if (net_fakelatency / 2 > 0 && doomcom.remotenode != -1) - { - PacketStore store; - store.message = doomcom; - store.timer = I_GetTime() + ((net_fakelatency / 2) / (1000 / TICRATE)); - InBuffer.Push(store); - doomcom.remotenode = -1; - } - - if (doomcom.remotenode == -1) - { - bool gotmessage = false; - for (unsigned int i = 0; i < InBuffer.Size(); i++) - { - if (InBuffer[i].timer <= I_GetTime()) - { - doomcom = InBuffer[i].message; - InBuffer.Delete(i); - gotmessage = true; - break; - } - } - if (!gotmessage) - return false; - } -#else - if (doomcom.remotenode == -1) - { + if (doomcom.remoteplayer == -1) return false; - } -#endif - - if (debugfile) + + // When the host randomly drops this can cause issues in packet server mode, so at + // least attempt to recover gracefully. + if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator + && (NetBuffer[0] & NCMD_EXIT) && doomcom.datalength == 1) { - int i, k, realretrans; - - if (netbuffer[0] & NCMD_SETUP) - { - fprintf (debugfile,"%i/%i get %i = SETUP [%3i]", gametic, maketic, doomcom.remotenode, doomcom.datalength); - for (i = 0; i < doomcom.datalength; i++) - fprintf (debugfile, " %2x", ((uint8_t *)netbuffer)[i]); - fprintf (debugfile, "\n"); - } - else if (netbuffer[0] & NCMD_EXIT) - { - fprintf (debugfile,"%i/%i get %i = EXIT [%3i]", gametic, maketic, doomcom.remotenode, doomcom.datalength); - for (i = 0; i < doomcom.datalength; i++) - fprintf (debugfile, " %2x", ((uint8_t *)netbuffer)[i]); - fprintf (debugfile, "\n"); - } - else { - k = 2; - - if (NetMode == NET_PacketServer && - doomcom.remotenode == nodeforplayer[Net_Arbitrator]) - { - k++; - } - - if (netbuffer[0] & NCMD_RETRANSMIT) - realretrans = ExpandTics (netbuffer[k++]); - else - realretrans = -1; - - int numtics = netbuffer[0] & 3; - if (numtics == 3) - numtics += netbuffer[k++]; - - fprintf (debugfile,"%i/%i get %i = (%i + %i, R %i) [%3i]", - gametic, maketic, - doomcom.remotenode, - ExpandTics(netbuffer[1]), - numtics, realretrans, doomcom.datalength); - - for (i = 0; i < doomcom.datalength; i++) - fprintf (debugfile, "%c%2x", i==k?'|':' ', ((uint8_t *)netbuffer)[i]); - if (numtics) - fprintf (debugfile, " <<%4x>>\n", - consistancy[playerfornode[doomcom.remotenode]][nettics[doomcom.remotenode]%BACKUPTICS] & 0xFFFF); - else - fprintf (debugfile, "\n"); - } + NetBuffer[1] = NetworkClients[1]; + doomcom.datalength = 2; } - if (doomcom.datalength != NetbufferSize ()) + int sizeCheck = GetNetBufferSize(); + if (doomcom.datalength != sizeCheck) { - Printf("Bad packet length %i (calculated %i)\n", - doomcom.datalength, NetbufferSize()); - - if (debugfile) - fprintf (debugfile,"---bad packet length %i (calculated %i)\n", - doomcom.datalength, NetbufferSize()); + Printf("Incorrect packet size %d (expected %d)\n", doomcom.datalength, sizeCheck); return false; } - return true; + return true; } -void PlayerIsGone (int netnode, int netconsole) +static void ClientConnecting(int client) { - int i; + if (consoleplayer != Net_Arbitrator) + return; - if (nodeingame[netnode]) + // TODO: Eventually... +} + +static void DisconnectClient(int clientNum) +{ + NetworkClients -= clientNum; + MutedClients &= ~(1 << clientNum); + I_ClearNode(clientNum); + // Capture the pawn leaving in the next world tick. + players[clientNum].playerstate = PST_GONE; +} + +static void SetArbitrator(int clientNum) +{ + Net_Arbitrator = clientNum; + players[Net_Arbitrator].settings_controller = true; + Printf("%s is the new host\n", players[Net_Arbitrator].userinfo.GetName()); + if (NetMode == NET_PacketServer) { - for (i = netnode + 1; i < doomcom.numnodes; ++i) - { - if (nodeingame[i]) - break; - } - if (i == doomcom.numnodes) - { - doomcom.numnodes = netnode; - } + for (auto client : NetworkClients) + ClientStates[client].AverageLatency = 0u; - if (playeringame[netconsole]) - { - players[netconsole].playerstate = PST_GONE; - } - nodeingame[netnode] = false; - nodejustleft[netnode] = false; + Net_ResetCommands(false); + Net_SetWaiting(); } - else if (nodejustleft[netnode]) // Packet Server - { - if (netnode + 1 == doomcom.numnodes) - { - doomcom.numnodes = netnode; - } - if (playeringame[netconsole]) - { - players[netconsole].playerstate = PST_GONE; - } - nodejustleft[netnode] = false; - } - else return; +} - if (netconsole == Net_Arbitrator) - { - // Pick a new network arbitrator - for (int i = 0; i < MAXPLAYERS; i++) - { - if (i != netconsole && playeringame[i] && players[i].Bot == NULL) - { - Net_Arbitrator = i; - players[i].settings_controller = true; - Printf("%s is the new arbitrator\n", players[i].userinfo.GetName()); - break; - } - } - } +static void ClientQuit(int clientNum, int newHost) +{ + if (!NetworkClients.InGame(clientNum)) + return; - if (debugfile && NetMode == NET_PacketServer) + // This will get caught in the main loop and send it out to everyone as one big packet. The only + // exception is the host who will leave instantly and send out any needed data. + if (NetMode == NET_PacketServer && clientNum != Net_Arbitrator) { - if (Net_Arbitrator == consoleplayer) - { - fprintf(debugfile, "I am the new master!\n"); - } + if (consoleplayer != Net_Arbitrator) + DPrintf(DMSG_WARNING, "Received disconnect packet from client %d erroneously\n", clientNum); else - { - fprintf(debugfile, "Node %d is the new master!\n", nodeforplayer[Net_Arbitrator]); - } + ClientStates[clientNum].Flags |= CF_QUIT; + + return; } + DisconnectClient(clientNum); + if (clientNum == Net_Arbitrator) + SetArbitrator(newHost >= 0 ? newHost : NetworkClients[0]); + if (demorecording) - { - G_CheckDemoStatus (); + G_CheckDemoStatus(); +} - //WriteByte (DEM_DROPPLAYER, &demo_p); - //WriteByte ((uint8_t)netconsole, &demo_p); +static bool IsMapLoaded() +{ + return gamestate == GS_LEVEL; +} + +static void CheckLevelStart(int client, int delayTics) +{ + if (LevelStartStatus != LST_WAITING) + { + if (consoleplayer == Net_Arbitrator && client != consoleplayer) + { + // Someone might've missed the previous packet, so resend it just in case. + NetBuffer[0] = NCMD_LEVELREADY; + NetBuffer[1] = CurrentLobbyID; + if (NetMode == NET_PacketServer) + { + NetBuffer[2] = 0; + NetBuffer[3] = 0; + } + + HSendPacket(client, NetMode == NET_PacketServer ? 4 : 2); + } + + return; + } + + if (client == Net_Arbitrator) + { + LevelStartAck = 0; + LevelStartStatus = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? LST_HOST : LST_READY; + LevelStartDelay = LevelStartDebug = delayTics; + LastGameUpdate = EnterTic; + return; + } + + int mask = 0; + for (auto pNum : NetworkClients) + { + if (pNum != Net_Arbitrator) + mask |= 1 << pNum; + } + + LevelStartAck |= 1 << client; + if ((LevelStartAck & mask) == mask && IsMapLoaded()) + { + // Beyond this point a player is likely lagging out anyway. + constexpr uint16_t LatencyCap = 350u; + + NetBuffer[0] = NCMD_LEVELREADY; + NetBuffer[1] = CurrentLobbyID; + uint16_t highestAvg = 0u; + if (NetMode == NET_PacketServer) + { + // Wait for enough latency info to be accepted so a better average + // can be calculated for everyone. + if (FullLatencyCycle > 0) + return; + + for (auto client : NetworkClients) + { + if (client == Net_Arbitrator) + continue; + + const uint16_t latency = min(ClientStates[client].AverageLatency, LatencyCap); + if (latency > highestAvg) + highestAvg = latency; + } + } + + constexpr double MS2Sec = 1.0 / 1000.0; + for (auto client : NetworkClients) + { + if (NetMode == NET_PacketServer) + { + int delay = 0; + if (client != Net_Arbitrator) + delay = int(floor((highestAvg - min(ClientStates[client].AverageLatency, LatencyCap)) * MS2Sec * TICRATE)); + + NetBuffer[2] = (delay << 8); + NetBuffer[3] = delay; + } + + HSendPacket(client, NetMode == NET_PacketServer ? 4 : 2); + } } } +struct FLatencyAck +{ + int Client; + uint8_t Seq; + + FLatencyAck(int client, uint8_t seq) : Client(client), Seq(seq) {} +}; + // // GetPackets // - -void GetPackets (void) +static void GetPackets() { - int netconsole; - int netnode; - int realend; - int realstart; - int numtics; - int retransmitfrom; - int k; - uint8_t playerbytes[MAXNETNODES]; - int numplayers; - - while ( HGetPacket() ) + TArray latencyAcks = {}; + TArray stuckInLobby = {}; + while (HGetPacket()) { - if (netbuffer[0] & NCMD_SETUP) + const int clientNum = doomcom.remoteplayer; + auto& clientState = ClientStates[clientNum]; + + if (NetBuffer[0] & NCMD_EXIT) { - if (consoleplayer == Net_Arbitrator) - { - // This player apparantly doesn't realise the game has started - netbuffer[0] = NCMD_SETUP+3; - HSendPacket (doomcom.remotenode, 1); - } - continue; // extra setup packet + ClientQuit(clientNum, NetMode == NET_PacketServer && clientNum == Net_Arbitrator ? NetBuffer[1] : -1); + continue; } - - netnode = doomcom.remotenode; - netconsole = playerfornode[netnode] & ~PL_DRONE; - // [RH] Get "ping" times - totally useless, since it's bound to the frequency - // packets go out at. - lastrecvtime[netconsole] = currrecvtime[netconsole]; - currrecvtime[netconsole] = I_msTime (); - - // check for exiting the game - if (netbuffer[0] & NCMD_EXIT) + if (NetBuffer[0] & NCMD_SETUP) { - if (!nodeingame[netnode]) - continue; - - if (NetMode != NET_PacketServer || netconsole == Net_Arbitrator) + if (NetworkClients.InGame(clientNum)) { - PlayerIsGone (netnode, netconsole); - if (NetMode == NET_PacketServer) - { - uint8_t *foo = &netbuffer[2]; - for (int i = 0; i < MAXPLAYERS; ++i) - { - if (playeringame[i]) - { - int resend = ReadInt32 (&foo); - if (i != consoleplayer) - { - resendto[nodeforplayer[i]] = resend; - } - } - } - } + if (consoleplayer == Net_Arbitrator && stuckInLobby.Find(clientNum) >= stuckInLobby.Size()) + stuckInLobby.Push(clientNum); } else { - nodeingame[netnode] = false; - nodejustleft[netnode] = true; + ClientConnecting(clientNum); } + continue; } - k = 2; - - if (NetMode == NET_PacketServer && - netconsole == Net_Arbitrator && - netconsole != consoleplayer) + if (NetBuffer[0] & NCMD_LATENCY) { - mastertics = ExpandTics (netbuffer[k++]); - } - - if (netbuffer[0] & NCMD_RETRANSMIT) - { - retransmitfrom = netbuffer[k++]; - } - else - { - retransmitfrom = 0; - } - - numtics = (netbuffer[0] & NCMD_XTICS); - if (numtics == 3) - { - numtics += netbuffer[k++]; - } - - if (netbuffer[0] & NCMD_QUITTERS) - { - numplayers = netbuffer[k++]; - for (int i = 0; i < numplayers; ++i) + size_t i = 0u; + for (; i < latencyAcks.Size(); ++i) { - PlayerIsGone (nodeforplayer[netbuffer[k]], netbuffer[k]); - k++; + if (latencyAcks[i].Client == clientNum) + break; + } + + if (i >= latencyAcks.Size()) + latencyAcks.Push({ clientNum, NetBuffer[1] }); + + continue; + } + + if (NetBuffer[0] & NCMD_LATENCYACK) + { + if (NetBuffer[1] == clientState.CurrentLatency) + { + clientState.RecvTime[clientState.CurrentLatency++ % MAXSENDTICS] = I_msTime(); + clientState.bNewLatency = true; + } + + continue; + } + + if (NetBuffer[0] & NCMD_LEVELREADY) + { + if (NetBuffer[1] == CurrentLobbyID) + { + int delay = 0; + if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator) + delay = (NetBuffer[2] << 8) | NetBuffer[3]; + + CheckLevelStart(clientNum, delay); + } + + continue; + } + + if (NetBuffer[0] & NCMD_RETRANSMIT) + { + clientState.ResendID = NetBuffer[1]; + clientState.Flags |= CF_RETRANSMIT; + } + + const bool validID = NetBuffer[1] == CurrentLobbyID; + if (validID) + { + clientState.Flags |= CF_UPDATED; + clientState.SequenceAck = (NetBuffer[2] << 24) | (NetBuffer[3] << 16) | (NetBuffer[4] << 8) | NetBuffer[5]; + } + + const int consistencyAck = (NetBuffer[6] << 24) | (NetBuffer[7] << 16) | (NetBuffer[8] << 8) | NetBuffer[9]; + + int curByte = 10; + if (NetBuffer[0] & NCMD_QUITTERS) + { + int numPlayers = NetBuffer[curByte++]; + for (int i = 0; i < numPlayers; ++i) + DisconnectClient(NetBuffer[curByte++]); + } + + const int playerCount = NetBuffer[curByte++]; + + int baseSequence = -1; + const int totalTics = NetBuffer[curByte++]; + if (totalTics > 0) + baseSequence = (NetBuffer[curByte++] << 24) | (NetBuffer[curByte++] << 16) | (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]; + + int baseConsistency = -1; + const int ranTics = NetBuffer[curByte++]; + if (ranTics > 0) + baseConsistency = (NetBuffer[curByte++] << 24) | (NetBuffer[curByte++] << 16) | (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]; + + if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator) + { + if (validID) + CommandsAhead = NetBuffer[curByte++]; + else + ++curByte; + } + + for (int p = 0; p < playerCount; ++p) + { + const int pNum = NetBuffer[curByte++]; + auto& pState = ClientStates[pNum]; + + // This gets sent over per-player so latencies are correct in packet server mode. + if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator) + { + if (consoleplayer != Net_Arbitrator) + pState.AverageLatency = (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]; + else + curByte += 2; + } + + // Make sure the host doesn't update a player's last consistency ack with their own data. + if (NetMode != NET_PacketServer || consoleplayer != Net_Arbitrator + || pNum == Net_Arbitrator || clientNum != Net_Arbitrator) + { + pState.ConsistencyAck = consistencyAck; + } + + TArray consistencies = {}; + for (int r = 0; r < ranTics; ++r) + { + int ofs = NetBuffer[curByte++]; + consistencies.Insert(ofs, (NetBuffer[curByte++] << 8) | NetBuffer[curByte++]); + } + + for (size_t i = 0u; i < consistencies.Size(); ++i) + { + const int cTic = baseConsistency + i; + if (cTic <= pState.CurrentNetConsistency) + continue; + + if (cTic > pState.CurrentNetConsistency + 1 || !consistencies[i]) + { + clientState.Flags |= CF_MISSING_CON; + break; + } + + pState.NetConsistency[cTic % BACKUPTICS] = consistencies[i]; + pState.CurrentNetConsistency = cTic; + } + + // Each tic within a given packet is given a sequence number to ensure that things were put + // back together correctly. Normally this wouldn't matter as much but since we need to keep + // clients in lock step a misordered packet will instantly cause a desync. + TArray data = {}; + for (int t = 0; t < totalTics; ++t) + { + // Try and reorder the tics if they're all there but end up out of order. + const int ofs = NetBuffer[curByte++]; + data.Insert(ofs, &NetBuffer[curByte]); + uint8_t* skipper = &NetBuffer[curByte]; + curByte += SkipUserCmdMessage(skipper); + } + + // If it's from a previous waiting period, the commands are no longer relevant. + if (!validID) + continue; + + for (size_t i = 0u; i < data.Size(); ++i) + { + const int seq = baseSequence + i; + // Duplicate command, ignore it. + if (seq <= pState.CurrentSequence) + continue; + + // Skipped a command. Packet likely got corrupted while being put back together, so have + // the client send over the properly ordered commands. + if (seq > pState.CurrentSequence + 1 || data[i] == nullptr) + { + clientState.Flags |= CF_MISSING_SEQ; + break; + } + + ReadUserCmdMessage(data[i], pNum, seq); + // The host and clients are a bit desynched here. We don't want to update the host's latest ack with their own + // info since they get those from the actual clients, but clients have to get them from the host since they + // don't commincate with each other except in P2P mode. + if (NetMode != NET_PacketServer || consoleplayer != Net_Arbitrator + || pNum == Net_Arbitrator || clientNum != Net_Arbitrator) + { + pState.CurrentSequence = seq; + } } } + } - // Pull current network delay from node - netdelay[netnode][(nettics[netnode]+1) % BACKUPTICS] = netbuffer[k++]; + for (const auto& ack : latencyAcks) + { + NetBuffer[0] = NCMD_LATENCYACK; + NetBuffer[1] = ack.Seq; + HSendPacket(ack.Client, 2); + } - playerbytes[0] = netconsole; - if (netbuffer[0] & NCMD_MULTI) - { - numplayers = netbuffer[k++]; - memcpy (playerbytes+1, &netbuffer[k], numplayers - 1); - k += numplayers - 1; - } - else - { - numplayers = 1; - } + for (auto client : stuckInLobby) + { + NetBuffer[0] = NCMD_GAMEREADY; + HSendPacket(client, 1); + } +} - // to save bytes, only the low byte of tic numbers are sent - // Figure out what the rest of the bytes are - realstart = ExpandTics (netbuffer[1]); - realend = (realstart + numtics); - - nodeforplayer[netconsole] = netnode; - - // check for retransmit request - if (resendcount[netnode] <= 0 && (netbuffer[0] & NCMD_RETRANSMIT)) - { - resendto[netnode] = ExpandTics (retransmitfrom); - if (debugfile) - fprintf (debugfile,"retransmit from %i\n", resendto[netnode]); - resendcount[netnode] = RESENDCOUNT; - } - else - { - resendcount[netnode]--; - } - - // check for out of order / duplicated packet - if (realend == nettics[netnode]) +static void SendHeartbeat() +{ + // TODO: This could probably also be used to determine if there's packets + // missing and a retransmission is needed. + const uint64_t time = I_msTime(); + for (auto client : NetworkClients) + { + if (client == consoleplayer) continue; - - if (realend < nettics[netnode]) - { - if (debugfile) - fprintf (debugfile, "out of order packet (%i + %i)\n" , - realstart, numtics); - continue; - } - - // check for a missed packet - if (realstart > nettics[netnode]) - { - // stop processing until the other system resends the missed tics - if (debugfile) - fprintf (debugfile, "missed tics from %i (%i to %i)\n", - netnode, nettics[netnode], realstart); - remoteresend[netnode] = true; - continue; - } - // update command store from the packet + auto& state = ClientStates[client]; + if (LastLatencyUpdate >= MAXSENDTICS) { - uint8_t *start; - int i, tics; - remoteresend[netnode] = false; - - start = &netbuffer[k]; - - for (i = 0; i < numplayers; ++i) + int delta = 0; + const uint8_t startTic = state.CurrentLatency - MAXSENDTICS; + for (int i = 0; i < MAXSENDTICS; ++i) { - int node = nodeforplayer[playerbytes[i]]; + const int tic = (startTic + i) % MAXSENDTICS; + const uint64_t high = state.RecvTime[tic] < state.SentTime[tic] ? time : state.RecvTime[tic]; + delta += high - state.SentTime[tic]; + } - SkipTicCmd (&start, nettics[node] - realstart); - for (tics = nettics[node]; tics < realend; tics++) - ReadTicCmd (&start, playerbytes[i], tics); + state.AverageLatency = delta / MAXSENDTICS; + } - nettics[nodeforplayer[playerbytes[i]]] = realend; + if (state.bNewLatency) + { + // Use the most up-to-date time here for better accuracy. + state.SentTime[state.CurrentLatency % MAXSENDTICS] = I_msTime(); + state.bNewLatency = false; + } + + NetBuffer[0] = NCMD_LATENCY; + NetBuffer[1] = state.CurrentLatency; + HSendPacket(client, 2); + } +} + +static void CheckConsistencies() +{ + // Check consistencies retroactively to see if there was a desync at some point. We still + // check the local client here because in packet server mode these could realistically desync + // if the client's current position doesn't agree with the host. + for (auto client : NetworkClients) + { + auto& clientState = ClientStates[client]; + // If previously inconsistent, always mark it as such going forward. We don't want this to + // accidentally go away at some point since the game state is already completely broken. + if (players[client].inconsistant) + { + clientState.LastVerifiedConsistency = clientState.CurrentNetConsistency; + } + else + { + // Make sure we don't check past tics we haven't even ran yet. + const int limit = min(CurrentConsistency - 1, clientState.CurrentNetConsistency); + while (clientState.LastVerifiedConsistency < limit) + { + const int tic = clientState.LastVerifiedConsistency % BACKUPTICS; + if (clientState.LocalConsistency[tic] != clientState.NetConsistency[tic]) + { + players[client].inconsistant = true; + clientState.LastVerifiedConsistency = clientState.CurrentNetConsistency; + break; + } + + ++clientState.LastVerifiedConsistency; } } } } +//========================================================================== // -// NetUpdate -// Builds ticcmds for console player, -// sends out a packet +// FRandom :: StaticSumSeeds // -int gametime; +// This function produces a uint32_t that can be used to check the consistancy +// of network games between different machines. Only a select few RNGs are +// used for the sum, because not all RNGs are important to network sync. +// +//========================================================================== -void NetUpdate (void) +extern FRandom pr_spawnmobj; +extern FRandom pr_acs; +extern FRandom pr_chase; +extern FRandom pr_damagemobj; + +static uint32_t StaticSumSeeds() { - int lowtic; - int nowtime; - int newtics; - int i,j; - int realstart; - uint8_t *cmddata; - bool resendOnly; + return + pr_spawnmobj.Seed() + + pr_acs.Seed() + + pr_chase.Seed() + + pr_damagemobj.Seed(); +} - GC::CheckGC(); - - if (ticdup == 0) +static int16_t CalculateConsistency(int client, uint32_t seed) +{ + if (players[client].mo != nullptr) { + seed += int((players[client].mo->X() + players[client].mo->Y() + players[client].mo->Z()) * 257) + players[client].mo->Angles.Yaw.BAMs() + players[client].mo->Angles.Pitch.BAMs(); + seed ^= players[client].health; + } + + // Zero value consistencies are seen as invalid, so always have a valid value. + return (seed & 0xFFFF) ? seed : 1; +} + +// Ran a tick, so prep the next consistencies to send out. +// [RH] Include some random seeds and player stuff in the consistancy +// check, not just the player's x position like BOOM. +static void MakeConsistencies() +{ + if (!netgame || demoplayback || (gametic % doomcom.ticdup) || !IsMapLoaded()) return; + + const uint32_t rngSum = StaticSumSeeds(); + for (auto client : NetworkClients) + { + auto& clientState = ClientStates[client]; + clientState.LocalConsistency[CurrentConsistency % BACKUPTICS] = CalculateConsistency(client, rngSum); } - // check time - nowtime = I_GetTime (); - newtics = nowtime - gametime; - gametime = nowtime; + ++CurrentConsistency; +} - if (newtics <= 0) // nothing new to update - { - GetPackets (); - return; - } +static bool Net_UpdateStatus() +{ + if (!netgame || demoplayback || NetworkClients.Size() <= 1) + return true; - if (skiptics <= newtics) - { - newtics -= skiptics; - skiptics = 0; - } - else - { - skiptics -= newtics; - newtics = 0; - } + if (LevelStartStatus == LST_WAITING || LevelStartDelay > 0) + return false; - // build new ticcmds for console player - for (i = 0; i < newtics; i++) + // Check against the previous tick in case we're recovering from a huge + // system hiccup. If the game has taken too long to update, it's likely + // another client is hanging up the game. + if (LastEnterTic - LastGameUpdate >= MAXSENDTICS * doomcom.ticdup) { - I_StartTic (); - D_ProcessEvents (); - if (pauseext || (maketic - gametic) / ticdup >= BACKUPTICS/2-1) - break; // can't hold any more - - //Printf ("mk:%i ",maketic); - G_BuildTiccmd (&localcmds[maketic % LOCALCMDTICS]); - maketic++; + // Try again in the next MaxDelay tics. + LastGameUpdate = EnterTic; - if (ticdup == 1 || maketic == 0) + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) { - Net_NewMakeTic (); + // Use a missing packet here to tell the other players to retransmit instead of simply retransmitting our + // own data over instantly. This avoids flooding the network at a time where it's not opportune to do so. + const int curTic = gametic / doomcom.ticdup; + for (auto client : NetworkClients) + { + if (client == consoleplayer) + continue; + + if (ClientStates[client].CurrentSequence < curTic) + { + ClientStates[client].Flags |= CF_MISSING; + players[client].waiting = true; + } + else + { + players[client].waiting = false; + } + } } else { - // Once ticdup tics have been collected, average their movements - // and combine their buttons, since they will all be sent as a - // single tic that gets duplicated ticdup times. Even with ticdup, - // tics are still collected at the normal rate so that, with the - // help of prediction, the game seems as responsive as normal. - if (maketic % ticdup != 0) - { - int mod = maketic - maketic % ticdup; - int j; + // In packet server mode, the client is waiting for data from the host and hasn't recieved it yet. Send + // our data back over in case the host is waiting for us. + ClientStates[Net_Arbitrator].Flags |= CF_MISSING; + players[Net_Arbitrator].waiting = true; + } + } - // Update the buttons for all tics in this ticdup set as soon as - // possible so that the prediction shows jumping as correctly as - // possible. (If you press +jump in the middle of a ticdup set, - // the jump will actually begin at the beginning of the set, not - // in the middle.) - for (j = maketic-2; j >= mod; --j) + if (LevelStartStatus == LST_HOST) + return false; + + for (auto client : NetworkClients) + { + if (players[client].waiting) + return false; + } + + // Wait for the game to stabilize a bit after launch before skipping commands. + bool updated = false; + int lowestDiff = INT_MAX; + if (gametic > TICRATE * 2) + { + if (NetMode != NET_PacketServer) + { + // Check if everyone has a buffer for us. If they do, we're too far ahead. + for (auto client : NetworkClients) + { + if (client != consoleplayer && (ClientStates[client].Flags & CF_UPDATED)) { - localcmds[j % LOCALCMDTICS].ucmd.buttons |= - localcmds[(j + 1) % LOCALCMDTICS].ucmd.buttons; + updated = true; + int diff = ClientStates[client].SequenceAck - ClientStates[client].CurrentSequence; + if (diff < lowestDiff) + lowestDiff = diff; } + + ClientStates[client].Flags &= ~CF_UPDATED; + } + } + else if (consoleplayer == Net_Arbitrator) + { + // If we're consistenty ahead of the highest sequence player, slow down. + const int curTic = ClientTic / doomcom.ticdup; + for (auto client : NetworkClients) + { + if (client != Net_Arbitrator && (ClientStates[client].Flags & CF_UPDATED)) + { + updated = true; + int diff = curTic - ClientStates[client].CurrentSequence; + if (diff < lowestDiff) + lowestDiff = diff; + } + + ClientStates[client].Flags &= ~CF_UPDATED; + } + } + else if (ClientStates[Net_Arbitrator].Flags & CF_UPDATED) + { + // Check if the host is reporting that we're too far ahead of them. + updated = true; + lowestDiff = CommandsAhead; + ClientStates[Net_Arbitrator].Flags &= ~CF_UPDATED; + } + } + + if (updated) + { + if (lowestDiff > 0) + { + if (SkipCommandTimer++ > TICRATE / 2) + { + SkipCommandTimer = 0; + if (SkipCommandAmount <= 0) + SkipCommandAmount = lowestDiff * doomcom.ticdup; + } + } + else + { + SkipCommandTimer = 0; + } + } + + return true; +} + +void NetUpdate(int tics) +{ + GetPackets(); + if (tics <= 0) + return; + + if (netgame && !demoplayback) + { + // If a tic has passed, always send out a heartbeat packet (also doubles as + // a latency measurement tool). + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) + { + LastLatencyUpdate += tics; + if (FullLatencyCycle > 0) + FullLatencyCycle = max(FullLatencyCycle - tics, 0); + + SendHeartbeat(); + + if (LastLatencyUpdate >= MAXSENDTICS) + LastLatencyUpdate = 0; + } + + CheckConsistencies(); + } + + // Sit idle after the level has loaded until everyone is ready to go. This keeps players better + // in sync with each other than relying on tic balancing to speed up/slow down the game and mirrors + // how players would wait for a true server to load. + if (LevelStartStatus != LST_READY) + { + if (LevelStartStatus == LST_WAITING) + { + if (NetworkClients.Size() == 1) + { + // If we got stuck in limbo waiting, force start the map. + CheckLevelStart(Net_Arbitrator, 0); } else { - // Average the ticcmds between these tics to get the - // movement that is actually sent across the network. We - // need to update them in all the localcmds slots that - // are dupped so that prediction works properly. - int mod = maketic - ticdup; - int modp, j; + if (consoleplayer != Net_Arbitrator && IsMapLoaded()) + { + NetBuffer[0] = NCMD_LEVELREADY; + NetBuffer[1] = CurrentLobbyID; + HSendPacket(Net_Arbitrator, 2); + } + } + } + else if (LevelStartStatus == LST_HOST) + { + // If we're the host, idly wait until all packets have arrived. There's no point in predicting since we + // know for a fact the game won't be started until everyone is accounted for. (Packet server only) + const int curTic = gametic / doomcom.ticdup; + int lowestSeq = curTic; + for (auto client : NetworkClients) + { + if (client != Net_Arbitrator && ClientStates[client].CurrentSequence < lowestSeq) + lowestSeq = ClientStates[client].CurrentSequence; + } + + if (lowestSeq >= curTic) + LevelStartStatus = LST_READY; + } + } + else if (LevelStartDelay > 0) + { + if (LevelStartDelay < tics) + tics -= LevelStartDelay; + + LevelStartDelay = max(LevelStartDelay - tics, 0); + } + + const bool netGood = Net_UpdateStatus(); + const int startTic = ClientTic; + tics = min(tics, MAXSENDTICS * doomcom.ticdup); + for (int i = 0; i < tics; ++i) + { + I_StartTic(); + D_ProcessEvents(); + if (pauseext || !netGood) + break; + + if (SkipCommandAmount > 0) + { + --SkipCommandAmount; + continue; + } + + G_BuildTiccmd(&LocalCmds[ClientTic++ % LOCALCMDTICS]); + if (doomcom.ticdup == 1) + { + Net_NewClientTic(); + } + else + { + const int ticDiff = ClientTic % doomcom.ticdup; + if (ticDiff) + { + const int startTic = ClientTic - ticDiff; + + // Even if we're not sending out inputs, update the local commands so that the doomcom.ticdup + // is correctly played back while predicting as best as possible. This will help prevent + // minor hitches when playing online. + for (int j = ClientTic - 1; j > startTic; --j) + LocalCmds[(j - 1) % LOCALCMDTICS].buttons |= LocalCmds[j % LOCALCMDTICS].buttons; + } + else + { + // Gather up the Command across the last doomcom.ticdup number of tics + // and average them out. These are propagated back to the local + // command so that they'll be predicted correctly. + const int lastTic = ClientTic - doomcom.ticdup; + for (int j = ClientTic - 1; j > lastTic; --j) + LocalCmds[(j - 1) % LOCALCMDTICS].buttons |= LocalCmds[j % LOCALCMDTICS].buttons; int pitch = 0; int yaw = 0; @@ -1050,784 +1289,710 @@ void NetUpdate (void) int sidemove = 0; int upmove = 0; - for (j = 0; j < ticdup; ++j) + for (int j = 0; j < doomcom.ticdup; ++j) { - modp = (mod + j) % LOCALCMDTICS; - pitch += localcmds[modp].ucmd.pitch; - yaw += localcmds[modp].ucmd.yaw; - roll += localcmds[modp].ucmd.roll; - forwardmove += localcmds[modp].ucmd.forwardmove; - sidemove += localcmds[modp].ucmd.sidemove; - upmove += localcmds[modp].ucmd.upmove; + const int mod = (lastTic + j) % LOCALCMDTICS; + pitch += LocalCmds[mod].pitch; + yaw += LocalCmds[mod].yaw; + roll += LocalCmds[mod].roll; + forwardmove += LocalCmds[mod].forwardmove; + sidemove += LocalCmds[mod].sidemove; + upmove += LocalCmds[mod].upmove; } - pitch /= ticdup; - yaw /= ticdup; - roll /= ticdup; - forwardmove /= ticdup; - sidemove /= ticdup; - upmove /= ticdup; + pitch /= doomcom.ticdup; + yaw /= doomcom.ticdup; + roll /= doomcom.ticdup; + forwardmove /= doomcom.ticdup; + sidemove /= doomcom.ticdup; + upmove /= doomcom.ticdup; - for (j = 0; j < ticdup; ++j) + for (int j = 0; j < doomcom.ticdup; ++j) { - modp = (mod + j) % LOCALCMDTICS; - localcmds[modp].ucmd.pitch = pitch; - localcmds[modp].ucmd.yaw = yaw; - localcmds[modp].ucmd.roll = roll; - localcmds[modp].ucmd.forwardmove = forwardmove; - localcmds[modp].ucmd.sidemove = sidemove; - localcmds[modp].ucmd.upmove = upmove; + const int mod = (lastTic + j) % LOCALCMDTICS; + LocalCmds[mod].pitch = pitch; + LocalCmds[mod].yaw = yaw; + LocalCmds[mod].roll = roll; + LocalCmds[mod].forwardmove = forwardmove; + LocalCmds[mod].sidemove = sidemove; + LocalCmds[mod].upmove = upmove; } - Net_NewMakeTic (); + Net_NewClientTic(); } } } - if (singletics) - return; // singletic update is synchronous - + const int newestTic = ClientTic / doomcom.ticdup; if (demoplayback) { - resendto[0] = nettics[0] = (maketic / ticdup); - return; // Don't touch netcmd data while playing a demo, as it'll already exist. + // Don't touch net command data while playing a demo, as it'll already exist. + for (auto client : NetworkClients) + ClientStates[client].CurrentSequence = newestTic; + + return; } - // If maketic didn't cross a ticdup boundary, only send packets - // to players waiting for resends. - resendOnly = (maketic / ticdup) == (maketic - i) / ticdup; - - // send the packet to the other nodes - int count = 1; - int quitcount = 0; - - if (consoleplayer == Net_Arbitrator) + int startSequence = startTic / doomcom.ticdup; + int endSequence = newestTic; + int quitters = 0; + int quitNums[MAXPLAYERS]; + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { - if (NetMode == NET_PacketServer) + // In packet server mode special handling is used to ensure the host only + // sends out available tics when ready instead of constantly shotgunning + // them out as they're made locally. + startSequence = gametic / doomcom.ticdup; + int lowestSeq = endSequence - 1; + for (auto client : NetworkClients) { - for (j = 0; j < MAXPLAYERS; j++) - { - if (playeringame[j] && players[j].Bot == NULL) - { - count++; - } - } + if (client == Net_Arbitrator) + continue; - // The loop above added the local player to the count a second time, - // and it also added the player being sent the packet to the count. - count -= 2; - - for (j = 0; j < doomcom.numnodes; ++j) - { - if (nodejustleft[j]) - { - if (count == 0) - { - PlayerIsGone (j, playerfornode[j]); - } - else - { - quitcount++; - } - } - } - - if (count == 0) - { - count = 1; - } + // The host has special handling when disconnecting in a packet server game. + if (ClientStates[client].Flags & CF_QUIT) + quitNums[quitters++] = client; + else if (ClientStates[client].CurrentSequence < lowestSeq) + lowestSeq = ClientStates[client].CurrentSequence; } + + endSequence = lowestSeq + 1; } - for (i = 0; i < doomcom.numnodes; i++) + const bool resendOnly = startSequence == endSequence && (ClientTic % doomcom.ticdup); + for (auto client : NetworkClients) { - uint8_t playerbytes[MAXPLAYERS]; - - if (!nodeingame[i]) - { + // If in packet server mode, we don't want to send information to anyone but the host. On the other + // hand, if we're the host we send out everyone's info to everyone else. + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator && client != Net_Arbitrator) continue; - } - if (NetMode == NET_PacketServer && - consoleplayer != Net_Arbitrator && - i != nodeforplayer[Net_Arbitrator] && - i != 0) - { + + auto& curState = ClientStates[client]; + // If we can only resend, don't send clients any information that they already have. If + // we couldn't generate any commands because we're at the cap, instead send out a heartbeat + // containing the latest command. + if (resendOnly && !(curState.Flags & (CF_RETRANSMIT | CF_MISSING))) continue; - } - if (resendOnly && resendcount[i] <= 0 && !remoteresend[i] && nettics[i]) + + NetBuffer[0] = (curState.Flags & CF_MISSING) ? NCMD_RETRANSMIT : 0; + curState.Flags &= ~CF_MISSING; + + NetBuffer[1] = (curState.Flags & CF_RETRANSMIT_SEQ) ? curState.ResendID : CurrentLobbyID; + // Last sequence we got from this client. + NetBuffer[2] = (curState.CurrentSequence >> 24); + NetBuffer[3] = (curState.CurrentSequence >> 16); + NetBuffer[4] = (curState.CurrentSequence >> 8); + NetBuffer[5] = curState.CurrentSequence; + // Last consistency we got from this client. + NetBuffer[6] = (curState.CurrentNetConsistency >> 24); + NetBuffer[7] = (curState.CurrentNetConsistency >> 16); + NetBuffer[8] = (curState.CurrentNetConsistency >> 8); + NetBuffer[9] = curState.CurrentNetConsistency; + + size_t size = 10; + if (quitters > 0) { - continue; + NetBuffer[0] |= NCMD_QUITTERS; + NetBuffer[size++] = quitters; + for (int i = 0; i < quitters; ++i) + NetBuffer[size++] = quitNums[i]; } - int numtics; - int k; - - lowtic = maketic / ticdup; - - netbuffer[0] = 0; - netbuffer[1] = realstart = resendto[i]; - k = 2; - - if (NetMode == NET_PacketServer && - consoleplayer == Net_Arbitrator && - i != 0) + int playerNums[MAXPLAYERS]; + int playerCount = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? NetworkClients.Size() : 1; + NetBuffer[size++] = playerCount; + if (playerCount > 1) { - for (j = 1; j < doomcom.numnodes; ++j) - { - if (nodeingame[j] && nettics[j] < lowtic && j != i) - { - lowtic = nettics[j]; - } - } - netbuffer[k++] = lowtic; - } - - numtics = max(0, lowtic - realstart); - if (numtics > BACKUPTICS) - I_Error ("NetUpdate: Node %d missed too many tics", i); - - switch (net_extratic) - { - case 0: - default: - resendto[i] = lowtic; break; - case 1: resendto[i] = max(0, lowtic - 1); break; - case 2: resendto[i] = nettics[i]; break; - } - - if (numtics == 0 && resendOnly && !remoteresend[i] && nettics[i]) - { - continue; - } - - if (remoteresend[i]) - { - netbuffer[0] |= NCMD_RETRANSMIT; - netbuffer[k++] = nettics[i]; - } - - if (numtics < 3) - { - netbuffer[0] |= numtics; + int i = 0; + for (auto cl : NetworkClients) + playerNums[i++] = cl; } else { - netbuffer[0] |= NCMD_XTICS; - netbuffer[k++] = numtics - 3; + playerNums[0] = consoleplayer; } - if (quitcount > 0) + // Only send over our newest commands. If a client missed one, they'll let us know. + if (curState.Flags & CF_RETRANSMIT_SEQ) { - netbuffer[0] |= NCMD_QUITTERS; - netbuffer[k++] = quitcount; - for (int l = 0; l < doomcom.numnodes; ++l) + curState.Flags &= ~CF_RETRANSMIT_SEQ; + if (curState.ResendSequenceFrom < 0) + curState.ResendSequenceFrom = curState.SequenceAck + 1; + } + + const int sequenceNum = curState.ResendSequenceFrom >= 0 ? curState.ResendSequenceFrom : startSequence; + const int numTics = clamp(endSequence - sequenceNum, 0, MAXSENDTICS); + if (curState.ResendSequenceFrom >= 0) + { + curState.ResendSequenceFrom += numTics; + if (curState.ResendSequenceFrom >= endSequence) + curState.ResendSequenceFrom = -1; + } + + NetBuffer[size++] = numTics; + if (numTics > 0) + { + NetBuffer[size++] = (sequenceNum >> 24); + NetBuffer[size++] = (sequenceNum >> 16); + NetBuffer[size++] = (sequenceNum >> 8); + NetBuffer[size++] = sequenceNum; + } + + if (curState.Flags & CF_RETRANSMIT_CON) + { + curState.Flags &= ~CF_RETRANSMIT_CON; + if (curState.ResendConsistencyFrom < 0) + curState.ResendConsistencyFrom = curState.ConsistencyAck + 1; + } + + const int baseConsistency = curState.ResendConsistencyFrom >= 0 ? curState.ResendConsistencyFrom : LastSentConsistency; + // Don't bother sending over consistencies in packet server unless you're the host. + int ran = 0; + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) + { + ran = clamp(CurrentConsistency - baseConsistency, 0, MAXSENDTICS); + if (curState.ResendConsistencyFrom >= 0) { - if (nodejustleft[l]) + curState.ResendConsistencyFrom += ran; + if (curState.ResendConsistencyFrom >= CurrentConsistency) + curState.ResendConsistencyFrom = -1; + } + } + + NetBuffer[size++] = ran; + if (ran > 0) + { + NetBuffer[size++] = (baseConsistency >> 24); + NetBuffer[size++] = (baseConsistency >> 16); + NetBuffer[size++] = (baseConsistency >> 8); + NetBuffer[size++] = baseConsistency; + } + + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence - newestTic, 0); + + // Client commands. + + uint8_t* cmd = &NetBuffer[size]; + for (int i = 0; i < playerCount; ++i) + { + cmd[0] = playerNums[i]; + ++cmd; + + auto& clientState = ClientStates[playerNums[i]]; + // Time used to track latency since in packet server mode we want each + // client's latency to the server itself. + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + { + cmd[0] = (clientState.AverageLatency >> 8); + ++cmd; + cmd[0] = clientState.AverageLatency; + ++cmd; + } + + for (int r = 0; r < ran; ++r) + { + cmd[0] = r; + ++cmd; + const int tic = (baseConsistency + r) % BACKUPTICS; + cmd[0] = (clientState.LocalConsistency[tic] >> 8); + ++cmd; + cmd[0] = clientState.LocalConsistency[tic]; + ++cmd; + } + + for (int t = 0; t < numTics; ++t) + { + cmd[0] = t; + ++cmd; + + int curTic = sequenceNum + t, lastTic = curTic - 1; + if (playerNums[i] == consoleplayer) { - netbuffer[k++] = playerfornode[l]; + int realTic = (curTic * doomcom.ticdup) % LOCALCMDTICS; + int realLastTic = (lastTic * doomcom.ticdup) % LOCALCMDTICS; + // Write out the net events before the user commands so inputs can + // be used as a marker for when the given command ends. + auto& stream = NetEvents.Streams[curTic % BACKUPTICS]; + if (stream.Used) + { + memcpy(cmd, stream.Stream, stream.Used); + cmd += stream.Used; + } + + WriteUserCmdMessage(LocalCmds[realTic], + realLastTic >= 0 ? &LocalCmds[realLastTic] : nullptr, cmd); + } + else + { + auto& netTic = clientState.Tics[curTic % BACKUPTICS]; + + int len; + uint8_t* data = netTic.Data.GetData(&len); + if (data != nullptr) + { + memcpy(cmd, data, len); + cmd += len; + } + + WriteUserCmdMessage(netTic.Command, + lastTic >= 0 ? &clientState.Tics[lastTic % BACKUPTICS].Command : nullptr, cmd); } } } - // Send current network delay - // The number of tics we just made should be removed from the count. - netbuffer[k++] = ((maketic - numtics - gametic) / ticdup); - - if (numtics > 0) - { - int l; - - if (count > 1 && i != 0 && consoleplayer == Net_Arbitrator) - { - netbuffer[0] |= NCMD_MULTI; - netbuffer[k++] = count; - - if (NetMode == NET_PacketServer) - { - for (l = 1, j = 0; j < MAXPLAYERS; j++) - { - if (playeringame[j] && players[j].Bot == NULL && j != playerfornode[i] && j != consoleplayer) - { - playerbytes[l++] = j; - netbuffer[k++] = j; - } - } - } - } - - cmddata = &netbuffer[k]; - - for (l = 0; l < count; ++l) - { - for (j = 0; j < numtics; j++) - { - int start = realstart + j, prev = start - 1; - int localstart, localprev; - - localstart = (start * ticdup) % LOCALCMDTICS; - localprev = (prev * ticdup) % LOCALCMDTICS; - start %= BACKUPTICS; - prev %= BACKUPTICS; - - // The local player has their tics sent first, followed by - // the other players. - if (l == 0) - { - WriteInt16 (localcmds[localstart].consistancy, &cmddata); - // [RH] Write out special "ticcmds" before real ticcmd - if (specials.used[start]) - { - memcpy (cmddata, specials.streams[start], specials.used[start]); - cmddata += specials.used[start]; - } - WriteUserCmdMessage (&localcmds[localstart].ucmd, - localprev >= 0 ? &localcmds[localprev].ucmd : NULL, &cmddata); - } - else if (i != 0) - { - int len; - uint8_t *spec; - - WriteInt16 (netcmds[playerbytes[l]][start].consistancy, &cmddata); - spec = NetSpecs[playerbytes[l]][start].GetData (&len); - if (spec != NULL) - { - memcpy (cmddata, spec, len); - cmddata += len; - } - - WriteUserCmdMessage (&netcmds[playerbytes[l]][start].ucmd, - prev >= 0 ? &netcmds[playerbytes[l]][prev].ucmd : NULL, &cmddata); - } - } - } - HSendPacket (i, int(cmddata - netbuffer)); - } - else - { - HSendPacket (i, k); - } + HSendPacket(client, int(cmd - NetBuffer)); + if (client != consoleplayer && net_extratic) + HSendPacket(client, int(cmd - NetBuffer)); } - // listen for other packets - GetPackets (); - + // Update this now that all the packets have been sent out. if (!resendOnly) - { - // ideally nettics[0] should be 1 - 3 tics above lowtic - // if we are consistantly slower, speed up time + LastSentConsistency = CurrentConsistency; - // [RH] I had erroneously assumed frameskip[] had 4 entries - // because there were 4 players, but that's not the case at - // all. The game is comparing the lag behind the master for - // four runs of TryRunTics. If our tic count is ahead of the - // master all 4 times, the next run of NetUpdate will not - // process any new input. If we have less input than the - // master, the next run of NetUpdate will process extra tics - // (because gametime gets decremented here). - - // the key player does not adapt - if (consoleplayer != Net_Arbitrator) - { - // I'm not sure about this when using a packet server, because - // if left unmodified from the P2P version, it can make the game - // very jerky. The way I have it written right now basically means - // that it won't adapt. Fortunately, player prediction helps - // alleviate the lag somewhat. - - if (NetMode == NET_PeerToPeer) - { - int totalavg = 0; - if (net_ticbalance) - { - // Try to guess ahead the time it takes to send responses to the slowest node - int nodeavg = 0, arbavg = 0; - - for (j = 0; j < BACKUPTICS; j++) - { - arbavg += netdelay[nodeforplayer[Net_Arbitrator]][j]; - nodeavg += netdelay[0][j]; - } - arbavg /= BACKUPTICS; - nodeavg /= BACKUPTICS; - - // We shouldn't adapt if we are already the arbitrator isn't what we are waiting for, otherwise it just adds more latency - if (arbavg > nodeavg) - { - lastaverage = totalavg = ((arbavg + nodeavg) / 2); - } - else - { - // Allow room to guess two tics ahead - if (nodeavg > (arbavg + 2) && lastaverage > 0) - lastaverage--; - totalavg = lastaverage; - } - } - - mastertics = nettics[nodeforplayer[Net_Arbitrator]] + totalavg; - } - if (nettics[0] <= mastertics) - { - gametime--; - if (debugfile) fprintf(debugfile, "-"); - } - if (NetMode != NET_PacketServer) - { - frameskip[(maketic / ticdup) & 3] = (oldnettics > mastertics); - } - else - { - frameskip[(maketic / ticdup) & 3] = (oldnettics - mastertics) > 3; - } - if (frameskip[0] && frameskip[1] && frameskip[2] && frameskip[3]) - { - skiptics = 1; - if (debugfile) fprintf(debugfile, "+"); - } - oldnettics = nettics[0]; - } - } + // Listen for other packets. This has to also come after sending so the player that sent + // data to themselves gets it immediately (important for singleplayer, otherwise there + // would always be a one-tic delay). + GetPackets(); } - -// -// D_ArbitrateNetStart -// // User info packets look like this: // -// 0 One byte set to NCMD_SETUP or NCMD_SETUP+1; if NCMD_SETUP+1, omit byte 9 -// 1 One byte for the player's number -//2-4 Three bytes for the game version (255,high byte,low byte) -//5-8 A bit mask for each player the sender knows about -// 9 The high bit is set if the sender got the game info -// 10 A stream of bytes with the user info +// One byte set to NCMD_SETUP or NCMD_USERINFO +// One byte for the relevant player number. +// Three bytes for the sender's game version (major, minor, revision). +// Four bytes for the bit mask indicating which players the sender knows about. +// If NCMD_SETUP, one byte indicating the guest got the game setup info. +// Player's user information. // // The guests always send NCMD_SETUP packets, and the host always -// sends NCMD_SETUP+1 packets. +// sends NCMD_USERINFO packets. // // Game info packets look like this: // -// 0 One byte set to NCMD_SETUP+2 -// 1 One byte for ticdup setting -// 2 One byte for NetMode setting -// 3 String with starting map's name -// . Four bytes for the RNG seed -// . Stream containing remaining game info +// One byte set to NCMD_GAMEINFO. +// One byte for doomcom.ticdup setting. +// One byte for NetMode setting. +// String with starting map's name. +// The game's RNG seed. +// Remaining game information. // -// Finished packet looks like this: +// Ready packets look like this: // -// 0 One byte set to NCMD_SETUP+3 +// One byte set to NCMD_GAMEREADY. // // Each machine sends user info packets to the host. The host sends user // info packets back to the other machines as well as game info packets. // Negotiation is done when all the guests have reported to the host that // they know about the other nodes. -struct ArbitrateData +bool ExchangeNetGameInfo(void *userdata) { - uint32_t playersdetected[MAXNETNODES]; - uint8_t gotsetup[MAXNETNODES]; -}; - -bool DoArbitrate (void *userdata) -{ - ArbitrateData *data = (ArbitrateData *)userdata; - uint8_t *stream; - int version; - int node; - int i, j; - - while (HGetPacket ()) + FNetGameInfo *data = reinterpret_cast(userdata); + uint8_t *stream = nullptr; + while (HGetPacket()) { - if (netbuffer[0] == NCMD_EXIT) + // For now throw an error until something more complex can be done to handle this case. + if (NetBuffer[0] == NCMD_EXIT) + I_Error("Game unexpectedly ended."); + + if (NetBuffer[0] == NCMD_SETUP || NetBuffer[0] == NCMD_USERINFO) { - I_FatalError ("The game was aborted."); - } + int clientNum = NetBuffer[1]; + data->DetectedPlayers[clientNum] = + (NetBuffer[5] << 24) | (NetBuffer[6] << 16) | (NetBuffer[7] << 8) | NetBuffer[8]; - if (doomcom.remotenode == 0) - { - continue; - } - - if (netbuffer[0] == NCMD_SETUP || netbuffer[0] == NCMD_SETUP+1) // got user info - { - node = (netbuffer[0] == NCMD_SETUP) ? doomcom.remotenode : nodeforplayer[netbuffer[1]]; - - data->playersdetected[node] = - (netbuffer[5] << 24) | (netbuffer[6] << 16) | (netbuffer[7] << 8) | netbuffer[8]; - - if (netbuffer[0] == NCMD_SETUP) - { // Sent to host - data->gotsetup[node] = netbuffer[9] & 0x80; - stream = &netbuffer[10]; + if (NetBuffer[0] == NCMD_SETUP) + { + // Sent from guest. + data->GotSetup[clientNum] = NetBuffer[9]; + stream = &NetBuffer[10]; } else - { // Sent from host - stream = &netbuffer[9]; - } - - D_ReadUserInfoStrings (netbuffer[1], &stream, false); - if (!nodeingame[node]) { - version = (netbuffer[2] << 16) | (netbuffer[3] << 8) | netbuffer[4]; - if (version != (0xFF0000 | NETGAMEVERSION)) - { - I_Error ("Different " GAMENAME " versions cannot play a net game"); - } + // Sent from host. + stream = &NetBuffer[9]; + } - playeringame[netbuffer[1]] = true; - nodeingame[node] = true; + D_ReadUserInfoStrings(clientNum, &stream, false); + if (!NetworkClients.InGame(clientNum)) + { + if (NetBuffer[2] != VER_MAJOR % 255 || NetBuffer[3] != VER_MINOR % 255 || NetBuffer[4] != VER_REVISION % 255) + I_Error("Different " GAMENAME " versions cannot play a net game"); - data->playersdetected[0] |= 1 << netbuffer[1]; + NetworkClients += clientNum; + data->DetectedPlayers[consoleplayer] |= 1 << clientNum; - I_NetMessage ("Found %s (node %d, player %d)", - players[netbuffer[1]].userinfo.GetName(), - node, netbuffer[1]+1); + I_NetMessage("Found %s (%d)", players[clientNum].userinfo.GetName(), clientNum); } } - else if (netbuffer[0] == NCMD_SETUP+2) // got game info + else if (NetBuffer[0] == NCMD_GAMEINFO) { - data->gotsetup[0] = 0x80; + data->GotSetup[consoleplayer] = true; - ticdup = doomcom.ticdup = clamp(netbuffer[1], 1, MAXTICDUP); - NetMode = netbuffer[2]; + doomcom.ticdup = clamp(NetBuffer[1], 1, MAXTICDUP); + NetMode = static_cast(NetBuffer[2]); - stream = &netbuffer[3]; + stream = &NetBuffer[3]; startmap = ReadStringConst(&stream); - rngseed = ReadInt32 (&stream); - C_ReadCVars (&stream); + rngseed = ReadInt32(&stream); + C_ReadCVars(&stream); } - else if (netbuffer[0] == NCMD_SETUP+3) + else if (NetBuffer[0] == NCMD_GAMEREADY) { return true; } } - // If everybody already knows everything, it's time to go + // If everybody already knows everything, it's time to start. if (consoleplayer == Net_Arbitrator) { - for (i = 0; i < doomcom.numnodes; ++i) - if (data->playersdetected[i] != uint32_t(1 << doomcom.numnodes) - 1 || !data->gotsetup[i]) - break; + bool stillWaiting = false; - if (i == doomcom.numnodes) + // Update this dynamically. + uint32_t allPlayers = 0u; + for (int i = 0; i < doomcom.numplayers; ++i) + allPlayers |= 1 << i; + + for (int i = 0; i < doomcom.numplayers; ++i) + { + if (!data->GotSetup[i] || (data->DetectedPlayers[i] & allPlayers) != allPlayers) + { + stillWaiting = true; + break; + } + } + + if (!stillWaiting) return true; } - netbuffer[2] = 255; - netbuffer[3] = (NETGAMEVERSION >> 8) & 255; - netbuffer[4] = NETGAMEVERSION & 255; - netbuffer[5] = data->playersdetected[0] >> 24; - netbuffer[6] = data->playersdetected[0] >> 16; - netbuffer[7] = data->playersdetected[0] >> 8; - netbuffer[8] = data->playersdetected[0]; + NetBuffer[2] = VER_MAJOR % 255; + NetBuffer[3] = VER_MINOR % 255; + NetBuffer[4] = VER_REVISION % 255; + NetBuffer[5] = data->DetectedPlayers[consoleplayer] >> 24; + NetBuffer[6] = data->DetectedPlayers[consoleplayer] >> 16; + NetBuffer[7] = data->DetectedPlayers[consoleplayer] >> 8; + NetBuffer[8] = data->DetectedPlayers[consoleplayer]; if (consoleplayer != Net_Arbitrator) - { // Send user info for the local node - netbuffer[0] = NCMD_SETUP; - netbuffer[1] = consoleplayer; - netbuffer[9] = data->gotsetup[0]; - stream = &netbuffer[10]; - auto str = D_GetUserInfoStrings (consoleplayer, true); - memcpy(stream, str.GetChars(), str.Len() + 1); - stream += str.Len(); - SendSetup (data->playersdetected, data->gotsetup, int(stream - netbuffer)); + { + // If we're a guest, send our info over to the host. + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = consoleplayer; + NetBuffer[9] = data->GotSetup[consoleplayer]; + stream = &NetBuffer[10]; + // If the host already knows we're here, just send over a heartbeat. + if (!(data->DetectedPlayers[Net_Arbitrator] & (1 << consoleplayer))) + { + auto str = D_GetUserInfoStrings(consoleplayer, true); + const size_t userSize = str.Len() + 1; + memcpy(stream, str.GetChars(), userSize); + stream += userSize; + } + else + { + *stream = 0; + ++stream; + } } else - { // Send user info for all nodes - netbuffer[0] = NCMD_SETUP+1; - for (i = 1; i < doomcom.numnodes; ++i) + { + // If we're the host, send over known player data to guests. This is done instantly + // since the game info will also get sent out after this. + NetBuffer[0] = NCMD_USERINFO; + for (auto client : NetworkClients) { - for (j = 0; j < doomcom.numnodes; ++j) + if (client == Net_Arbitrator) + continue; + + for (auto cl : NetworkClients) { - // Send info about player j to player i? - if ((data->playersdetected[0] & (1<playersdetected[i] & (1<DetectedPlayers[Net_Arbitrator] & clBit) && !(data->DetectedPlayers[client] & clBit)) { - netbuffer[1] = j; - stream = &netbuffer[9]; - auto str = D_GetUserInfoStrings(j, true); - memcpy(stream, str.GetChars(), str.Len() + 1); - stream += str.Len(); - HSendPacket (i, int(stream - netbuffer)); + NetBuffer[1] = cl; + stream = &NetBuffer[9]; + auto str = D_GetUserInfoStrings(cl, true); + const size_t userSize = str.Len() + 1; + memcpy(stream, str.GetChars(), userSize); + stream += userSize; + HSendPacket(client, int(stream - NetBuffer)); } } } + + // If we're the host, send the game info too. + NetBuffer[0] = NCMD_GAMEINFO; + NetBuffer[1] = (uint8_t)doomcom.ticdup; + NetBuffer[2] = NetMode; + stream = &NetBuffer[3]; + WriteString(startmap.GetChars(), &stream); + WriteInt32(rngseed, &stream); + C_WriteCVars(&stream, CVAR_SERVERINFO, true); } - // If we're the host, send the game info, too - if (consoleplayer == Net_Arbitrator) - { - netbuffer[0] = NCMD_SETUP+2; - netbuffer[1] = (uint8_t)doomcom.ticdup; - netbuffer[2] = NetMode; - stream = &netbuffer[3]; - WriteString (startmap.GetChars(), &stream); - WriteInt32 (rngseed, &stream); - C_WriteCVars (&stream, CVAR_SERVERINFO, true); - - SendSetup (data->playersdetected, data->gotsetup, int(stream - netbuffer)); - } + SendSetup(*data, int(stream - NetBuffer)); return false; } -bool D_ArbitrateNetStart (void) +static bool D_ExchangeNetInfo() { - ArbitrateData data; - int i; - - // Return right away if we're just playing with ourselves. - if (doomcom.numnodes == 1) + // Return right away if it's just a solo net game. + if (doomcom.numplayers == 1) return true; autostart = true; - memset (data.playersdetected, 0, sizeof(data.playersdetected)); - memset (data.gotsetup, 0, sizeof(data.gotsetup)); - - // The arbitrator knows about himself, but the other players must - // be told about themselves, in case the host had to adjust their - // userinfo (e.g. assign them to a different team). + FNetGameInfo info = {}; + info.DetectedPlayers[consoleplayer] = 1 << consoleplayer; + info.GotSetup[consoleplayer] = consoleplayer == Net_Arbitrator; + NetworkClients += consoleplayer; + if (consoleplayer == Net_Arbitrator) - { - data.playersdetected[0] = 1 << consoleplayer; - } - - // Assign nodes to players. The local player is always node 0. - // If the local player is not the host, then the host is node 1. - // Any remaining players are assigned node numbers in the order - // they were detected. - playerfornode[0] = consoleplayer; - nodeforplayer[consoleplayer] = 0; - if (consoleplayer == Net_Arbitrator) - { - for (i = 1; i < doomcom.numnodes; ++i) - { - playerfornode[i] = i; - nodeforplayer[i] = i; - } - } + I_NetInit("Sending game information", 1); else - { - playerfornode[1] = 0; - nodeforplayer[0] = 1; - for (i = 1; i < doomcom.numnodes; ++i) - { - if (i < consoleplayer) - { - playerfornode[i+1] = i; - nodeforplayer[i] = i+1; - } - else if (i > consoleplayer) - { - playerfornode[i] = i; - nodeforplayer[i] = i; - } - } - } + I_NetInit("Waiting for host information", 1); - if (consoleplayer == Net_Arbitrator) - { - data.gotsetup[0] = 0x80; - } - - I_NetInit ("Exchanging game information", 1); - if (!I_NetLoop (DoArbitrate, &data)) - { + if (!I_NetLoop(ExchangeNetGameInfo, &info)) return false; - } + // Let everyone else know the game is ready to start. if (consoleplayer == Net_Arbitrator) { - netbuffer[0] = NCMD_SETUP+3; - SendSetup (data.playersdetected, data.gotsetup, 1); - } - - if (debugfile) - { - for (i = 0; i < doomcom.numnodes; ++i) + NetBuffer[0] = NCMD_GAMEREADY; + for (auto client : NetworkClients) { - fprintf (debugfile, "player %d is on node %d\n", i, nodeforplayer[i]); + if (client != Net_Arbitrator) + HSendPacket(client, 1); } } + I_NetDone(); return true; } -static void SendSetup (uint32_t playersdetected[MAXNETNODES], uint8_t gotsetup[MAXNETNODES], int len) +static void SendSetup(const FNetGameInfo& info, int len) { if (consoleplayer != Net_Arbitrator) { - if (playersdetected[1] & (1 << consoleplayer)) - { - HSendPacket (1, 10); - } - else - { - HSendPacket (1, len); - } + HSendPacket(Net_Arbitrator, len); } else { - for (int i = 1; i < doomcom.numnodes; ++i) + for (auto client : NetworkClients) { - if (!gotsetup[i] || netbuffer[0] == NCMD_SETUP+3) - { - HSendPacket (i, len); - } + // Only send game info over to clients still needing it. + if (client != Net_Arbitrator && !info.GotSetup[client]) + HSendPacket(client, len); } } } -// -// D_CheckNetGame -// Works out player numbers among the net participants -// - -bool D_CheckNetGame (void) +// Connects players to each other if needed. +bool D_CheckNetGame() { - const char *v; - int i; - - for (i = 0; i < MAXNETNODES; i++) - { - nodeingame[i] = false; - nettics[i] = 0; - remoteresend[i] = false; // set when local needs tics - resendto[i] = 0; // which tic to start sending - } - - // Packet server has proven to be rather slow over the internet. Print a warning about it. - v = Args->CheckValue("-netmode"); - if (v != NULL && (atoi(v) != 0)) - { - Printf(TEXTCOLOR_YELLOW "Notice: Using PacketServer (netmode 1) over the internet is prone to running too slow on some internet configurations." - "\nIf the game is running well below expected speeds, use netmode 0 (P2P) instead.\n"); - } - - int result = I_InitNetwork (); - // I_InitNetwork sets doomcom and netgame + const char* v = Args->CheckValue("-netmode"); + int result = I_InitNetwork(); // I_InitNetwork sets doomcom and netgame if (result == -1) - { return false; - } - else if (result > 0) - { - // For now, stop auto selecting PacketServer, as it's more likely to cause confusion. - //NetMode = NET_PacketServer; - } - if (doomcom.id != DOOMCOM_ID) - { - I_FatalError ("Doomcom buffer invalid!"); - } - players[0].settings_controller = true; + else if (result > 0 && v == nullptr) // Don't override manual netmode setting. + NetMode = NET_PacketServer; + if (doomcom.id != DOOMCOM_ID) + I_FatalError("Invalid doomcom id set for network buffer"); + + players[Net_Arbitrator].settings_controller = true; consoleplayer = doomcom.consoleplayer; if (consoleplayer == Net_Arbitrator) { - v = Args->CheckValue("-netmode"); - if (v != NULL) - { - NetMode = atoi(v) != 0 ? NET_PacketServer : NET_PeerToPeer; - } - if (doomcom.numnodes > 1) + if (v != nullptr) + NetMode = atoi(v) ? NET_PacketServer : NET_PeerToPeer; + + if (doomcom.numplayers > 1) { Printf("Selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode. (%s)\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server", - v != NULL ? "forced" : "auto"); + v != nullptr ? "forced" : "auto"); } if (Args->CheckParm("-extratic")) - { - net_extratic = 1; - } + net_extratic = true; } // [RH] Setup user info - D_SetupUserInfo (); - - if (Args->CheckParm ("-debugfile")) - { - char filename[20]; - mysnprintf (filename, countof(filename), "debug%i.txt", consoleplayer); - Printf ("debug output to: %s\n", filename); - debugfile = fopen (filename, "w"); - } + D_SetupUserInfo(); if (netgame) { - GameConfig->ReadNetVars (); // [RH] Read network ServerInfo cvars - if (!D_ArbitrateNetStart ()) return false; + GameConfig->ReadNetVars(); // [RH] Read network ServerInfo cvars + if (!D_ExchangeNetInfo()) + return false; } - // read values out of doomcom - ticdup = doomcom.ticdup; + for (auto client : NetworkClients) + playeringame[client] = true; - for (i = 0; i < doomcom.numplayers; i++) - playeringame[i] = true; - for (i = 0; i < doomcom.numnodes; i++) - nodeingame[i] = true; + if (consoleplayer != Net_Arbitrator && doomcom.numplayers > 1) + Printf("Host selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode.\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); - if (consoleplayer != Net_Arbitrator && doomcom.numnodes > 1) - { - Printf("Arbitrator selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode.\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); - } - - if (!batchrun) Printf ("player %i of %i (%i nodes)\n", - consoleplayer+1, doomcom.numplayers, doomcom.numnodes); + Printf("player %d of %d\n", consoleplayer + 1, doomcom.numplayers); return true; } - // // D_QuitNetGame // Called before quitting to leave a net game // without hanging the other players // -void D_QuitNetGame (void) +void D_QuitNetGame() { - int i, j, k; - - if (!netgame || !usergame || consoleplayer == -1 || demoplayback) + if (!netgame || !usergame || consoleplayer == -1 || demoplayback || NetworkClients.Size() == 1) return; - // send a bunch of packets for security - netbuffer[0] = NCMD_EXIT; - netbuffer[1] = 0; - - k = 2; + // Send a bunch of packets for stability. + NetBuffer[0] = NCMD_EXIT; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { - uint8_t *foo = &netbuffer[2]; - - // Let the new arbitrator know what resendto counts to use - - for (i = 0; i < MAXPLAYERS; ++i) + // This currently isn't much different from the regular P2P code, but it's being split off into its + // own branch should proper host migration be added in the future (i.e. sending over stored event + // data rather than just dropping it entirely). + int nextHost = 0; + for (auto client : NetworkClients) { - if (playeringame[i] && i != consoleplayer) - WriteInt32 (resendto[nodeforplayer[i]], &foo); + if (client != Net_Arbitrator) + { + nextHost = client; + break; + } } - k = int(foo - netbuffer); - } - for (i = 0; i < 4; i++) + NetBuffer[1] = nextHost; + for (int i = 0; i < 4; ++i) + { + for (auto client : NetworkClients) + { + if (client != Net_Arbitrator) + HSendPacket(client, 2); + } + + I_WaitVBL(1); + } + } + else { - if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + for (int i = 0; i < 4; ++i) { - HSendPacket (nodeforplayer[Net_Arbitrator], 2); + // If in packet server mode, only the host should know about this + // information. + if (NetMode == NET_PacketServer) + { + HSendPacket(Net_Arbitrator, 1); + } + else + { + for (auto client : NetworkClients) + { + if (client != consoleplayer) + HSendPacket(client, 1); + } + } + + I_WaitVBL(1); } - else - { - for (j = 1; j < doomcom.numnodes; j++) - if (nodeingame[j]) - HSendPacket (j, k); - } - I_WaitVBL (1); + } +} + +ADD_STAT(network) +{ + FString out = {}; + if (!netgame || demoplayback) + { + out.AppendFormat("No network stats available."); + return out; } - if (debugfile) - fclose (debugfile); + out.AppendFormat("Max players: %d\tNet mode: %s\tTic dup: %d", + doomcom.numplayers, + NetMode == NET_PacketServer ? "Packet server" : "Peer to peer", + doomcom.ticdup); + + if (net_extratic) + out.AppendFormat("\tExtra tic enabled"); + + out.AppendFormat("\nWorld tic: %06d (sequence %06d)", gametic, gametic / doomcom.ticdup); + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + out.AppendFormat("\tStart tics delay: %d", LevelStartDebug); + + const int delay = max((ClientTic - gametic) / doomcom.ticdup, 0); + const int msDelay = min(delay * doomcom.ticdup * 1000.0 / TICRATE, 999); + out.AppendFormat("\nLocal\n\tIs arbitrator: %d\tDelay: %02d (%03dms)", + consoleplayer == Net_Arbitrator, + delay, msDelay); + + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + out.AppendFormat("\tAvg latency: %03ums", min(ClientStates[consoleplayer].AverageLatency, 999u)); + + if (LevelStartStatus != LST_READY) + { + if (LevelStartStatus == LST_HOST) + out.AppendFormat("\tWaiting for packets"); + else if (consoleplayer == Net_Arbitrator) + out.AppendFormat("\tWaiting for acks"); + else + out.AppendFormat("\tWaiting for arbitrator"); + } + + int lowestSeq = ClientTic / doomcom.ticdup; + for (auto client : NetworkClients) + { + if (client == consoleplayer) + continue; + + auto& state = ClientStates[client]; + if (state.CurrentSequence < lowestSeq) + lowestSeq = state.CurrentSequence; + + out.AppendFormat("\n%s", players[client].userinfo.GetName(12)); + if (client == Net_Arbitrator) + out.AppendFormat("\t(Host)"); + + if ((state.Flags & CF_RETRANSMIT) == CF_RETRANSMIT) + out.AppendFormat("\t(RT)"); + else if (state.Flags & CF_RETRANSMIT_SEQ) + out.AppendFormat("\t(RT SEQ)"); + else if (state.Flags & CF_RETRANSMIT_CON) + out.AppendFormat("\t(RT CON)"); + + if ((state.Flags & CF_MISSING) == CF_MISSING) + out.AppendFormat("\t(MISS)"); + else if (state.Flags & CF_MISSING_SEQ) + out.AppendFormat("\t(MISS SEQ)"); + else if (state.Flags & CF_MISSING_CON) + out.AppendFormat("\t(MISS CON)"); + + out.AppendFormat("\n"); + + if (NetMode != NET_PacketServer) + { + const int cDelay = max(state.CurrentSequence - (gametic / doomcom.ticdup), 0); + const int mscDelay = min(cDelay * doomcom.ticdup * 1000.0 / TICRATE, 999); + out.AppendFormat("\tDelay: %02d (%03dms)", cDelay, mscDelay); + } + + out.AppendFormat("\tAck: %06d\tConsistency: %06d", state.SequenceAck, state.ConsistencyAck); + if (NetMode != NET_PacketServer || client != Net_Arbitrator) + out.AppendFormat("\tAvg latency: %03ums", min(state.AverageLatency, 999u)); + } + + if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) + out.AppendFormat("\nAvailable tics: %03d", max(lowestSeq - (gametic / doomcom.ticdup), 0)); + return out; } // Forces playsim processing time to be consistent across frames. @@ -1871,254 +2036,184 @@ static void TicStabilityEnd() stabilityticduration = min(stabilityendtime - stabilitystarttime, (uint64_t)1'000'000); } +// Don't stabilize tics that are going to have incredibly long pauses in them. +static bool ShouldStabilizeTick() +{ + return gameaction != ga_recordgame && gameaction != ga_newgame && gameaction != ga_newgame2 + && gameaction != ga_loadgame && gameaction != ga_loadgamehidecon && gameaction != ga_autoloadgame && gameaction != ga_loadgameplaydemo + && gameaction != ga_savegame && gameaction != ga_autosave + && gameaction != ga_worlddone && gameaction != ga_completed && gameaction != ga_screenshot && gameaction != ga_fullconsole; +} + // // TryRunTics // -void TryRunTics (void) +void TryRunTics() { - int i; - int lowtic; - int realtics; - int availabletics; - int counts; - int numplaying; - - bool doWait = (cl_capfps || pauseext || (r_NoInterpolate && !M_IsAnimated())); - - if (vid_dontdowait && ((vid_maxfps > 0) || (vid_vsync == true))) + GC::CheckGC(); + + bool doWait = (cl_capfps || pauseext || (!netgame && r_NoInterpolate && !M_IsAnimated())); + if (vid_dontdowait && (vid_maxfps > 0 || vid_vsync)) doWait = false; - - if (!AppActive && vid_lowerinbackground) + if (!netgame && !AppActive && vid_lowerinbackground) doWait = true; - // get real tics + // Get the full number of tics the client can run. if (doWait) - { - entertic = I_WaitForTic (oldentertics); - } + EnterTic = I_WaitForTic(LastEnterTic); else - { - entertic = I_GetTime (); - } - realtics = entertic - oldentertics; - oldentertics = entertic; + EnterTic = I_GetTime(); - // get available tics - NetUpdate (); + const int startCommand = ClientTic; + int totalTics = EnterTic - LastEnterTic; + if (totalTics > 1 && singletics) + totalTics = 1; + // Listen for other clients and send out data as needed. This is also + // needed for singleplayer! But is instead handled entirely through local + // buffers. This has a limit of 17 tics that can be generated. + NetUpdate(totalTics); + + LastEnterTic = EnterTic; + + // If the game is paused, everything we need to update has already done so. if (pauseext) return; - lowtic = INT_MAX; - numplaying = 0; - for (i = 0; i < doomcom.numnodes; i++) + // Get the amount of tics the client can actually run. This accounts for waiting for other + // players over the network. + int lowestSequence = INT_MAX; + for (auto client : NetworkClients) { - if (nodeingame[i]) - { - numplaying++; - if (nettics[i] < lowtic) - lowtic = nettics[i]; - } + if (ClientStates[client].CurrentSequence < lowestSequence) + lowestSequence = ClientStates[client].CurrentSequence; } - if (ticdup <= 1) - { - availabletics = lowtic - gametic; - } - else - { - availabletics = lowtic - gametic / ticdup; - } + // If the lowest confirmed tic matches the server gametic or greater, allow the client + // to run some of them. + const int availableTics = (lowestSequence - gametic / doomcom.ticdup) + 1; - // decide how many tics to run - if (realtics < availabletics-1) - counts = realtics+1; - else if (realtics < availabletics) - counts = realtics; - else - counts = availabletics; + // If the amount of tics to run is falling behind the amount of available tics, + // speed the playsim up a bit to help catch up. + int runTics = min(totalTics, availableTics); + if (totalTics > 0 && totalTics < availableTics - 1 && !singletics) + ++runTics; - // Uncapped framerate needs seprate checks - if (counts == 0 && !doWait) + // If there are no tics to run, check for possible stall conditions and new + // commands to predict. + if (runTics <= 0) { - TicStabilityWait(); - - // Check possible stall conditions - Net_CheckLastReceived(counts); - if (realtics >= 1) + // 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(); - // Repredict the player for new buffered movement + } + + // If we're in between a tic, try and balance things out. + if (totalTics <= 0) + TicStabilityWait(); + else + P_ClearLevelInterpolation(); + + // If we actually advanced a command, update the player's position (even if a + // tic passes this isn't guaranteed to happen since it's capped to 35 in advance). + if (ClientTic > startCommand) + { P_UnPredictPlayer(); P_PredictPlayer(&players[consoleplayer]); + S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position. } + return; } - if (counts < 1) - counts = 1; + for (auto client : NetworkClients) + players[client].waiting = false; - if (debugfile) - fprintf (debugfile, - "=======real: %i avail: %i game: %i\n", - realtics, availabletics, counts); + // Update the last time the game tic'd. + LastGameUpdate = EnterTic; - // wait for new tics if needed - while (lowtic < gametic + counts) + // Run the available tics. + P_UnPredictPlayer(); + while (runTics--) { - NetUpdate (); - lowtic = INT_MAX; - - for (i = 0; i < doomcom.numnodes; i++) - if (nodeingame[i] && nettics[i] < lowtic) - lowtic = nettics[i]; - - lowtic = lowtic * ticdup; - - if (lowtic < gametic) - I_Error ("TryRunTics: lowtic < gametic"); - - // Check possible stall conditions - Net_CheckLastReceived (counts); - - // Update time returned by I_GetTime, but only if we are stuck in this loop - if (lowtic < gametic + counts) - I_SetFrameTime(); - - // don't stay in here forever -- give the menu a chance to work - if (I_GetTime () - entertic >= 1) - { - C_Ticker (); - M_Ticker (); - // Repredict the player for new buffered movement - P_UnPredictPlayer(); - P_PredictPlayer(&players[consoleplayer]); - return; - } - } - - //Tic lowtic is high enough to process this gametic. Clear all possible waiting info - hadlate = false; - for (i = 0; i < MAXPLAYERS; i++) - players[i].waiting = false; - lastglobalrecvtime = I_GetTime (); //Update the last time the game tic'd over - - // run the count tics - if (counts > 0) - { - P_UnPredictPlayer(); - while (counts--) - { + const bool stabilize = ShouldStabilizeTick(); + if (stabilize) TicStabilityBegin(); - if (gametic > lowtic) - { - I_Error ("gametic>lowtic"); - } - if (advancedemo) - { - D_DoAdvanceDemo (); - } - if (debugfile) fprintf (debugfile, "run tic %d\n", gametic); - C_Ticker (); - M_Ticker (); - G_Ticker(); - gametic++; - NetUpdate (); // check for new console commands + if (advancedemo) + D_DoAdvanceDemo(); + + C_Ticker(); + M_Ticker(); + G_Ticker(); + MakeConsistencies(); + ++gametic; + + if (stabilize) TicStabilityEnd(); - } - P_PredictPlayer(&players[consoleplayer]); - S_UpdateSounds (players[consoleplayer].camera); // move positional sounds - } - else - { - TicStabilityWait(); - } -} -void Net_CheckLastReceived (int counts) -{ - // [Ed850] Check to see the last time a packet was received. - // If it's longer then 3 seconds, a node has likely stalled. - if (I_GetTime() - lastglobalrecvtime >= TICRATE * 3) - { - lastglobalrecvtime = I_GetTime(); //Bump the count - - if (NetMode == NET_PeerToPeer || consoleplayer == Net_Arbitrator) + if (bCommandsReset) { - //Keep the local node in the for loop so we can still log any cases where the local node is /somehow/ late. - //However, we don't send a resend request for sanity reasons. - for (int i = 0; i < doomcom.numnodes; i++) - { - if (nodeingame[i] && nettics[i] <= gametic + counts) - { - if (debugfile && !players[playerfornode[i]].waiting) - fprintf(debugfile, "%i is slow (%i to %i)\n", - i, nettics[i], gametic + counts); - //Send resend request to the late node. Also mark the node as waiting to display it in the hud. - if (i != 0) - remoteresend[i] = players[playerfornode[i]].waiting = hadlate = true; - } - else - players[playerfornode[i]].waiting = false; - } - } - else - { //Send a resend request to the Arbitrator, as it's obvious we are stuck here. - if (debugfile && !players[Net_Arbitrator].waiting) - fprintf(debugfile, "Arbitrator is slow (%i to %i)\n", - nettics[nodeforplayer[Net_Arbitrator]], gametic + counts); - //Send resend request to the Arbitrator. Also mark the Arbitrator as waiting to display it in the hud. - remoteresend[nodeforplayer[Net_Arbitrator]] = players[Net_Arbitrator].waiting = hadlate = true; + bCommandsReset = false; + break; } } + P_PredictPlayer(&players[consoleplayer]); + S_UpdateSounds(players[consoleplayer].camera); // Update sounds only after predicting the client's newest position. } -void Net_NewMakeTic (void) +void Net_NewClientTic() { - specials.NewMakeTic (); + NetEvents.NewClientTic(); } -void Net_WriteInt8 (uint8_t it) +void Net_Initialize() { - specials << it; + NetEvents.InitializeEventData(); } -void Net_WriteInt16 (int16_t it) +void Net_WriteInt8(uint8_t it) { - specials << it; + NetEvents << it; } -void Net_WriteInt32 (int32_t it) +void Net_WriteInt16(int16_t it) { - specials << it; + NetEvents << it; +} + +void Net_WriteInt32(int32_t it) +{ + NetEvents << it; } void Net_WriteInt64(int64_t it) { - specials << it; + NetEvents << it; } -void Net_WriteFloat (float it) +void Net_WriteFloat(float it) { - specials << it; + NetEvents << it; } void Net_WriteDouble(double it) { - specials << it; + NetEvents << it; } -void Net_WriteString (const char *it) +void Net_WriteString(const char *it) { - specials << it; + NetEvents << it; } -void Net_WriteBytes (const uint8_t *block, int len) +void Net_WriteBytes(const uint8_t *block, int len) { while (len--) - specials << *block++; + NetEvents << *block++; } //========================================================================== @@ -2127,33 +2222,34 @@ void Net_WriteBytes (const uint8_t *block, int len) // //========================================================================== -FDynamicBuffer::FDynamicBuffer () +FDynamicBuffer::FDynamicBuffer() { - m_Data = NULL; + m_Data = nullptr; m_Len = m_BufferLen = 0; } -FDynamicBuffer::~FDynamicBuffer () +FDynamicBuffer::~FDynamicBuffer() { - if (m_Data) + if (m_Data != nullptr) { - M_Free (m_Data); - m_Data = NULL; + M_Free(m_Data); + m_Data = nullptr; } m_Len = m_BufferLen = 0; } -void FDynamicBuffer::SetData (const uint8_t *data, int len) +void FDynamicBuffer::SetData(const uint8_t *data, int len) { if (len > m_BufferLen) { m_BufferLen = (len + 255) & ~255; - m_Data = (uint8_t *)M_Realloc (m_Data, m_BufferLen); + m_Data = (uint8_t *)M_Realloc(m_Data, m_BufferLen); } - if (data != NULL) + + if (data != nullptr) { m_Len = len; - memcpy (m_Data, data, len); + memcpy(m_Data, data, len); } else { @@ -2161,14 +2257,13 @@ void FDynamicBuffer::SetData (const uint8_t *data, int len) } } -uint8_t *FDynamicBuffer::GetData (int *len) +uint8_t *FDynamicBuffer::GetData(int *len) { - if (len) + if (len != nullptr) *len = m_Len; - return m_Len ? m_Data : NULL; + return m_Len ? m_Data : nullptr; } - static int RemoveClass(FLevelLocals *Level, const PClass *cls) { AActor *actor; @@ -2180,66 +2275,81 @@ static int RemoveClass(FLevelLocals *Level, const PClass *cls) if (actor->IsA(cls)) { // [MC]Do not remove LIVE players. - if (actor->player != NULL) + if (actor->player != nullptr) { player = true; continue; } // [SP] Don't remove owned inventory objects. if (!actor->IsMapActor()) - { continue; - } + removecount++; actor->ClearCounters(); actor->Destroy(); } } + if (player) Printf("Cannot remove live players!\n"); + return removecount; } // [RH] Execute a special "ticcmd". The type byte should // have already been read, and the stream is positioned // at the beginning of the command's actual data. -void Net_DoCommand (int type, uint8_t **stream, int player) +void Net_DoCommand(int cmd, uint8_t **stream, int player) { uint8_t pos = 0; const char* s = nullptr; - int i; + int i = 0; - switch (type) + switch (cmd) { case DEM_SAY: { const char *name = players[player].userinfo.GetName(); - uint8_t who = ReadInt8 (stream); + uint8_t who = ReadInt8(stream); s = ReadStringConst(stream); - if (((who & 1) == 0) || players[player].userinfo.GetTeam() == TEAM_NONE) - { // Said to everyone - if (who & 2) - { - Printf (PRINT_CHAT, TEXTCOLOR_BOLD "* %s" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, s); - } + // If chat is disabled, there's nothing else to do here since the stream has been advanced. + if (cl_showchat == CHAT_DISABLED || (MutedClients & (1 << player))) + break; + + constexpr int MSG_TEAM = 1; + constexpr int MSG_BOLD = 2; + if (!(who & MSG_TEAM)) + { + if (cl_showchat < CHAT_GLOBAL) + break; + + // Said to everyone + if (deathmatch && teamplay) + Printf(PRINT_CHAT, "(All) "); + if ((who & MSG_BOLD) && !cl_noboldchat) + Printf(PRINT_CHAT, TEXTCOLOR_BOLD "* %s" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, s); else - { - Printf (PRINT_CHAT, "%s" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, s); - } - S_Sound (CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1, ATTN_NONE); + Printf(PRINT_CHAT, "%s" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, s); + + if (!cl_nochatsound) + S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE); } - else if (players[player].userinfo.GetTeam() == players[consoleplayer].userinfo.GetTeam()) - { // Said only to members of the player's team - if (who & 2) - { - Printf (PRINT_TEAMCHAT, TEXTCOLOR_BOLD "* (%s" TEXTCOLOR_BOLD ")%s" TEXTCOLOR_BOLD "\n", name, s); - } + else if (!deathmatch || players[player].userinfo.GetTeam() == players[consoleplayer].userinfo.GetTeam()) + { + if (cl_showchat < CHAT_TEAM_ONLY) + break; + + // Said only to members of the player's team + if (deathmatch && teamplay) + Printf(PRINT_TEAMCHAT, "(Team) "); + if ((who & MSG_BOLD) && !cl_noboldchat) + Printf(PRINT_TEAMCHAT, TEXTCOLOR_BOLD "* %s" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, s); else - { - Printf (PRINT_TEAMCHAT, "(%s" TEXTCOLOR_TEAMCHAT "): %s" TEXTCOLOR_TEAMCHAT "\n", name, s); - } - S_Sound (CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1, ATTN_NONE); + Printf(PRINT_TEAMCHAT, "%s" TEXTCOLOR_TEAMCHAT ": %s" TEXTCOLOR_TEAMCHAT "\n", name, s); + + if (!cl_nochatsound) + S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE); } } break; @@ -2257,32 +2367,31 @@ void Net_DoCommand (int type, uint8_t **stream, int player) break; case DEM_UINFCHANGED: - D_ReadUserInfoStrings (player, stream, true); + D_ReadUserInfoStrings(player, stream, true); break; case DEM_SINFCHANGED: - D_DoServerInfoChange (stream, false); + D_DoServerInfoChange(stream, false); break; case DEM_SINFCHANGEDXOR: - D_DoServerInfoChange (stream, true); + D_DoServerInfoChange(stream, true); break; case DEM_GIVECHEAT: s = ReadStringConst(stream); - cht_Give (&players[player], s, ReadInt32 (stream)); + cht_Give(&players[player], s, ReadInt32(stream)); if (player != consoleplayer) { FString message = GStrings.GetString("TXT_X_CHEATS"); message.Substitute("%s", players[player].userinfo.GetName()); Printf("%s: give %s\n", message.GetChars(), s); } - break; case DEM_TAKECHEAT: s = ReadStringConst(stream); - cht_Take (&players[player], s, ReadInt32 (stream)); + cht_Take(&players[player], s, ReadInt32(stream)); break; case DEM_SETINV: @@ -2293,21 +2402,20 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_WARPCHEAT: { - int x, y, z; - x = ReadInt16 (stream); - y = ReadInt16 (stream); - z = ReadInt16 (stream); - P_TeleportMove (players[player].mo, DVector3(x, y, z), true); + int x = ReadInt16(stream); + int y = ReadInt16(stream); + int z = ReadInt16(stream); + P_TeleportMove(players[player].mo, DVector3(x, y, z), true); } break; case DEM_GENERICCHEAT: - cht_DoCheat (&players[player], ReadInt8 (stream)); + cht_DoCheat(&players[player], ReadInt8(stream)); break; case DEM_CHANGEMAP2: - pos = ReadInt8 (stream); - /* intentional fall-through */ + pos = ReadInt8(stream); + [[fallthrough]]; case DEM_CHANGEMAP: // Change to another map without disconnecting other players s = ReadStringConst(stream); @@ -2318,15 +2426,15 @@ void Net_DoCommand (int type, uint8_t **stream, int player) break; case DEM_SUICIDE: - cht_Suicide (&players[player]); + cht_Suicide(&players[player]); break; case DEM_ADDBOT: - primaryLevel->BotInfo.TryAddBot (primaryLevel, stream, player); + primaryLevel->BotInfo.TryAddBot(primaryLevel, stream, player); break; case DEM_KILLBOTS: - primaryLevel->BotInfo.RemoveAllBots (primaryLevel, true); + primaryLevel->BotInfo.RemoveAllBots(primaryLevel, true); Printf ("Removed all bots\n"); break; @@ -2335,7 +2443,8 @@ void Net_DoCommand (int type, uint8_t **stream, int player) break; case DEM_INVUSEALL: - if (gamestate == GS_LEVEL && !paused) + if (gamestate == GS_LEVEL && !paused + && players[player].playerstate != PST_DEAD) { AActor *item = players[player].mo->Inventory; auto pitype = PClass::FindActor(NAME_PuzzleItem); @@ -2355,29 +2464,24 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_INVUSE: case DEM_INVDROP: { - uint32_t which = ReadInt32 (stream); + uint32_t which = ReadInt32(stream); int amt = -1; - - if (type == DEM_INVDROP) amt = ReadInt32(stream); + if (cmd == DEM_INVDROP) + amt = ReadInt32(stream); if (gamestate == GS_LEVEL && !paused && players[player].playerstate != PST_DEAD) { auto item = players[player].mo->Inventory; - while (item != NULL && item->InventoryID != which) - { + while (item != nullptr && item->InventoryID != which) item = item->Inventory; - } - if (item != NULL) + + if (item != nullptr) { - if (type == DEM_INVUSE) - { - players[player].mo->UseInventory (item); - } + if (cmd == DEM_INVUSE) + players[player].mo->UseInventory(item); else - { - players[player].mo->DropInventory (item, amt); - } + players[player].mo->DropInventory(item, amt); } } } @@ -2397,36 +2501,36 @@ void Net_DoCommand (int type, uint8_t **stream, int player) int args[5]; s = ReadStringConst(stream); - if (type >= DEM_SUMMON2 && type <= DEM_SUMMONFOE2) + if (cmd >= DEM_SUMMON2 && cmd <= DEM_SUMMONFOE2) { angle = ReadInt16(stream); tid = ReadInt16(stream); special = ReadInt8(stream); - for(i = 0; i < 5; i++) args[i] = ReadInt32(stream); + for (i = 0; i < 5; i++) args[i] = ReadInt32(stream); } - AActor *source = players[player].mo; - if(source != NULL) + AActor* source = players[player].mo; + if (source != NULL) { - PClassActor * typeinfo = PClass::FindActor(s); + PClassActor* typeinfo = PClass::FindActor(s); if (typeinfo != NULL) { - if (GetDefaultByType (typeinfo)->flags & MF_MISSILE) + if (GetDefaultByType(typeinfo)->flags & MF_MISSILE) { - P_SpawnPlayerMissile (source, 0, 0, 0, typeinfo, source->Angles.Yaw); + P_SpawnPlayerMissile(source, 0, 0, 0, typeinfo, source->Angles.Yaw); } else { - const AActor *def = GetDefaultByType (typeinfo); + const AActor* def = GetDefaultByType(typeinfo); DVector3 spawnpos = source->Vec3Angle(def->radius * 2 + source->radius, source->Angles.Yaw, 8.); - AActor *spawned = Spawn (primaryLevel, typeinfo, spawnpos, ALLOW_REPLACE); + AActor* spawned = Spawn(primaryLevel, typeinfo, spawnpos, ALLOW_REPLACE); if (spawned != NULL) { spawned->SpawnFlags |= MTF_CONSOLETHING; - if (type == DEM_SUMMONFRIEND || type == DEM_SUMMONFRIEND2 || type == DEM_SUMMONMBF) + if (cmd == DEM_SUMMONFRIEND || cmd == DEM_SUMMONFRIEND2 || cmd == DEM_SUMMONMBF) { - if (spawned->CountsAsKill()) + if (spawned->CountsAsKill()) { primaryLevel->total_monsters--; } @@ -2434,36 +2538,36 @@ void Net_DoCommand (int type, uint8_t **stream, int player) spawned->flags |= MF_FRIENDLY; spawned->LastHeard = players[player].mo; spawned->health = spawned->SpawnHealth(); - if (type == DEM_SUMMONMBF) + if (cmd == DEM_SUMMONMBF) spawned->flags3 |= MF3_NOBLOCKMONST; } - else if (type == DEM_SUMMONFOE || type == DEM_SUMMONFOE2) + else if (cmd == DEM_SUMMONFOE || cmd == DEM_SUMMONFOE2) { spawned->FriendPlayer = 0; spawned->flags &= ~MF_FRIENDLY; spawned->health = spawned->SpawnHealth(); } - if (type >= DEM_SUMMON2 && type <= DEM_SUMMONFOE2) + if (cmd >= DEM_SUMMON2 && cmd <= DEM_SUMMONFOE2) { spawned->Angles.Yaw = source->Angles.Yaw - DAngle::fromDeg(angle); spawned->special = special; - for(i = 0; i < 5; i++) { + for (i = 0; i < 5; i++) { spawned->args[i] = args[i]; } - if(tid) spawned->SetTID(tid); + if (tid) spawned->SetTID(tid); } } } } else { // not an actor, must be a visualthinker - PClass * typeinfo = PClass::FindClass(s); - if(typeinfo && typeinfo->IsDescendantOf("VisualThinker")) + PClass* typeinfo = PClass::FindClass(s); + if (typeinfo && typeinfo->IsDescendantOf("VisualThinker")) { DVector3 spawnpos = source->Vec3Angle(source->radius * 4, source->Angles.Yaw, 8.); auto vt = DVisualThinker::NewVisualThinker(source->Level, typeinfo); - if(vt) + if (vt) { vt->PT.Pos = spawnpos; vt->UpdateSector(); @@ -2490,12 +2594,12 @@ void Net_DoCommand (int type, uint8_t **stream, int player) if (paused) { paused = 0; - S_ResumeSound (false); + S_ResumeSound(false); } else { paused = player + 1; - S_PauseSound (false, false); + S_PauseSound(false, false); } } break; @@ -2510,7 +2614,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) // Paths sent over the network will be valid for the system that sent // the save command. For other systems, the path needs to be changed. FString basename = ExtractFileBase(savegamefile.GetChars(), true); - savegamefile = G_BuildSaveName (basename.GetChars()); + savegamefile = G_BuildSaveName(basename.GetChars()); } } gameaction = ga_savegame; @@ -2520,15 +2624,11 @@ void Net_DoCommand (int type, uint8_t **stream, int player) // Do not autosave in multiplayer games or when dead. // For demo playback, DEM_DOAUTOSAVE already exists in the demo if the // autosave happened. And if it doesn't, we must not generate it. - if (multiplayer || - demoplayback || - players[consoleplayer].playerstate != PST_LIVE || - disableautosave >= 2 || - autosavecount == 0) + if (!netgame && !demoplayback && disableautosave < 2 && autosavecount + && players[player].playerstate == PST_LIVE) { - break; + Net_WriteInt8(DEM_DOAUTOSAVE); } - Net_WriteInt8 (DEM_DOAUTOSAVE); break; case DEM_DOAUTOSAVE: @@ -2537,36 +2637,29 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_FOV: { - float newfov = ReadFloat (stream); - - if (newfov != players[consoleplayer].DesiredFOV) + float newfov = ReadFloat(stream); + if (newfov != players[player].DesiredFOV) { - Printf ("FOV%s set to %g\n", - consoleplayer == Net_Arbitrator ? " for everyone" : "", + Printf("FOV%s set to %g\n", + player == Net_Arbitrator ? " for everyone" : "", newfov); } - for (i = 0; i < MAXPLAYERS; ++i) - { - if (playeringame[i]) - { - players[i].DesiredFOV = newfov; - } - } + for (auto client : NetworkClients) + players[client].DesiredFOV = newfov; } break; case DEM_MYFOV: - players[player].DesiredFOV = ReadFloat (stream); + players[player].DesiredFOV = ReadFloat(stream); break; case DEM_RUNSCRIPT: case DEM_RUNSCRIPT2: { - int snum = ReadInt16 (stream); - int argn = ReadInt8 (stream); - - RunScript(stream, players[player].mo, snum, argn, (type == DEM_RUNSCRIPT2) ? ACS_ALWAYS : 0); + int snum = ReadInt16(stream); + int argn = ReadInt8(stream); + RunScript(stream, players[player].mo, snum, argn, (cmd == DEM_RUNSCRIPT2) ? ACS_ALWAYS : 0); } break; @@ -2574,7 +2667,6 @@ void Net_DoCommand (int type, uint8_t **stream, int player) { s = ReadStringConst(stream); int argn = ReadInt8(stream); - RunScript(stream, players[player].mo, -FName(s).GetIndex(), argn & 127, (argn & 128) ? ACS_ALWAYS : 0); } break; @@ -2583,27 +2675,24 @@ void Net_DoCommand (int type, uint8_t **stream, int player) { int snum = ReadInt16(stream); int argn = ReadInt8(stream); - int arg[5] = { 0, 0, 0, 0, 0 }; + int arg[5] = {}; for (i = 0; i < argn; ++i) { int argval = ReadInt32(stream); if ((unsigned)i < countof(arg)) - { arg[i] = argval; - } } + if (!CheckCheatmode(player == consoleplayer)) - { - P_ExecuteSpecial(primaryLevel, snum, NULL, players[player].mo, false, arg[0], arg[1], arg[2], arg[3], arg[4]); - } + P_ExecuteSpecial(primaryLevel, snum, nullptr, players[player].mo, false, arg[0], arg[1], arg[2], arg[3], arg[4]); } break; case DEM_CROUCH: - if (gamestate == GS_LEVEL && players[player].mo != NULL && - players[player].health > 0 && !(players[player].oldbuttons & BT_JUMP) && - !P_IsPlayerTotallyFrozen(&players[player])) + if (gamestate == GS_LEVEL && players[player].mo != nullptr + && players[player].playerstate == PST_LIVE && !(players[player].oldbuttons & BT_JUMP) + && !P_IsPlayerTotallyFrozen(&players[player])) { players[player].crouching = players[player].crouchdir < 0 ? 1 : -1; } @@ -2612,31 +2701,27 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_MORPHEX: { s = ReadStringConst(stream); - FString msg = cht_Morph (players + player, PClass::FindActor(s), false); + FString msg = cht_Morph(players + player, PClass::FindActor(s), false); if (player == consoleplayer) - { - Printf ("%s\n", msg[0] != '\0' ? msg.GetChars() : "Morph failed."); - } + Printf("%s\n", msg[0] != '\0' ? msg.GetChars() : "Morph failed."); } break; case DEM_ADDCONTROLLER: { - uint8_t playernum = ReadInt8 (stream); + uint8_t playernum = ReadInt8(stream); players[playernum].settings_controller = true; - if (consoleplayer == playernum || consoleplayer == Net_Arbitrator) - Printf ("%s has been added to the controller list.\n", players[playernum].userinfo.GetName()); + Printf("%s has been added to the controller list.\n", players[playernum].userinfo.GetName()); } break; case DEM_DELCONTROLLER: { - uint8_t playernum = ReadInt8 (stream); + uint8_t playernum = ReadInt8(stream); players[playernum].settings_controller = false; - if (consoleplayer == playernum || consoleplayer == Net_Arbitrator) - Printf ("%s has been removed from the controller list.\n", players[playernum].userinfo.GetName()); + Printf("%s has been removed from the controller list.\n", players[playernum].userinfo.GetName()); } break; @@ -2646,70 +2731,62 @@ void Net_DoCommand (int type, uint8_t **stream, int player) int killcount = 0; PClassActor *cls = PClass::FindActor(s); - if (cls != NULL) + if (cls != nullptr) { killcount = primaryLevel->Massacre(false, cls->TypeName); PClassActor *cls_rep = cls->GetReplacement(primaryLevel); if (cls != cls_rep) - { killcount += primaryLevel->Massacre(false, cls_rep->TypeName); - } - Printf ("Killed %d monsters of type %s.\n",killcount, s); + + Printf("Killed %d monsters of type %s.\n", killcount, s); } else { - Printf ("%s is not an actor class.\n", s); + Printf("%s is not an actor class.\n", s); } - } break; + case DEM_REMOVE: - { - s = ReadStringConst(stream); - int removecount = 0; - PClassActor *cls = PClass::FindActor(s); - if (cls != NULL && cls->IsDescendantOf(RUNTIME_CLASS(AActor))) { - removecount = RemoveClass(primaryLevel, cls); - const PClass *cls_rep = cls->GetReplacement(primaryLevel); - if (cls != cls_rep) + s = ReadStringConst(stream); + int removecount = 0; + PClassActor *cls = PClass::FindActor(s); + if (cls != nullptr && cls->IsDescendantOf(RUNTIME_CLASS(AActor))) { - removecount += RemoveClass(primaryLevel, cls_rep); + removecount = RemoveClass(primaryLevel, cls); + const PClass *cls_rep = cls->GetReplacement(primaryLevel); + if (cls != cls_rep) + removecount += RemoveClass(primaryLevel, cls_rep); + + Printf("Removed %d actors of type %s.\n", removecount, s); + } + else + { + Printf("%s is not an actor class.\n", s); } - Printf("Removed %d actors of type %s.\n", removecount, s); } - else - { - Printf("%s is not an actor class.\n", s); - } - } break; case DEM_CONVREPLY: case DEM_CONVCLOSE: case DEM_CONVNULL: - P_ConversationCommand (type, player, stream); + P_ConversationCommand(cmd, player, stream); break; case DEM_SETSLOT: case DEM_SETSLOTPNUM: { - int pnum; - if (type == DEM_SETSLOTPNUM) - { + int pnum = player; + if (cmd == DEM_SETSLOTPNUM) pnum = ReadInt8(stream); - } - else - { - pnum = player; - } + unsigned int slot = ReadInt8(stream); int count = ReadInt8(stream); if (slot < NUM_WEAPON_SLOTS) - { players[pnum].weapons.ClearSlot(slot); - } - for(i = 0; i < count; ++i) + + for (i = 0; i < count; ++i) { PClassActor *wpn = Net_ReadWeapon(stream); players[pnum].weapons.AddSlot(slot, wpn, pnum == consoleplayer); @@ -2744,7 +2821,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) case DEM_FINISHGAME: // Simulate an end-of-game action - primaryLevel->ChangeLevel(NULL, 0, 0); + primaryLevel->ChangeLevel(nullptr, 0, 0); break; case DEM_NETEVENT: @@ -2768,7 +2845,7 @@ void Net_DoCommand (int type, uint8_t **stream, int player) FName cmd = ReadStringConst(stream); unsigned int size = ReadInt16(stream); - TArray buffer = {}; + TArray buffer; if (size) { buffer.Grow(size); @@ -2779,14 +2856,30 @@ void Net_DoCommand (int type, uint8_t **stream, int player) FNetworkCommand netCmd = { player, cmd, buffer }; primaryLevel->localEventManager->NetCommand(netCmd); } - break; + break; case DEM_CHANGESKILL: NextSkill = ReadInt32(stream); break; + + case DEM_KICK: + { + const int pNum = ReadInt8(stream); + if (pNum == consoleplayer) + { + I_Error("You have been kicked from the game"); + } + else + { + Printf("%s has been kicked from the game\n", players[pNum].userinfo.GetName()); + if (NetworkClients.InGame(pNum)) + DisconnectClient(pNum); + } + } + break; default: - I_Error ("Unknown net command: %d", type); + I_Error("Unknown net command: %d", cmd); break; } } @@ -2794,44 +2887,41 @@ void Net_DoCommand (int type, uint8_t **stream, int player) // Used by DEM_RUNSCRIPT, DEM_RUNSCRIPT2, and DEM_RUNNAMEDSCRIPT static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always) { + // Scripts can be invoked without a level loaded, e.g. via puke(name) CCMD in fullscreen console if (pawn == nullptr) - { - // Scripts can be invoked without a level loaded, e.g. via puke(name) CCMD in fullscreen console return; - } - int arg[4] = { 0, 0, 0, 0 }; - int i; - - for (i = 0; i < argn; ++i) + int arg[4] = {}; + for (int i = 0; i < argn; ++i) { int argval = ReadInt32(stream); if ((unsigned)i < countof(arg)) - { arg[i] = argval; - } } - P_StartScript(pawn->Level, pawn, NULL, snum, primaryLevel->MapName.GetChars(), arg, min(countof(arg), argn), ACS_NET | always); + + P_StartScript(pawn->Level, pawn, nullptr, snum, primaryLevel->MapName.GetChars(), arg, min(countof(arg), argn), ACS_NET | always); } -void Net_SkipCommand (int type, uint8_t **stream) +// TODO: This really needs to be replaced with some kind of packet system that can simply read through packets and opt +// not to execute them. Right now this is making setting up net commands a nightmare. +// Reads through the network stream but doesn't actually execute any command. Used for getting the size of a stream. +// The skip amount is the number of bytes the command possesses. This should mirror the bytes in Net_DoCommand(). +void Net_SkipCommand(int cmd, uint8_t **stream) { - uint8_t t; - size_t skip; - - switch (type) + size_t skip = 0; + switch (cmd) { case DEM_SAY: - skip = strlen ((char *)(*stream + 1)) + 2; + skip = strlen((char *)(*stream + 1)) + 2; break; case DEM_ADDBOT: - skip = strlen ((char *)(*stream + 1)) + 6; + skip = strlen((char *)(*stream + 1)) + 6; break; case DEM_GIVECHEAT: case DEM_TAKECHEAT: - skip = strlen ((char *)(*stream)) + 5; + skip = strlen((char *)(*stream)) + 5; break; case DEM_SETINV: @@ -2850,7 +2940,7 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: - skip = strlen ((char *)(*stream)) + 26; + skip = strlen((char *)(*stream)) + 26; break; case DEM_CHANGEMAP2: skip = strlen((char *)(*stream + 1)) + 2; @@ -2869,7 +2959,7 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_MORPHEX: case DEM_KILLCLASSCHEAT: case DEM_MDK: - skip = strlen ((char *)(*stream)) + 1; + skip = strlen((char *)(*stream)) + 1; break; case DEM_WARPCHEAT: @@ -2891,36 +2981,40 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_DROPPLAYER: case DEM_ADDCONTROLLER: case DEM_DELCONTROLLER: + case DEM_KICK: skip = 1; break; case DEM_SAVEGAME: - skip = strlen ((char *)(*stream)) + 1; - skip += strlen ((char *)(*stream) + skip) + 1; + skip = strlen((char *)(*stream)) + 1; + skip += strlen((char *)(*stream) + skip) + 1; break; case DEM_SINFCHANGEDXOR: case DEM_SINFCHANGED: - t = **stream; - skip = 1 + (t & 63); - if (type == DEM_SINFCHANGED) { - switch (t >> 6) + uint8_t t = **stream; + skip = 1 + (t & 63); + if (cmd == DEM_SINFCHANGED) { - case CVAR_Bool: + switch (t >> 6) + { + case CVAR_Bool: + skip += 1; + break; + case CVAR_Int: + case CVAR_Float: + skip += 4; + break; + case CVAR_String: + skip += strlen((char*)(*stream + skip)) + 1; + break; + } + } + else + { skip += 1; - break; - case CVAR_Int: case CVAR_Float: - skip += 4; - break; - case CVAR_String: - skip += strlen ((char *)(*stream + skip)) + 1; - break; } - } - else - { - skip += 1; } break; @@ -2945,11 +3039,9 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_SETSLOT: case DEM_SETSLOTPNUM: { - skip = 2 + (type == DEM_SETSLOTPNUM); - for(int numweapons = (*stream)[skip-1]; numweapons > 0; numweapons--) - { + skip = 2 + (cmd == DEM_SETSLOTPNUM); + for (int numweapons = (*stream)[skip-1]; numweapons > 0; --numweapons) skip += 1 + ((*stream)[skip] >> 7); - } } break; @@ -2961,39 +3053,25 @@ void Net_SkipCommand (int type, uint8_t **stream) case DEM_SETPITCHLIMIT: skip = 2; break; - - default: - return; } *stream += skip; } // This was taken out of shared_hud, because UI code shouldn't do low level calculations that may change if the backing implementation changes. -int Net_GetLatency(int *ld, int *ad) +int Net_GetLatency(int* localDelay, int* arbitratorDelay) { - int i, localdelay = 0, arbitratordelay = 0; - - for (i = 0; i < BACKUPTICS; i++) localdelay += netdelay[0][i]; - for (i = 0; i < BACKUPTICS; i++) arbitratordelay += netdelay[nodeforplayer[Net_Arbitrator]][i]; - arbitratordelay = ((arbitratordelay / BACKUPTICS) * ticdup) * (1000 / TICRATE); - localdelay = ((localdelay / BACKUPTICS) * ticdup) * (1000 / TICRATE); + const int gameDelayMs = (ClientTic - gametic) * 1000 / TICRATE; int severity = 0; - - if (max(localdelay, arbitratordelay) > 200) - { - severity = 1; - } - if (max(localdelay, arbitratordelay) > 400) - { - severity = 2; - } - if (max(localdelay, arbitratordelay) >= ((BACKUPTICS / 2 - 1) * ticdup) * (1000 / TICRATE)) - { + if (gameDelayMs >= 160) severity = 3; - } - *ld = localdelay; - *ad = arbitratordelay; + else if (gameDelayMs >= 120) + severity = 2; + else if (gameDelayMs >= 80) + severity = 1; + + *localDelay = gameDelayMs; + *arbitratorDelay = NetMode == NET_PacketServer ? ClientStates[consoleplayer].AverageLatency : ClientStates[Net_Arbitrator].AverageLatency; return severity; } @@ -3004,13 +3082,198 @@ int Net_GetLatency(int *ld, int *ad) //========================================================================== // [RH] List "ping" times -CCMD (pings) +CCMD(pings) { - int i; - for (i = 0; i < MAXPLAYERS; i++) - if (playeringame[i]) - Printf ("% 4" PRId64 " %s\n", currrecvtime[i] - lastrecvtime[i], - players[i].userinfo.GetName()); + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + if (NetworkClients.Size() <= 1) + return; + + // In Packet Server mode, this displays the latency each individual client has to the host + for (auto client : NetworkClients) + { + if ((NetMode == NET_PeerToPeer && client != consoleplayer) || (NetMode == NET_PacketServer && client != Net_Arbitrator)) + Printf("%ums %s\n", ClientStates[client].AverageLatency, players[client].userinfo.GetName()); + } +} + +CCMD(listplayers) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + for (auto client : NetworkClients) + { + if (client == consoleplayer) + Printf("* "); + Printf("%s - %d\n", players[client].userinfo.GetName(), client); + } +} + +CCMD(kick) +{ + if (argv.argc() == 1) + { + Printf("Usage: kick \n"); + return; + } + + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + // Dont give settings controllers access to this. That should be reserved as a separate power + // the host can grant. + if (consoleplayer != Net_Arbitrator) + { + Printf("Only the host is allowed to kick other players\n"); + return; + } + + int pNum = -1; + if (!C_IsValidInt(argv[1], pNum)) + { + Printf("A player number must be provided. Use listplayers for more information\n"); + return; + } + + if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS) + { + Printf("Invalid player number provided\n"); + return; + } + + if (!NetworkClients.InGame(pNum)) + { + Printf("Player is not currently in the game\n"); + return; + } + + Net_WriteInt8(DEM_KICK); + Net_WriteInt8(pNum); +} + +CCMD(mute) +{ + if (argv.argc() == 1) + { + Printf("Usage: mute - Don't receive messages from this player\n"); + return; + } + + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + int pNum = -1; + if (!C_IsValidInt(argv[1], pNum)) + { + Printf("A player number must be provided. Use listplayers for more information\n"); + return; + } + + if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS) + { + Printf("Invalid player number provided\n"); + return; + } + + if (!NetworkClients.InGame(pNum)) + { + Printf("Player is not currently in the game\n"); + return; + } + + MutedClients |= 1 << pNum; +} + +CCMD(muteall) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + for (auto client : NetworkClients) + { + if (client != consoleplayer) + MutedClients |= 1 << client; + } +} + +CCMD(listmuted) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + bool found = false; + for (auto client : NetworkClients) + { + if (MutedClients & (1 << client)) + { + found = true; + Printf("%s - %d\n", players[client].userinfo.GetName(), client); + } + } + + if (!found) + Printf("No one currently muted\n"); +} + +CCMD(unmute) +{ + if (argv.argc() == 1) + { + Printf("Usage: unmute - Allow messages from this player again\n"); + return; + } + + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + int pNum = -1; + if (!C_IsValidInt(argv[1], pNum)) + { + Printf("A player number must be provided. Use listplayers for more information\n"); + return; + } + + if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS) + { + Printf("Invalid player number provided\n"); + return; + } + + MutedClients &= ~(1 << pNum); +} + +CCMD(unmuteall) +{ + if (!netgame) + { + Printf("Not currently in a net game\n"); + return; + } + + MutedClients = 0; } //========================================================================== @@ -3022,50 +3285,46 @@ CCMD (pings) // //========================================================================== -static void Network_Controller (int playernum, bool add) +static void Network_Controller(int pNum, bool add) { + if (!netgame) + { + Printf("This command can only be used when playing a net game.\n"); + return; + } + if (consoleplayer != Net_Arbitrator) { - Printf ("This command is only accessible to the net arbitrator.\n"); + Printf("This command is only accessible to the host.\n"); return; } - if (players[playernum].settings_controller && add) + if (pNum == Net_Arbitrator) { - Printf ("%s is already on the setting controller list.\n", players[playernum].userinfo.GetName()); + Printf("The host cannot change their own settings controller status.\n"); return; } - if (!players[playernum].settings_controller && !add) + if (!NetworkClients.InGame(pNum)) { - Printf ("%s is not on the setting controller list.\n", players[playernum].userinfo.GetName()); + Printf("Player %d is not a valid client\n", pNum); return; } - if (!playeringame[playernum]) + if (players[pNum].settings_controller && add) { - Printf ("Player (%d) not found!\n", playernum); + Printf("%s is already on the setting controller list.\n", players[pNum].userinfo.GetName()); return; } - if (players[playernum].Bot != NULL) + if (!players[pNum].settings_controller && !add) { - Printf ("Bots cannot be added to the controller list.\n"); + Printf("%s is not on the setting controller list.\n", players[pNum].userinfo.GetName()); return; } - if (playernum == Net_Arbitrator) - { - Printf ("The net arbitrator cannot have their status changed on this list.\n"); - return; - } - - if (add) - Net_WriteInt8 (DEM_ADDCONTROLLER); - else - Net_WriteInt8 (DEM_DELCONTROLLER); - - Net_WriteInt8 (playernum); + Net_WriteInt8(add ? DEM_ADDCONTROLLER : DEM_DELCONTROLLER); + Net_WriteInt8(pNum); } //========================================================================== @@ -3074,21 +3333,15 @@ static void Network_Controller (int playernum, bool add) // //========================================================================== -CCMD (net_addcontroller) +CCMD(net_addcontroller) { - if (!netgame) + if (argv.argc() < 2) { - Printf ("This command can only be used when playing a net game.\n"); + Printf("Usage: net_addcontroller \n"); return; } - if (argv.argc () < 2) - { - Printf ("Usage: net_addcontroller \n"); - return; - } - - Network_Controller (atoi (argv[1]), true); + Network_Controller(atoi (argv[1]), true); } //========================================================================== @@ -3097,21 +3350,15 @@ CCMD (net_addcontroller) // //========================================================================== -CCMD (net_removecontroller) +CCMD(net_removecontroller) { - if (!netgame) + if (argv.argc() < 2) { - Printf ("This command can only be used when playing a net game.\n"); + Printf("Usage: net_removecontroller \n"); return; } - if (argv.argc () < 2) - { - Printf ("Usage: net_removecontroller \n"); - return; - } - - Network_Controller (atoi (argv[1]), false); + Network_Controller(atoi(argv[1]), false); } //========================================================================== @@ -3120,7 +3367,7 @@ CCMD (net_removecontroller) // //========================================================================== -CCMD (net_listcontrollers) +CCMD(net_listcontrollers) { if (!netgame) { @@ -3128,16 +3375,11 @@ CCMD (net_listcontrollers) return; } - Printf ("The following players can change the game settings:\n"); + Printf("The following players can change the game settings:\n"); - for (int i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (!playeringame[i]) - continue; - - if (players[i].settings_controller) - { - Printf ("- %s\n", players[i].userinfo.GetName()); - } + if (players[client].settings_controller) + Printf("- %s\n", players[client].userinfo.GetName()); } } diff --git a/src/d_net.h b/src/d_net.h index e6a6a32b9..c04ec3c34 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -32,25 +32,109 @@ #include "doomdef.h" #include "d_protocol.h" #include "i_net.h" +#include + +uint64_t I_msTime(); class FDynamicBuffer { public: - FDynamicBuffer (); - ~FDynamicBuffer (); + FDynamicBuffer(); + ~FDynamicBuffer(); - void SetData (const uint8_t *data, int len); - uint8_t *GetData (int *len = NULL); + void SetData(const uint8_t *data, int len); + uint8_t* GetData(int *len = nullptr); private: - uint8_t *m_Data; + uint8_t* m_Data; int m_Len, m_BufferLen; }; -extern FDynamicBuffer NetSpecs[MAXPLAYERS][BACKUPTICS]; +enum EClientFlags +{ + CF_NONE = 0, + CF_QUIT = 1, // If in packet server mode, this client sent an exit command and needs to be disconnected. + CF_MISSING_SEQ = 1 << 1, // If a sequence was missed/out of order, ask this client to send back over their info. + CF_RETRANSMIT_SEQ = 1 << 2, // If set, this client needs command data resent to them. + CF_MISSING_CON = 1 << 3, // If a consistency was missed/out of order, ask this client to send back over their info. + CF_RETRANSMIT_CON = 1 << 4, // If set, this client needs consistency data resent to them. + CF_UPDATED = 1 << 5, // Got an updated packet from this client. + + CF_RETRANSMIT = CF_RETRANSMIT_CON | CF_RETRANSMIT_SEQ, + CF_MISSING = CF_MISSING_CON | CF_MISSING_SEQ, +}; + +struct FClientNetState +{ + // Networked client data. + struct FNetTic { + FDynamicBuffer Data; + usercmd_t Command; + } Tics[BACKUPTICS] = {}; + + // Local information about client. + uint8_t CurrentLatency = 0u; // Current latency id the client is on. If the one the client sends back is > this, update RecvTime and mark a new SentTime. + bool bNewLatency = true; // If the sequence was bumped, the next latency packet sent out should record the send time. + uint16_t AverageLatency = 0u; // Calculate the average latency every second or so, that way it doesn't give huge variance in the scoreboard. + uint64_t SentTime[MAXSENDTICS] = {}; // Timestamp for when we sent out the packet to this client. + uint64_t RecvTime[MAXSENDTICS] = {}; // Timestamp for when the client acknowledged our last packet. If in packet server mode, this is the server's delta. + + int Flags = 0; // State of this client. + + uint8_t ResendID = 0u; // Make sure that if the retransmit happened on a wait barrier, it can be properly resent back over. + int ResendSequenceFrom = -1; // If >= 0, send from this sequence up to the most recent one, capped to MAXSENDTICS. + int SequenceAck = -1; // The last sequence the client reported from us. + int CurrentSequence = -1; // The last sequence we've gotten from this client. + + // Every packet includes consistencies for tics that client ran. When + // a world tic is ran, the local client will store all the consistencies + // of the clients in their LocalConsistency. Then the consistencies will + // be checked against retroactively as they come in. + int ResendConsistencyFrom = -1; // If >= 0, send from this consistency up to the most recent one, capped to MAXSENDTICS. + int ConsistencyAck = -1; // Last consistency the client reported from us. + int LastVerifiedConsistency = -1; // Last consistency we checked from this client. If < CurrentNetConsistency, run through them. + int CurrentNetConsistency = -1; // Last consistency we got from this client. + int16_t NetConsistency[BACKUPTICS] = {}; // Consistencies we got from this client. + int16_t LocalConsistency[BACKUPTICS] = {}; // Local consistency of the client to check against. +}; + +enum ENetMode : uint8_t +{ + NET_PeerToPeer, + NET_PacketServer +}; + +enum EChatType +{ + CHAT_DISABLED, + CHAT_TEAM_ONLY, + CHAT_GLOBAL, +}; + +// New packet structure: +// +// One byte for the net command flags. +// Four bytes for the last sequence we got from that client. +// Four bytes for the last consistency we got from that client. +// If NCMD_QUITTERS set, one byte for the number of players followed by one byte for each player's consolenum. Packet server mode only. +// One byte for the number of players. +// One byte for the number of tics. +// If > 0, four bytes for the base sequence being worked from. +// One byte for the number of world tics ran. +// If > 0, four bytes for the base consistency being worked from. +// If in packet server mode and from the host, one byte for how far ahead of the host we are. +// For each player: +// One byte for the player number. +// If in packet server mode and from the host, two bytes for the latency to the host. +// For each consistency: +// One byte for the delta from the base consistency. +// Two bytes for each consistency. +// For each tic: +// One byte for the delta from the base sequence. +// The remaining command and event data for that player. // Create any new ticcmds and broadcast to other players. -void NetUpdate (void); +void NetUpdate(int tics); // Broadcasts special packets to other players // to notify of game exit @@ -59,74 +143,36 @@ void D_QuitNetGame (void); //? how many ticks to run? void TryRunTics (void); -//Use for checking to see if the netgame has stalled -void Net_CheckLastReceived(int); - // [RH] Functions for making and using special "ticcmds" -void Net_NewMakeTic (); -void Net_WriteInt8 (uint8_t); -void Net_WriteInt16 (int16_t); -void Net_WriteInt32 (int32_t); +void Net_NewClientTic(); +void Net_Initialize(); +void Net_WriteInt8(uint8_t); +void Net_WriteInt16(int16_t); +void Net_WriteInt32(int32_t); void Net_WriteInt64(int64_t); -void Net_WriteFloat (float); +void Net_WriteFloat(float); void Net_WriteDouble(double); -void Net_WriteString (const char *); -void Net_WriteBytes (const uint8_t *, int len); +void Net_WriteString(const char *); +void Net_WriteBytes(const uint8_t *, int len); -void Net_DoCommand (int type, uint8_t **stream, int player); -void Net_SkipCommand (int type, uint8_t **stream); - -void Net_ClearBuffers (); +void Net_DoCommand(int cmd, uint8_t **stream, int player); +void Net_SkipCommand(int cmd, uint8_t **stream); +void Net_ResetCommands(bool midTic); +void Net_SetWaiting(); +void Net_ClearBuffers(); // Netgame stuff (buffers and pointers, i.e. indices). // This is the interface to the packet driver, a separate program // in DOS, but just an abstraction here. -extern doomcom_t doomcom; - -extern struct ticcmd_t localcmds[LOCALCMDTICS]; - -extern int maketic; -extern int nettics[MAXNETNODES]; -extern int netdelay[MAXNETNODES][BACKUPTICS]; -extern int nodeforplayer[MAXPLAYERS]; - -extern ticcmd_t netcmds[MAXPLAYERS][BACKUPTICS]; -extern int ticdup; +extern doomcom_t doomcom; +extern usercmd_t LocalCmds[LOCALCMDTICS]; +extern int ClientTic; +extern FClientNetState ClientStates[MAXPLAYERS]; +extern ENetMode NetMode; class player_t; class DObject; - -// [RH] -// New generic packet structure: -// -// Header: -// One byte with following flags. -// One byte with starttic -// One byte with master's maketic (master -> slave only!) -// If NCMD_RETRANSMIT set, one byte with retransmitfrom -// If NCMD_XTICS set, one byte with number of tics (minus 3, so theoretically up to 258 tics in one packet) -// If NCMD_QUITTERS, one byte with number of players followed by one byte with each player's consolenum -// If NCMD_MULTI, one byte with number of players followed by one byte with each player's consolenum -// - The first player's consolenum is not included in this list, because it always matches the sender -// -// For each tic: -// Two bytes with consistancy check, followed by tic data -// -// Setup packets are different, and are described just before D_ArbitrateNetStart(). - -#define NCMD_EXIT 0x80 -#define NCMD_RETRANSMIT 0x40 -#define NCMD_SETUP 0x20 -#define NCMD_MULTI 0x10 // multiple players in this packet -#define NCMD_QUITTERS 0x08 // one or more players just quit (packet server only) -#define NCMD_COMPRESSED 0x04 // remainder of packet is compressed - -#define NCMD_XTICS 0x03 // packet contains >2 tics -#define NCMD_2TICS 0x02 // packet contains 2 tics -#define NCMD_1TICS 0x01 // packet contains 1 tic -#define NCMD_0TICS 0x00 // packet contains 0 tics - #endif diff --git a/src/d_protocol.cpp b/src/d_protocol.cpp index ec1750a12..044593c96 100644 --- a/src/d_protocol.cpp +++ b/src/d_protocol.cpp @@ -32,44 +32,42 @@ ** */ - #include "d_protocol.h" #include "d_net.h" #include "doomstat.h" #include "cmdlib.h" #include "serializer.h" - -char *ReadString (uint8_t **stream) +char* ReadString(uint8_t **stream) { char *string = *((char **)stream); - *stream += strlen (string) + 1; - return copystring (string); + *stream += strlen(string) + 1; + return copystring(string); } -const char *ReadStringConst(uint8_t **stream) +const char* ReadStringConst(uint8_t **stream) { const char *string = *((const char **)stream); - *stream += strlen (string) + 1; + *stream += strlen(string) + 1; return string; } -uint8_t ReadInt8 (uint8_t **stream) +uint8_t ReadInt8(uint8_t **stream) { uint8_t v = **stream; *stream += 1; return v; } -int16_t ReadInt16 (uint8_t **stream) +int16_t ReadInt16(uint8_t **stream) { int16_t v = (((*stream)[0]) << 8) | (((*stream)[1])); *stream += 2; return v; } -int32_t ReadInt32 (uint8_t **stream) +int32_t ReadInt32(uint8_t **stream) { int32_t v = (((*stream)[0]) << 24) | (((*stream)[1]) << 16) | (((*stream)[2]) << 8) | (((*stream)[3])); *stream += 4; @@ -84,7 +82,7 @@ int64_t ReadInt64(uint8_t** stream) return v; } -float ReadFloat (uint8_t **stream) +float ReadFloat(uint8_t **stream) { union { @@ -106,7 +104,7 @@ double ReadDouble(uint8_t** stream) return fakeint.f; } -void WriteString (const char *string, uint8_t **stream) +void WriteString(const char *string, uint8_t **stream) { char *p = *((char **)stream); @@ -118,21 +116,20 @@ void WriteString (const char *string, uint8_t **stream) *stream = (uint8_t *)p; } - -void WriteInt8 (uint8_t v, uint8_t **stream) +void WriteInt8(uint8_t v, uint8_t **stream) { **stream = v; *stream += 1; } -void WriteInt16 (int16_t v, uint8_t **stream) +void WriteInt16(int16_t v, uint8_t **stream) { (*stream)[0] = v >> 8; (*stream)[1] = v & 255; *stream += 2; } -void WriteInt32 (int32_t v, uint8_t **stream) +void WriteInt32(int32_t v, uint8_t **stream) { (*stream)[0] = v >> 24; (*stream)[1] = (v >> 16) & 255; @@ -154,7 +151,7 @@ void WriteInt64(int64_t v, uint8_t** stream) *stream += 8; } -void WriteFloat (float v, uint8_t **stream) +void WriteFloat(float v, uint8_t **stream) { union { @@ -176,171 +173,7 @@ void WriteDouble(double v, uint8_t** stream) WriteInt64(fakeint.i, stream); } -// Returns the number of bytes read -int UnpackUserCmd (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream) -{ - uint8_t *start = *stream; - uint8_t flags; - - if (basis != NULL) - { - if (basis != ucmd) - { - memcpy (ucmd, basis, sizeof(usercmd_t)); - } - } - else - { - memset (ucmd, 0, sizeof(usercmd_t)); - } - - flags = ReadInt8 (stream); - - if (flags) - { - // We can support up to 29 buttons, using from 0 to 4 bytes to store them. - if (flags & UCMDF_BUTTONS) - { - uint32_t buttons = ucmd->buttons; - uint8_t in = ReadInt8(stream); - - buttons = (buttons & ~0x7F) | (in & 0x7F); - if (in & 0x80) - { - in = ReadInt8(stream); - buttons = (buttons & ~(0x7F << 7)) | ((in & 0x7F) << 7); - if (in & 0x80) - { - in = ReadInt8(stream); - buttons = (buttons & ~(0x7F << 14)) | ((in & 0x7F) << 14); - if (in & 0x80) - { - in = ReadInt8(stream); - buttons = (buttons & ~(0xFF << 21)) | (in << 21); - } - } - } - ucmd->buttons = buttons; - } - if (flags & UCMDF_PITCH) - ucmd->pitch = ReadInt16 (stream); - if (flags & UCMDF_YAW) - ucmd->yaw = ReadInt16 (stream); - if (flags & UCMDF_FORWARDMOVE) - ucmd->forwardmove = ReadInt16 (stream); - if (flags & UCMDF_SIDEMOVE) - ucmd->sidemove = ReadInt16 (stream); - if (flags & UCMDF_UPMOVE) - ucmd->upmove = ReadInt16 (stream); - if (flags & UCMDF_ROLL) - ucmd->roll = ReadInt16 (stream); - } - - return int(*stream - start); -} - -// Returns the number of bytes written -int PackUserCmd (const usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream) -{ - uint8_t flags = 0; - uint8_t *temp = *stream; - uint8_t *start = *stream; - usercmd_t blank; - uint32_t buttons_changed; - - if (basis == NULL) - { - memset (&blank, 0, sizeof(blank)); - basis = ␣ - } - - WriteInt8 (0, stream); // Make room for the packing bits - - buttons_changed = ucmd->buttons ^ basis->buttons; - if (buttons_changed != 0) - { - uint8_t bytes[4] = { uint8_t(ucmd->buttons & 0x7F), - uint8_t((ucmd->buttons >> 7) & 0x7F), - uint8_t((ucmd->buttons >> 14) & 0x7F), - uint8_t((ucmd->buttons >> 21) & 0xFF) }; - - flags |= UCMDF_BUTTONS; - - if (buttons_changed & 0xFFFFFF80) - { - bytes[0] |= 0x80; - if (buttons_changed & 0xFFFFC000) - { - bytes[1] |= 0x80; - if (buttons_changed & 0xFFE00000) - { - bytes[2] |= 0x80; - } - } - } - WriteInt8 (bytes[0], stream); - if (bytes[0] & 0x80) - { - WriteInt8 (bytes[1], stream); - if (bytes[1] & 0x80) - { - WriteInt8 (bytes[2], stream); - if (bytes[2] & 0x80) - { - WriteInt8 (bytes[3], stream); - } - } - } - } - if (ucmd->pitch != basis->pitch) - { - flags |= UCMDF_PITCH; - WriteInt16 (ucmd->pitch, stream); - } - if (ucmd->yaw != basis->yaw) - { - flags |= UCMDF_YAW; - WriteInt16 (ucmd->yaw, stream); - } - if (ucmd->forwardmove != basis->forwardmove) - { - flags |= UCMDF_FORWARDMOVE; - WriteInt16 (ucmd->forwardmove, stream); - } - if (ucmd->sidemove != basis->sidemove) - { - flags |= UCMDF_SIDEMOVE; - WriteInt16 (ucmd->sidemove, stream); - } - if (ucmd->upmove != basis->upmove) - { - flags |= UCMDF_UPMOVE; - WriteInt16 (ucmd->upmove, stream); - } - if (ucmd->roll != basis->roll) - { - flags |= UCMDF_ROLL; - WriteInt16 (ucmd->roll, stream); - } - - // Write the packing bits - WriteInt8 (flags, &temp); - - return int(*stream - start); -} - -FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &cmd, ticcmd_t *def) -{ - if (arc.BeginObject(key)) - { - arc("consistency", cmd.consistancy) - ("ucmd", cmd.ucmd) - .EndObject(); - } - return arc; -} - -FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def) +FSerializer& Serialize(FSerializer& arc, const char* key, usercmd_t& cmd, usercmd_t* def) { // This used packed data with the old serializer but that's totally counterproductive when // having a text format that is human-readable. So this compression has been undone here. @@ -360,192 +193,308 @@ FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercm return arc; } -int WriteUserCmdMessage (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream) +// Returns the number of bytes read +int UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) { - if (basis == NULL) + const uint8_t* start = stream; + + if (basis != nullptr) { - if (ucmd->buttons != 0 || - ucmd->pitch != 0 || - ucmd->yaw != 0 || - ucmd->forwardmove != 0 || - ucmd->sidemove != 0 || - ucmd->upmove != 0 || - ucmd->roll != 0) - { - WriteInt8 (DEM_USERCMD, stream); - return PackUserCmd (ucmd, basis, stream) + 1; - } + if (basis != &cmd) + memcpy(&cmd, basis, sizeof(usercmd_t)); } else - if (ucmd->buttons != basis->buttons || - ucmd->pitch != basis->pitch || - ucmd->yaw != basis->yaw || - ucmd->forwardmove != basis->forwardmove || - ucmd->sidemove != basis->sidemove || - ucmd->upmove != basis->upmove || - ucmd->roll != basis->roll) { - WriteInt8 (DEM_USERCMD, stream); - return PackUserCmd (ucmd, basis, stream) + 1; + memset(&cmd, 0, sizeof(usercmd_t)); } - WriteInt8 (DEM_EMPTYUSERCMD, stream); + uint8_t flags = ReadInt8(&stream); + if (flags) + { + // We can support up to 29 buttons using 1 to 4 bytes to store them. The most + // significant bit of each button byte is a flag to indicate whether or not more buttons + // should be read in excluding the last one which supports all 8 bits. + if (flags & UCMDF_BUTTONS) + { + uint8_t in = ReadInt8(&stream); + uint32_t buttons = (cmd.buttons & ~0x7F) | (in & 0x7F); + if (in & MoreButtons) + { + in = ReadInt8(&stream); + buttons = (buttons & ~(0x7F << 7)) | ((in & 0x7F) << 7); + if (in & MoreButtons) + { + in = ReadInt8(&stream); + buttons = (buttons & ~(0x7F << 14)) | ((in & 0x7F) << 14); + if (in & MoreButtons) + { + in = ReadInt8(&stream); + buttons = (buttons & ~(0xFF << 21)) | (in << 21); + } + } + } + cmd.buttons = buttons; + } + if (flags & UCMDF_PITCH) + cmd.pitch = ReadInt16(&stream); + if (flags & UCMDF_YAW) + cmd.yaw = ReadInt16(&stream); + if (flags & UCMDF_FORWARDMOVE) + cmd.forwardmove = ReadInt16(&stream); + if (flags & UCMDF_SIDEMOVE) + cmd.sidemove = ReadInt16(&stream); + if (flags & UCMDF_UPMOVE) + cmd.upmove = ReadInt16(&stream); + if (flags & UCMDF_ROLL) + cmd.roll = ReadInt16(&stream); + } + + return int(stream - start); +} + +int PackUserCmd(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +{ + uint8_t flags = 0; + uint8_t* start = stream; + + usercmd_t blank; + if (basis == nullptr) + { + memset(&blank, 0, sizeof(blank)); + basis = ␣ + } + + ++stream; // Make room for the flags. + uint32_t buttons_changed = cmd.buttons ^ basis->buttons; + if (buttons_changed != 0) + { + uint8_t bytes[4] = { uint8_t(cmd.buttons & 0x7F), + uint8_t((cmd.buttons >> 7) & 0x7F), + uint8_t((cmd.buttons >> 14) & 0x7F), + uint8_t((cmd.buttons >> 21) & 0xFF) }; + + flags |= UCMDF_BUTTONS; + if (buttons_changed & 0xFFFFFF80) + { + bytes[0] |= MoreButtons; + if (buttons_changed & 0xFFFFC000) + { + bytes[1] |= MoreButtons; + if (buttons_changed & 0xFFE00000) + bytes[2] |= MoreButtons; + } + } + WriteInt8(bytes[0], &stream); + if (bytes[0] & MoreButtons) + { + WriteInt8(bytes[1], &stream); + if (bytes[1] & MoreButtons) + { + WriteInt8(bytes[2], &stream); + if (bytes[2] & MoreButtons) + WriteInt8(bytes[3], &stream); + } + } + } + if (cmd.pitch != basis->pitch) + { + flags |= UCMDF_PITCH; + WriteInt16(cmd.pitch, &stream); + } + if (cmd.yaw != basis->yaw) + { + flags |= UCMDF_YAW; + WriteInt16 (cmd.yaw, &stream); + } + if (cmd.forwardmove != basis->forwardmove) + { + flags |= UCMDF_FORWARDMOVE; + WriteInt16 (cmd.forwardmove, &stream); + } + if (cmd.sidemove != basis->sidemove) + { + flags |= UCMDF_SIDEMOVE; + WriteInt16(cmd.sidemove, &stream); + } + if (cmd.upmove != basis->upmove) + { + flags |= UCMDF_UPMOVE; + WriteInt16(cmd.upmove, &stream); + } + if (cmd.roll != basis->roll) + { + flags |= UCMDF_ROLL; + WriteInt16(cmd.roll, &stream); + } + + // Write the packing bits + WriteInt8(flags, &start); + + return int(stream - start); +} + +int WriteUserCmdMessage(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +{ + if (basis == nullptr) + { + if (cmd.buttons + || cmd.pitch || cmd.yaw || cmd.roll + || cmd.forwardmove || cmd.sidemove || cmd.upmove) + { + WriteInt8(DEM_USERCMD, &stream); + return PackUserCmd(cmd, basis, stream) + 1; + } + } + else if (cmd.buttons != basis->buttons + || cmd.yaw != basis->yaw || cmd.pitch != basis->pitch || cmd.roll != basis->roll + || cmd.forwardmove != basis->forwardmove || cmd.sidemove != basis->sidemove || cmd.upmove != basis->upmove) + { + WriteInt8(DEM_USERCMD, &stream); + return PackUserCmd(cmd, basis, stream) + 1; + } + + WriteInt8(DEM_EMPTYUSERCMD, &stream); return 1; } - -int SkipTicCmd (uint8_t **stream, int count) +// Reads through the user command without actually setting any of its info. Used to get the size +// of the command when getting the length of the stream. +int SkipUserCmdMessage(uint8_t*& stream) { - int i, skip; - uint8_t *flow = *stream; + const uint8_t* start = stream; - for (i = count; i > 0; i--) + while (true) { - bool moreticdata = true; - - flow += 2; // Skip consistancy marker - while (moreticdata) + const uint8_t type = *stream++; + if (type == DEM_USERCMD) { - uint8_t type = *flow++; - - if (type == DEM_USERCMD) + int skip = 1; + if (*stream & UCMDF_PITCH) + skip += 2; + if (*stream & UCMDF_YAW) + skip += 2; + if (*stream & UCMDF_FORWARDMOVE) + skip += 2; + if (*stream & UCMDF_SIDEMOVE) + skip += 2; + if (*stream & UCMDF_UPMOVE) + skip += 2; + if (*stream & UCMDF_ROLL) + skip += 2; + if (*stream & UCMDF_BUTTONS) { - moreticdata = false; - skip = 1; - if (*flow & UCMDF_PITCH) skip += 2; - if (*flow & UCMDF_YAW) skip += 2; - if (*flow & UCMDF_FORWARDMOVE) skip += 2; - if (*flow & UCMDF_SIDEMOVE) skip += 2; - if (*flow & UCMDF_UPMOVE) skip += 2; - if (*flow & UCMDF_ROLL) skip += 2; - if (*flow & UCMDF_BUTTONS) + if (*++stream & MoreButtons) { - if (*++flow & 0x80) + if (*++stream & MoreButtons) { - if (*++flow & 0x80) - { - if (*++flow & 0x80) - { - ++flow; - } - } + if (*++stream & MoreButtons) + ++stream; } } - flow += skip; - } - else if (type == DEM_EMPTYUSERCMD) - { - moreticdata = false; - } - else - { - Net_SkipCommand (type, &flow); } + stream += skip; + break; } - } - - skip = int(flow - *stream); - *stream = flow; - - return skip; -} - -extern short consistancy[MAXPLAYERS][BACKUPTICS]; -void ReadTicCmd (uint8_t **stream, int player, int tic) -{ - int type; - uint8_t *start; - ticcmd_t *tcmd; - - int ticmod = tic % BACKUPTICS; - - tcmd = &netcmds[player][ticmod]; - tcmd->consistancy = ReadInt16 (stream); - - start = *stream; - - while ((type = ReadInt8 (stream)) != DEM_USERCMD && type != DEM_EMPTYUSERCMD) - Net_SkipCommand (type, stream); - - NetSpecs[player][ticmod].SetData (start, int(*stream - start - 1)); - - if (type == DEM_USERCMD) - { - UnpackUserCmd (&tcmd->ucmd, - tic ? &netcmds[player][(tic-1)%BACKUPTICS].ucmd : NULL, stream); - } - else - { - if (tic) + else if (type == DEM_EMPTYUSERCMD) { - memcpy (&tcmd->ucmd, &netcmds[player][(tic-1)%BACKUPTICS].ucmd, sizeof(tcmd->ucmd)); + break; } else { - memset (&tcmd->ucmd, 0, sizeof(tcmd->ucmd)); + Net_SkipCommand(type, &stream); } } - if (player==consoleplayer&&tic>BACKUPTICS) - assert(consistancy[player][ticmod] == tcmd->consistancy); + return int(stream - start); } -void RunNetSpecs (int player, int buf) +int ReadUserCmdMessage(uint8_t*& stream, int player, int tic) { - uint8_t *stream; - int len; + const int ticMod = tic % BACKUPTICS; - if (gametic % ticdup == 0) + auto& curTic = ClientStates[player].Tics[ticMod]; + usercmd_t& ticCmd = curTic.Command; + + const uint8_t* start = stream; + + // Skip until we reach the player command. Event data will get read off once the + // tick is actually executed. + int type; + while ((type = ReadInt8(&stream)) != DEM_USERCMD && type != DEM_EMPTYUSERCMD) + Net_SkipCommand(type, &stream); + + // Subtract a byte to account for the fact the stream head is now sitting on the + // user command. + curTic.Data.SetData(start, int(stream - start - 1)); + + if (type == DEM_USERCMD) { - stream = NetSpecs[player][buf].GetData (&len); - if (stream) - { - uint8_t *end = stream + len; - while (stream < end) - { - int type = ReadInt8 (&stream); - Net_DoCommand (type, &stream, player); - } - if (!demorecording) - NetSpecs[player][buf].SetData (NULL, 0); - } + UnpackUserCmd(ticCmd, + tic > 0 ? &ClientStates[player].Tics[(tic - 1) % BACKUPTICS].Command : nullptr, stream); + } + else + { + if (tic > 0) + memcpy(&ticCmd, &ClientStates[player].Tics[(tic - 1) % BACKUPTICS].Command, sizeof(ticCmd)); + else + memset(&ticCmd, 0, sizeof(ticCmd)); + } + + return int(stream - start); +} + +void RunPlayerCommands(int player, int tic) +{ + // We don't have the full command yet, so don't run it. + if (gametic % doomcom.ticdup) + return; + + int len; + auto& data = ClientStates[player].Tics[tic % BACKUPTICS].Data; + uint8_t* stream = data.GetData(&len); + if (stream != nullptr) + { + uint8_t* end = stream + len; + while (stream < end) + Net_DoCommand(ReadInt8(&stream), &stream, player); + + if (!demorecording) + data.SetData(nullptr, 0); } } -uint8_t *lenspot; +// Demo related functionality + +uint8_t* streamPos = nullptr; // Write the header of an IFF chunk and leave space // for the length field. -void StartChunk (int id, uint8_t **stream) +void StartChunk(int id, uint8_t **stream) { - WriteInt32 (id, stream); - lenspot = *stream; + WriteInt32(id, stream); + streamPos = *stream; *stream += 4; } // Write the length field for the chunk and insert // pad byte if the chunk is odd-sized. -void FinishChunk (uint8_t **stream) +void FinishChunk(uint8_t **stream) { - int len; - - if (!lenspot) + if (streamPos == nullptr) return; - len = int(*stream - lenspot - 4); - WriteInt32 (len, &lenspot); + int len = int(*stream - streamPos - 4); + WriteInt32(len, &streamPos); if (len & 1) - WriteInt8 (0, stream); + WriteInt8(0, stream); - lenspot = NULL; + streamPos = nullptr; } // Skip past an unknown chunk. *stream should be // pointing to the chunk's length field. -void SkipChunk (uint8_t **stream) +void SkipChunk(uint8_t **stream) { - int len; - - len = ReadInt32 (stream); + int len = ReadInt32(stream); *stream += len + (len & 1); } diff --git a/src/d_protocol.h b/src/d_protocol.h index f726a6ef2..10b38f801 100644 --- a/src/d_protocol.h +++ b/src/d_protocol.h @@ -50,6 +50,7 @@ #define NETD_ID BIGE_ID('N','E','T','D') #define WEAP_ID BIGE_ID('W','E','A','P') +constexpr int MoreButtons = 0x80; struct zdemoheader_s { uint8_t demovermajor; @@ -165,6 +166,7 @@ enum EDemoCommand DEM_ENDSCREENJOB, DEM_ZSC_CMD, // 74 String: Command, Word: Byte size of command DEM_CHANGESKILL, // 75 Int: Skill + DEM_KICK, // 76 Byte: Player number }; // The following are implemented by cht_DoCheat in m_cheat.cpp @@ -230,23 +232,14 @@ void StartChunk (int id, uint8_t **stream); void FinishChunk (uint8_t **stream); void SkipChunk (uint8_t **stream); -int UnpackUserCmd (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream); -int PackUserCmd (const usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream); -int WriteUserCmdMessage (usercmd_t *ucmd, const usercmd_t *basis, uint8_t **stream); +// Returns the number of bytes written to the stream +int UnpackUserCmd(usercmd_t& ucmd, const usercmd_t* basis, uint8_t*& stream); +int PackUserCmd(const usercmd_t& ucmd, const usercmd_t* basis, uint8_t*& stream); +int WriteUserCmdMessage(const usercmd_t& ucmd, const usercmd_t *basis, uint8_t*& stream); -// The data sampled per tick (single player) -// and transmitted to other peers (multiplayer). -// Mainly movements/button commands per game tick, -// plus a checksum for internal state consistency. -struct ticcmd_t -{ - usercmd_t ucmd; - int16_t consistancy; // checks for net game -}; - -int SkipTicCmd (uint8_t **stream, int count); -void ReadTicCmd (uint8_t **stream, int player, int tic); -void RunNetSpecs (int player, int buf); +int SkipUserCmdMessage(uint8_t*& stream); +int ReadUserCmdMessage(uint8_t*& stream, int player, int tic); +void RunPlayerCommands(int player, int tic); uint8_t ReadInt8 (uint8_t **stream); int16_t ReadInt16 (uint8_t **stream); diff --git a/src/doomdef.h b/src/doomdef.h index f1f4f352e..ce9b82316 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -64,7 +64,7 @@ constexpr int TICRATE = 35; enum { // The maximum number of players, multiplayer/networking. - MAXPLAYERS = 8, + MAXPLAYERS = 16, // Amount of damage done by a telefrag. TELEFRAG_DAMAGE = 1000000 diff --git a/src/g_game.cpp b/src/g_game.cpp index 786bf0629..c8b990150 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -100,8 +100,8 @@ extern int startpos, laststartpos; bool WriteZip(const char* filename, const FileSys::FCompressedBuffer* content, size_t contentcount); bool G_CheckDemoStatus (void); -void G_ReadDemoTiccmd (ticcmd_t *cmd, int player); -void G_WriteDemoTiccmd (ticcmd_t *cmd, int player, int buf); +void G_ReadDemoTiccmd (usercmd_t *cmd, int player); +void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf); void G_PlayerReborn (int player); void G_DoNewGame (void); @@ -110,6 +110,7 @@ void G_DoPlayDemo (void); void G_DoCompleted (void); void G_DoVictory (void); void G_DoWorldDone (void); +void G_DoMapWarp(); void G_DoSaveGame (bool okForQuicksave, bool forceQuicksave, FString filename, const char *description); void G_DoAutoSave (); void G_DoQuickSave (); @@ -126,6 +127,7 @@ CVAR (Bool, cl_waitforsave, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, enablescriptscreenshot, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, cl_restartondeath, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); EXTERN_CVAR (Float, con_midtime); +EXTERN_CVAR(Bool, net_disablepause) //========================================================================== // @@ -185,8 +187,6 @@ uint8_t* zdembodyend; // end of ZDEM BODY chunk bool singledemo; // quit after playing a demo from cmdline bool precache = true; // if true, load all graphics at start - -short consistancy[MAXPLAYERS][BACKUPTICS]; #define MAXPLMOVE (forwardmove[1]) @@ -353,6 +353,12 @@ CCMD (land) CCMD (pause) { + if (netgame && !players[consoleplayer].settings_controller && net_disablepause) + { + Printf("Only settings controllers can currently (un)pause the game\n"); + return; + } + sendpause = true; } @@ -580,9 +586,9 @@ FBaseCVar* G_GetUserCVar(int playernum, const char* cvarname) return cvar; } -static ticcmd_t emptycmd; +static usercmd_t emptycmd; -ticcmd_t* G_BaseTiccmd() +usercmd_t* G_BaseTiccmd() { return &emptycmd; } @@ -594,7 +600,7 @@ ticcmd_t* G_BaseTiccmd() // or reads it from the demo buffer. // If recording a demo, write it out // -void G_BuildTiccmd (ticcmd_t *cmd) +void G_BuildTiccmd (usercmd_t *cmd) { int strafe; int speed; @@ -602,13 +608,11 @@ void G_BuildTiccmd (ticcmd_t *cmd) int side; int fly; - ticcmd_t *base; + usercmd_t *base; base = G_BaseTiccmd (); *cmd = *base; - cmd->consistancy = consistancy[consoleplayer][(maketic/ticdup)%BACKUPTICS]; - strafe = buttonMap.ButtonDown(Button_Strafe); speed = buttonMap.ButtonDown(Button_Speed) ^ (int)cl_run; @@ -618,7 +622,7 @@ void G_BuildTiccmd (ticcmd_t *cmd) // and not the joystick, since we treat the joystick as // the analog device it is. if (buttonMap.ButtonDown(Button_Left) || buttonMap.ButtonDown(Button_Right)) - turnheld += ticdup; + turnheld += doomcom.ticdup; else turnheld = 0; @@ -682,33 +686,33 @@ void G_BuildTiccmd (ticcmd_t *cmd) side -= sidemove[speed]; // buttons - if (buttonMap.ButtonDown(Button_Attack)) cmd->ucmd.buttons |= BT_ATTACK; - if (buttonMap.ButtonDown(Button_AltAttack)) cmd->ucmd.buttons |= BT_ALTATTACK; - if (buttonMap.ButtonDown(Button_Use)) cmd->ucmd.buttons |= BT_USE; - if (buttonMap.ButtonDown(Button_Jump)) cmd->ucmd.buttons |= BT_JUMP; - if (buttonMap.ButtonDown(Button_Crouch)) cmd->ucmd.buttons |= BT_CROUCH; - if (buttonMap.ButtonDown(Button_Zoom)) cmd->ucmd.buttons |= BT_ZOOM; - if (buttonMap.ButtonDown(Button_Reload)) cmd->ucmd.buttons |= BT_RELOAD; + if (buttonMap.ButtonDown(Button_Attack)) cmd->buttons |= BT_ATTACK; + if (buttonMap.ButtonDown(Button_AltAttack)) cmd->buttons |= BT_ALTATTACK; + if (buttonMap.ButtonDown(Button_Use)) cmd->buttons |= BT_USE; + if (buttonMap.ButtonDown(Button_Jump)) cmd->buttons |= BT_JUMP; + if (buttonMap.ButtonDown(Button_Crouch)) cmd->buttons |= BT_CROUCH; + if (buttonMap.ButtonDown(Button_Zoom)) cmd->buttons |= BT_ZOOM; + if (buttonMap.ButtonDown(Button_Reload)) cmd->buttons |= BT_RELOAD; - if (buttonMap.ButtonDown(Button_User1)) cmd->ucmd.buttons |= BT_USER1; - if (buttonMap.ButtonDown(Button_User2)) cmd->ucmd.buttons |= BT_USER2; - if (buttonMap.ButtonDown(Button_User3)) cmd->ucmd.buttons |= BT_USER3; - if (buttonMap.ButtonDown(Button_User4)) cmd->ucmd.buttons |= BT_USER4; + if (buttonMap.ButtonDown(Button_User1)) cmd->buttons |= BT_USER1; + if (buttonMap.ButtonDown(Button_User2)) cmd->buttons |= BT_USER2; + if (buttonMap.ButtonDown(Button_User3)) cmd->buttons |= BT_USER3; + if (buttonMap.ButtonDown(Button_User4)) cmd->buttons |= BT_USER4; - if (buttonMap.ButtonDown(Button_Speed)) cmd->ucmd.buttons |= BT_SPEED; - if (buttonMap.ButtonDown(Button_Strafe)) cmd->ucmd.buttons |= BT_STRAFE; - if (buttonMap.ButtonDown(Button_MoveRight)) cmd->ucmd.buttons |= BT_MOVERIGHT; - if (buttonMap.ButtonDown(Button_MoveLeft)) cmd->ucmd.buttons |= BT_MOVELEFT; - if (buttonMap.ButtonDown(Button_LookDown)) cmd->ucmd.buttons |= BT_LOOKDOWN; - if (buttonMap.ButtonDown(Button_LookUp)) cmd->ucmd.buttons |= BT_LOOKUP; - if (buttonMap.ButtonDown(Button_Back)) cmd->ucmd.buttons |= BT_BACK; - if (buttonMap.ButtonDown(Button_Forward)) cmd->ucmd.buttons |= BT_FORWARD; - if (buttonMap.ButtonDown(Button_Right)) cmd->ucmd.buttons |= BT_RIGHT; - if (buttonMap.ButtonDown(Button_Left)) cmd->ucmd.buttons |= BT_LEFT; - if (buttonMap.ButtonDown(Button_MoveDown)) cmd->ucmd.buttons |= BT_MOVEDOWN; - if (buttonMap.ButtonDown(Button_MoveUp)) cmd->ucmd.buttons |= BT_MOVEUP; - if (buttonMap.ButtonDown(Button_ShowScores)) cmd->ucmd.buttons |= BT_SHOWSCORES; - if (speed) cmd->ucmd.buttons |= BT_RUN; + if (buttonMap.ButtonDown(Button_Speed)) cmd->buttons |= BT_SPEED; + if (buttonMap.ButtonDown(Button_Strafe)) cmd->buttons |= BT_STRAFE; + if (buttonMap.ButtonDown(Button_MoveRight)) cmd->buttons |= BT_MOVERIGHT; + if (buttonMap.ButtonDown(Button_MoveLeft)) cmd->buttons |= BT_MOVELEFT; + if (buttonMap.ButtonDown(Button_LookDown)) cmd->buttons |= BT_LOOKDOWN; + if (buttonMap.ButtonDown(Button_LookUp)) cmd->buttons |= BT_LOOKUP; + if (buttonMap.ButtonDown(Button_Back)) cmd->buttons |= BT_BACK; + if (buttonMap.ButtonDown(Button_Forward)) cmd->buttons |= BT_FORWARD; + if (buttonMap.ButtonDown(Button_Right)) cmd->buttons |= BT_RIGHT; + if (buttonMap.ButtonDown(Button_Left)) cmd->buttons |= BT_LEFT; + if (buttonMap.ButtonDown(Button_MoveDown)) cmd->buttons |= BT_MOVEDOWN; + if (buttonMap.ButtonDown(Button_MoveUp)) cmd->buttons |= BT_MOVEUP; + if (buttonMap.ButtonDown(Button_ShowScores)) cmd->buttons |= BT_SHOWSCORES; + if (speed) cmd->buttons |= BT_RUN; // Handle joysticks/game controllers. float joyaxes[NUM_JOYAXIS]; @@ -746,7 +750,7 @@ void G_BuildTiccmd (ticcmd_t *cmd) forward += xs_CRoundToInt(mousey * m_forward); } - cmd->ucmd.pitch = LocalViewPitch >> 16; + cmd->pitch = LocalViewPitch >> 16; if (SendLand) { @@ -769,10 +773,10 @@ void G_BuildTiccmd (ticcmd_t *cmd) else if (side < -MAXPLMOVE) side = -MAXPLMOVE; - cmd->ucmd.forwardmove += forward; - cmd->ucmd.sidemove += side; - cmd->ucmd.yaw = LocalViewAngle >> 16; - cmd->ucmd.upmove = fly; + cmd->forwardmove += forward; + cmd->sidemove += side; + cmd->yaw = LocalViewAngle >> 16; + cmd->upmove = fly; LocalViewAngle = 0; LocalViewPitch = 0; @@ -780,7 +784,7 @@ void G_BuildTiccmd (ticcmd_t *cmd) if (sendturn180) { sendturn180 = false; - cmd->ucmd.buttons |= BT_TURN180; + cmd->buttons |= BT_TURN180; } if (sendpause) { @@ -814,8 +818,8 @@ void G_BuildTiccmd (ticcmd_t *cmd) SendItemDrop = NULL; } - cmd->ucmd.forwardmove <<= 8; - cmd->ucmd.sidemove <<= 8; + cmd->forwardmove <<= 8; + cmd->sidemove <<= 8; } static int LookAdjust(int look) @@ -1111,53 +1115,25 @@ static void G_FullConsole() } -//========================================================================== -// -// FRandom :: StaticSumSeeds -// -// This function produces a uint32_t that can be used to check the consistancy -// of network games between different machines. Only a select few RNGs are -// used for the sum, because not all RNGs are important to network sync. -// -//========================================================================== - -extern FRandom pr_spawnmobj; -extern FRandom pr_acs; -extern FRandom pr_chase; -extern FRandom pr_damagemobj; - -static uint32_t StaticSumSeeds() -{ - return - pr_spawnmobj.Seed() + - pr_acs.Seed() + - pr_chase.Seed() + - pr_damagemobj.Seed(); -} - // // G_Ticker // Make ticcmd_ts for the players. // void G_Ticker () { - int i; gamestate_t oldgamestate; // do player reborns if needed - for (i = 0; i < MAXPLAYERS; i++) + // TODO: These should really be moved to queues. + for (int i = 0; i < MAXPLAYERS; ++i) { - if (playeringame[i]) - { - if (players[i].playerstate == PST_GONE) - { - G_DoPlayerPop(i); - } - if (players[i].playerstate == PST_REBORN || players[i].playerstate == PST_ENTER) - { - primaryLevel->DoReborn(i); - } - } + if (!playeringame[i]) + continue; + + if (players[i].playerstate == PST_GONE) + G_DoPlayerPop(i); + else if (players[i].playerstate == PST_REBORN || players[i].playerstate == PST_ENTER) + primaryLevel->DoReborn(i, false); } if (ToggleFullscreen) @@ -1207,6 +1183,9 @@ void G_Ticker () case ga_playdemo: G_DoPlayDemo (); break; + case ga_mapwarp: + G_DoMapWarp(); + break; case ga_completed: G_DoCompleted (); break; @@ -1253,68 +1232,34 @@ void G_Ticker () } // get commands, check consistancy, and build new consistancy check - int buf = (gametic/ticdup)%BACKUPTICS; - - // [RH] Include some random seeds and player stuff in the consistancy - // check, not just the player's x position like BOOM. - uint32_t rngsum = StaticSumSeeds (); + const int curTic = gametic / doomcom.ticdup; //Added by MC: For some of that bot stuff. The main bot function. primaryLevel->BotInfo.Main (primaryLevel); - for (i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (playeringame[i]) + usercmd_t *cmd = &players[client].cmd; + usercmd_t* nextCmd = &ClientStates[client].Tics[curTic % BACKUPTICS].Command; + + RunPlayerCommands(client, curTic); + if (demorecording) + G_WriteDemoTiccmd(nextCmd, client, curTic); + + players[client].oldbuttons = cmd->buttons; + // If the user alt-tabbed away, paused gets set to -1. In this case, + // we do not want to read more demo commands until paused is no + // longer negative. + if (demoplayback) + G_ReadDemoTiccmd(cmd, client); + else + memcpy(cmd, nextCmd, sizeof(usercmd_t)); + + // check for turbo cheats + if (multiplayer && turbo > 100.f && cmd->forwardmove > TURBOTHRESHOLD && + !(gametic & 31) && ((gametic >> 5) & (MAXPLAYERS-1)) == client) { - ticcmd_t *cmd = &players[i].cmd; - ticcmd_t *newcmd = &netcmds[i][buf]; - - if ((gametic % ticdup) == 0) - { - RunNetSpecs (i, buf); - } - if (demorecording) - { - G_WriteDemoTiccmd (newcmd, i, buf); - } - players[i].oldbuttons = cmd->ucmd.buttons; - // If the user alt-tabbed away, paused gets set to -1. In this case, - // we do not want to read more demo commands until paused is no - // longer negative. - if (demoplayback) - { - G_ReadDemoTiccmd (cmd, i); - } - else - { - memcpy(cmd, newcmd, sizeof(ticcmd_t)); - } - - // check for turbo cheats - if (multiplayer && turbo > 100.f && cmd->ucmd.forwardmove > TURBOTHRESHOLD && - !(gametic&31) && ((gametic>>5)&(MAXPLAYERS-1)) == i ) - { - Printf ("%s is turbo!\n", players[i].userinfo.GetName()); - } - - if (netgame && players[i].Bot == NULL && !demoplayback && (gametic%ticdup) == 0) - { - //players[i].inconsistant = 0; - if (gametic > BACKUPTICS*ticdup && consistancy[i][buf] != cmd->consistancy) - { - players[i].inconsistant = gametic - BACKUPTICS*ticdup; - } - if (players[i].mo) - { - uint32_t sum = rngsum + int((players[i].mo->X() + players[i].mo->Y() + players[i].mo->Z())*257) + players[i].mo->Angles.Yaw.BAMs() + players[i].mo->Angles.Pitch.BAMs(); - sum ^= players[i].health; - consistancy[i][buf] = sum; - } - else - { - consistancy[i][buf] = rngsum; - } - } + Printf("%s is turbo!\n", players[client].userinfo.GetName()); } } @@ -2026,6 +1971,7 @@ void FinishLoadingCVars(); void G_DoLoadGame () { + Net_ResetCommands(true); SetupLoadingCVars(); bool hidecon; @@ -2182,6 +2128,7 @@ void G_DoLoadGame () NextSkill = -1; arc("nextskill", NextSkill); + Net_SetWaiting(); if (level.info != nullptr) level.info->Snapshot.Clean(); @@ -2526,7 +2473,7 @@ void G_DoSaveGame (bool okForQuicksave, bool forceQuicksave, FString filename, c // DEMO RECORDING // -void G_ReadDemoTiccmd (ticcmd_t *cmd, int player) +void G_ReadDemoTiccmd (usercmd_t *cmd, int player) { int id = DEM_BAD; @@ -2549,7 +2496,7 @@ void G_ReadDemoTiccmd (ticcmd_t *cmd, int player) break; case DEM_USERCMD: - UnpackUserCmd (&cmd->ucmd, &cmd->ucmd, &demo_p); + UnpackUserCmd (*cmd, cmd, demo_p); break; case DEM_EMPTYUSERCMD: @@ -2580,9 +2527,9 @@ CCMD (stop) stoprecording = true; } -extern uint8_t *lenspot; +extern uint8_t *streamPos; -void G_WriteDemoTiccmd (ticcmd_t *cmd, int player, int buf) +void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf) { uint8_t *specdata; int speclen; @@ -2598,28 +2545,29 @@ void G_WriteDemoTiccmd (ticcmd_t *cmd, int player, int buf) } // [RH] Write any special "ticcmds" for this player to the demo - if ((specdata = NetSpecs[player][buf].GetData (&speclen)) && gametic % ticdup == 0) + if ((specdata = ClientStates[player].Tics[buf % BACKUPTICS].Data.GetData (&speclen)) && !(gametic % doomcom.ticdup)) { memcpy (demo_p, specdata, speclen); demo_p += speclen; - NetSpecs[player][buf].SetData (NULL, 0); + + ClientStates[player].Tics[buf % BACKUPTICS].Data.SetData(nullptr, 0); } // [RH] Now write out a "normal" ticcmd. - WriteUserCmdMessage (&cmd->ucmd, &players[player].cmd.ucmd, &demo_p); + WriteUserCmdMessage (*cmd, &players[player].cmd, demo_p); // [RH] Bigger safety margin if (demo_p > demobuffer + maxdemosize - 64) { ptrdiff_t pos = demo_p - demobuffer; - ptrdiff_t spot = lenspot - demobuffer; + ptrdiff_t spot = streamPos - demobuffer; ptrdiff_t comp = democompspot - demobuffer; ptrdiff_t body = demobodyspot - demobuffer; // [RH] Allocate more space for the demo maxdemosize += 0x20000; demobuffer = (uint8_t *)M_Realloc (demobuffer, maxdemosize); demo_p = demobuffer + pos; - lenspot = demobuffer + spot; + streamPos = demobuffer + spot; democompspot = demobuffer + comp; demobodyspot = demobuffer + body; } @@ -3001,7 +2949,6 @@ void G_TimeDemo (const char* name) nodrawers = !!Args->CheckParm ("-nodraw"); noblit = !!Args->CheckParm ("-noblit"); timingdemo = true; - singletics = true; defdemoname = name; gameaction = (gameaction == ga_loadgame) ? ga_loadgameplaydemo : ga_playdemo; @@ -3041,7 +2988,6 @@ bool G_CheckDemoStatus (void) demoplayback = false; netgame = false; multiplayer = false; - singletics = false; for (int i = 1; i < MAXPLAYERS; i++) playeringame[i] = 0; consoleplayer = 0; diff --git a/src/g_level.cpp b/src/g_level.cpp index 9fe3c9699..f39588cae 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1560,13 +1560,28 @@ DEFINE_ACTION_FUNCTION(FLevelLocals, WorldDone) return 0; } +//========================================================================== +// +// Started the game loaded into a map from the console. Run at least one tic +// so it can properly render out in net games. +// +//========================================================================== + +void G_DoMapWarp() +{ + gameaction = ga_nothing; + Net_ResetCommands(true); + Net_SetWaiting(); +} + //========================================================================== // // //========================================================================== void G_DoWorldDone (void) -{ +{ + Net_ResetCommands(true); gamestate = GS_LEVEL; if (nextlevel.IsEmpty()) { @@ -1578,6 +1593,7 @@ void G_DoWorldDone (void) G_DoLoadLevel (nextlevel, startpos, true, false); gameaction = ga_nothing; viewactive = true; + Net_SetWaiting(); } //========================================================================== diff --git a/src/g_statusbar/sbar.h b/src/g_statusbar/sbar.h index bffa1a823..5d3f9794d 100644 --- a/src/g_statusbar/sbar.h +++ b/src/g_statusbar/sbar.h @@ -413,6 +413,10 @@ public: return SBarTop; } void DoDrawAutomapHUD(int crdefault, int highlight); + void ClearInterpolation() + { + PrevCrosshairSize = CrosshairSize; + } //protected: void DrawPowerups (); diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 5890fd58f..d336808b1 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -1234,19 +1234,28 @@ void DBaseStatusBar::DrawTopStuff (EHudState state) } -void DBaseStatusBar::DrawConsistancy () const +void DBaseStatusBar::DrawConsistancy() const { if (!netgame) return; bool desync = false; FString text = "Out of sync with:"; - for (int i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (playeringame[i] && players[i].inconsistant) + if (players[client].inconsistant) { desync = true; - text.AppendFormat(" %s (%d)", players[i].userinfo.GetName(10u), i + 1); + // Fell out of sync with the host in packet server mode. Which specific user it is doesn't really matter. + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + { + text.AppendFormat(" %s (%d)", players[Net_Arbitrator].userinfo.GetName(10u), Net_Arbitrator + 1); + break; + } + else + { + text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client + 1); + } } } @@ -1265,19 +1274,19 @@ void DBaseStatusBar::DrawConsistancy () const } } -void DBaseStatusBar::DrawWaiting () const +void DBaseStatusBar::DrawWaiting() const { if (!netgame) return; FString text = "Waiting for:"; bool isWaiting = false; - for (int i = 0; i < MAXPLAYERS; i++) + for (auto client : NetworkClients) { - if (playeringame[i] && players[i].waiting) + if (players[client].waiting) { isWaiting = true; - text.AppendFormat(" %s (%d)", players[i].userinfo.GetName(10u), i + 1); + text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client + 1); } } diff --git a/src/hu_scores.cpp b/src/hu_scores.cpp index d7cde85fc..bb7999dda 100644 --- a/src/hu_scores.cpp +++ b/src/hu_scores.cpp @@ -146,10 +146,17 @@ static int FontScale; // //========================================================================== -void HU_DrawScores (player_t *player) +void HU_DrawScores(player_t* player) { displayFont = NewSmallFont; - FontScale = max(screen->GetHeight() / 400, 1); + FontScale = max(screen->GetHeight() / 400, 1); + + int numPlayers = 0; + for (int i = 0; i < MAXPLAYERS; ++i) + numPlayers += playeringame[i]; + + if (numPlayers > 8) + FontScale = static_cast(ceil(FontScale * 0.75)); if (deathmatch) { @@ -158,39 +165,34 @@ void HU_DrawScores (player_t *player) if (!sb_teamdeathmatch_enable) return; } - else + else if (!sb_deathmatch_enable) { - if (!sb_deathmatch_enable) - return; + return; } } - else + else if (!multiplayer || !sb_cooperative_enable) { - if (!sb_cooperative_enable || !multiplayer) - return; + return; } - int i, j; - player_t *sortedplayers[MAXPLAYERS]; - + player_t* sortedPlayers[MAXPLAYERS]; if (player->camera && player->camera->player) player = player->camera->player; - sortedplayers[MAXPLAYERS-1] = player; - for (i = 0, j = 0; j < MAXPLAYERS - 1; i++, j++) + sortedPlayers[MAXPLAYERS - 1] = player; + for (int i = 0, j = 0; j < MAXPLAYERS - 1; ++i, ++j) { if (&players[i] == player) - i++; - sortedplayers[j] = &players[i]; + ++i; + sortedPlayers[j] = &players[i]; } if (teamplay && deathmatch) - qsort (sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams); + qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), compareteams); else - qsort (sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints); - - HU_DoDrawScores (player, sortedplayers); + qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), comparepoints); + HU_DoDrawScores(player, sortedPlayers); } //========================================================================== @@ -201,39 +203,37 @@ void HU_DrawScores (player_t *player) // //========================================================================== -void HU_GetPlayerWidths(int &maxnamewidth, int &maxscorewidth, int &maxiconheight) +void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight) { - displayFont = NewSmallFont; - maxnamewidth = displayFont->StringWidth("Name"); - maxscorewidth = 0; - maxiconheight = 0; + constexpr char NameLabel[] = "Name"; - for (int i = 0; i < MAXPLAYERS; i++) + displayFont = NewSmallFont; + maxNameWidth = displayFont->StringWidth(NameLabel); + maxScoreWidth = 0; + maxIconHeight = 0; + + for (int i = 0; i < MAXPLAYERS; ++i) { - if (playeringame[i]) + if (!playeringame[i]) + continue; + + int width = displayFont->StringWidth(players[i].userinfo.GetName(16)); + if (width > maxNameWidth) + maxNameWidth = width; + + auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon)); + if (icon.isValid()) { - int width = displayFont->StringWidth(players[i].userinfo.GetName()); - if (width > maxnamewidth) - { - maxnamewidth = width; - } - auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon)); - if (icon.isValid()) - { - auto pic = TexMan.GetGameTexture(icon); - width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5); - if (width > maxscorewidth) - { - maxscorewidth = width; - } - // The icon's top offset does not count toward its height, because - // zdoom.pk3's standard Hexen class icons are designed that way. - int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5); - if (height > maxiconheight) - { - maxiconheight = height; - } - } + auto pic = TexMan.GetGameTexture(icon); + width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5); + if (width > maxScoreWidth) + maxScoreWidth = width; + + // The icon's top offset does not count toward its height, because + // zdoom.pk3's standard Hexen class icons are designed that way. + int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5); + if (height > maxIconHeight) + maxIconHeight = height; } } } @@ -249,16 +249,9 @@ static void HU_DrawFontScaled(double x, double y, int color, const char *text) DrawText(twod, displayFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, twod->GetWidth() / FontScale, DTA_VirtualHeight, twod->GetHeight() / FontScale, TAG_END); } -static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYERS]) +static void HU_DoDrawScores(player_t* player, player_t* sortedPlayers[MAXPLAYERS]) { - int color; - int height, lineheight; - unsigned int i; - int maxnamewidth, maxscorewidth, maxiconheight; - int numTeams = 0; - int x, y, ypadding, bottom; - int col2, col3, col4, col5; - + int color = sb_cooperative_headingcolor; if (deathmatch) { if (teamplay) @@ -266,86 +259,77 @@ static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYER else color = sb_deathmatch_headingcolor; } - else - { - color = sb_cooperative_headingcolor; - } - HU_GetPlayerWidths(maxnamewidth, maxscorewidth, maxiconheight); - height = displayFont->GetHeight() * FontScale; - lineheight = max(height, maxiconheight * CleanYfac); - ypadding = (lineheight - height + 1) / 2; + int maxNameWidth, maxScoreWidth, maxIconHeight; + HU_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); + int height = displayFont->GetHeight() * FontScale; + int lineHeight = max(height, maxIconHeight * CleanYfac); + int yPadding = (lineHeight - height + 1) / 2; - bottom = StatusBar->GetTopOfStatusbar(); - y = max(48*CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); + int bottom = StatusBar->GetTopOfStatusbar(); + int y = max(48 * CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); - HU_DrawTimeRemaining (bottom - height); + HU_DrawTimeRemaining(bottom - height); if (teamplay && deathmatch) { y -= (BigFont->GetHeight() + 8) * CleanYfac; - for (i = 0; i < Teams.Size (); i++) + for (size_t i = 0u; i < Teams.Size(); ++i) { Teams[i].m_iPlayerCount = 0; Teams[i].m_iScore = 0; } - for (i = 0; i < MAXPLAYERS; ++i) + int numTeams = 0; + for (int i = 0; i < MAXPLAYERS; ++i) { - if (playeringame[sortedplayers[i]-players] && FTeam::IsValid (sortedplayers[i]->userinfo.GetTeam())) + if (playeringame[sortedPlayers[i]-players] && FTeam::IsValid(sortedPlayers[i]->userinfo.GetTeam())) { - if (Teams[sortedplayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0) - { - numTeams++; - } + if (Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0) + ++numTeams; - Teams[sortedplayers[i]->userinfo.GetTeam()].m_iScore += sortedplayers[i]->fragcount; + Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iScore += sortedPlayers[i]->fragcount; } } - int scorexwidth = twod->GetWidth() / max(8, numTeams); - int numscores = 0; - int scorex; - - for (i = 0; i < Teams.Size(); ++i) + int scoreXWidth = twod->GetWidth() / max(8, numTeams); + int numScores = 0; + for (size_t i = 0u; i < Teams.Size(); ++i) { if (Teams[i].m_iPlayerCount) - { - numscores++; - } + ++numScores; } - scorex = (twod->GetWidth() - scorexwidth * (numscores - 1)) / 2; - - for (i = 0; i < Teams.Size(); ++i) + int scoreX = (twod->GetWidth() - scoreXWidth * (numScores - 1)) / 2; + for (size_t i = 0u; i < Teams.Size(); ++i) { - if (Teams[i].m_iPlayerCount) - { - char score[80]; - mysnprintf (score, countof(score), "%d", Teams[i].m_iScore); + if (!Teams[i].m_iPlayerCount) + continue; - DrawText(twod, BigFont, Teams[i].GetTextColor(), - scorex - BigFont->StringWidth(score)*CleanXfac/2, y, score, - DTA_CleanNoMove, true, TAG_DONE); + char score[80]; + mysnprintf(score, countof(score), "%d", Teams[i].m_iScore); - scorex += scorexwidth; - } + DrawText(twod, BigFont, Teams[i].GetTextColor(), + scoreX - BigFont->StringWidth(score)*CleanXfac/2, y, score, + DTA_CleanNoMove, true, TAG_DONE); + + scoreX += scoreXWidth; } y += (BigFont->GetHeight() + 8) * CleanYfac; } - const char *text_color = GStrings.GetString("SCORE_COLOR"), + const char* text_color = GStrings.GetString("SCORE_COLOR"), *text_frags = GStrings.GetString(deathmatch ? "SCORE_FRAGS" : "SCORE_KILLS"), *text_name = GStrings.GetString("SCORE_NAME"), *text_delay = GStrings.GetString("SCORE_DELAY"); - col2 = (displayFont->StringWidth(text_color) + 16) * FontScale; - col3 = col2 + (displayFont->StringWidth(text_frags) + 16) * FontScale; - col4 = col3 + maxscorewidth * FontScale; - col5 = col4 + (maxnamewidth + 16) * FontScale; - x = (twod->GetWidth() >> 1) - (((displayFont->StringWidth(text_delay) * FontScale) + col5) >> 1); + int col2 = (displayFont->StringWidth(text_color) + 16) * FontScale; + int col3 = col2 + (displayFont->StringWidth(text_frags) + 16) * FontScale; + int col4 = col3 + maxScoreWidth * FontScale; + int col5 = col4 + (maxNameWidth + 16) * FontScale; + int x = (twod->GetWidth() >> 1) - (((displayFont->StringWidth(text_delay) * FontScale) + col5) >> 1); //HU_DrawFontScaled(x, y, color, text_color); HU_DrawFontScaled(x + col2, y, color, text_frags); @@ -355,13 +339,13 @@ static void HU_DoDrawScores (player_t *player, player_t *sortedplayers[MAXPLAYER y += height + 6 * CleanYfac; bottom -= height; - for (i = 0; i < MAXPLAYERS && y <= bottom; i++) + for (int i = 0; i < MAXPLAYERS && y <= bottom; ++i) { - if (playeringame[sortedplayers[i] - players]) - { - HU_DrawPlayer(sortedplayers[i], player == sortedplayers[i], x, col2, col3, col4, col5, maxnamewidth, y, ypadding, lineheight); - y += lineheight + CleanYfac; - } + if (!playeringame[sortedPlayers[i] - players]) + continue; + + HU_DrawPlayer(sortedPlayers[i], player == sortedPlayers[i], x, col2, col3, col4, col5, maxNameWidth, y, yPadding, lineHeight); + y += lineHeight + CleanYfac; } } @@ -437,14 +421,7 @@ static void HU_DrawPlayer (player_t *player, bool highlight, int col1, int col2, HU_DrawFontScaled(col4, y + ypadding, color, player->userinfo.GetName()); - int avgdelay = 0; - for (int i = 0; i < BACKUPTICS; i++) - { - avgdelay += netdelay[nodeforplayer[(int)(player - players)]][i]; - } - avgdelay /= BACKUPTICS; - - mysnprintf(str, countof(str), "%d", (avgdelay * ticdup) * (1000 / TICRATE)); + mysnprintf(str, countof(str), "%u", ClientStates[player - players].AverageLatency); HU_DrawFontScaled(col5, y + ypadding, color, str); diff --git a/src/hu_stuff.h b/src/hu_stuff.h index 5c6fe4b02..01cf429cc 100644 --- a/src/hu_stuff.h +++ b/src/hu_stuff.h @@ -42,8 +42,8 @@ void CT_Drawer (void); // [RH] Draw deathmatch scores -void HU_DrawScores (player_t *me); -void HU_GetPlayerWidths(int &maxnamewidth, int &maxscorewidth, int &maxiconheight); +void HU_DrawScores(player_t* me); +void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight); void HU_DrawColorBar(int x, int y, int height, int playernum); int HU_GetRowColor(player_t *player, bool hightlight); diff --git a/src/p_tick.cpp b/src/p_tick.cpp index 59d66a285..efe35f412 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -72,6 +72,41 @@ bool P_CheckTickerPaused () return false; } +void P_ClearLevelInterpolation() +{ + for (auto Level : AllLevels()) + { + Level->interpolator.UpdateInterpolations(); + + auto it = Level->GetThinkerIterator(); + AActor* ac; + + while ((ac = it.Next())) + { + ac->ClearInterpolation(); + ac->ClearFOVInterpolation(); + } + } + + for (int i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i]) + { + DPSprite* pspr = players[i].psprites; + while (pspr) + { + pspr->ResetInterpolation(); + + pspr = pspr->Next; + } + } + } + + R_ClearInterpolationPath(); + if (StatusBar != nullptr) + StatusBar->ClearInterpolation(); +} + // // P_Ticker // diff --git a/src/playsim/bots/b_bot.h b/src/playsim/bots/b_bot.h index 2f214bbd9..9e9cadeb8 100644 --- a/src/playsim/bots/b_bot.h +++ b/src/playsim/bots/b_bot.h @@ -130,12 +130,12 @@ public: void FinishTravel (); bool IsLeader (player_t *player); void SetBodyAt (FLevelLocals *Level, const DVector3 &pos, int hostnum); - double FakeFire (AActor *source, AActor *dest, ticcmd_t *cmd); + double FakeFire (AActor *source, AActor *dest, usercmd_t *cmd); bool SafeCheckPosition (AActor *actor, double x, double y, FCheckPosition &tm); void BotTick(AActor *mo); //(b_move.cpp) - bool CleanAhead (AActor *thing, double x, double y, ticcmd_t *cmd); + bool CleanAhead (AActor *thing, double x, double y, usercmd_t *cmd); bool IsDangerous (sector_t *sec); TArray getspawned; //Array of bots (their names) which should be spawned when starting a game. @@ -216,21 +216,21 @@ public: private: //(b_think.cpp) void Think (); - void ThinkForMove (ticcmd_t *cmd); + void ThinkForMove (usercmd_t *cmd); void Set_enemy (); //(b_func.cpp) bool Reachable (AActor *target); - void Dofire (ticcmd_t *cmd); + void Dofire (usercmd_t *cmd); AActor *Choose_Mate (); AActor *Find_enemy (); - DAngle FireRox (AActor *enemy, ticcmd_t *cmd); + DAngle FireRox (AActor *enemy, usercmd_t *cmd); //(b_move.cpp) - void Roam (ticcmd_t *cmd); - bool Move (ticcmd_t *cmd); - bool TryWalk (ticcmd_t *cmd); - void NewChaseDir (ticcmd_t *cmd); + void Roam (usercmd_t *cmd); + bool Move (usercmd_t *cmd); + bool TryWalk (usercmd_t *cmd); + void NewChaseDir (usercmd_t *cmd); void TurnToAng (); void Pitch (AActor *target); }; diff --git a/src/playsim/bots/b_func.cpp b/src/playsim/bots/b_func.cpp index e537bf9f7..711553fdb 100644 --- a/src/playsim/bots/b_func.cpp +++ b/src/playsim/bots/b_func.cpp @@ -164,7 +164,7 @@ bool DBot::Check_LOS (AActor *to, DAngle vangle) //------------------------------------- //The bot will check if it's time to fire //and do so if that is the case. -void DBot::Dofire (ticcmd_t *cmd) +void DBot::Dofire (usercmd_t *cmd) { bool no_fire; //used to prevent bot from pumping rockets into nearby walls. int aiming_penalty=0; //For shooting at shading target, if screen is red, MAKEME: When screen red. @@ -276,7 +276,7 @@ void DBot::Dofire (ticcmd_t *cmd) } if (!no_fire) //If going to fire weapon { - cmd->ucmd.buttons |= BT_ATTACK; + cmd->buttons |= BT_ATTACK; } //Prevents bot from jerking, when firing automatic things with low skill. } @@ -503,7 +503,7 @@ void FCajunMaster::SetBodyAt (FLevelLocals *Level, const DVector3 &pos, int host //Emulates missile travel. Returns distance travelled. -double FCajunMaster::FakeFire (AActor *source, AActor *dest, ticcmd_t *cmd) +double FCajunMaster::FakeFire (AActor *source, AActor *dest, usercmd_t *cmd) { AActor *th = Spawn (source->Level, "CajunTrace", source->PosPlusZ(4*8.), NO_REPLACE); @@ -525,7 +525,7 @@ double FCajunMaster::FakeFire (AActor *source, AActor *dest, ticcmd_t *cmd) return dist; } -DAngle DBot::FireRox (AActor *enemy, ticcmd_t *cmd) +DAngle DBot::FireRox (AActor *enemy, usercmd_t *cmd) { double dist; AActor *actor; diff --git a/src/playsim/bots/b_move.cpp b/src/playsim/bots/b_move.cpp index 4d91839d8..87c21a5ae 100644 --- a/src/playsim/bots/b_move.cpp +++ b/src/playsim/bots/b_move.cpp @@ -63,7 +63,7 @@ extern dirtype_t diags[4]; //Called while the bot moves after its dest mobj //which can be a weapon/enemy/item whatever. -void DBot::Roam (ticcmd_t *cmd) +void DBot::Roam (usercmd_t *cmd) { if (Reachable(dest)) @@ -89,7 +89,7 @@ void DBot::Roam (ticcmd_t *cmd) } } -bool DBot::Move (ticcmd_t *cmd) +bool DBot::Move (usercmd_t *cmd) { double tryx, tryy; bool try_ok; @@ -136,20 +136,20 @@ bool DBot::Move (ticcmd_t *cmd) } if (good && ((pr_botopendoor() >= 203) ^ (good & 1))) { - cmd->ucmd.buttons |= BT_USE; - cmd->ucmd.forwardmove = FORWARDRUN; + cmd->buttons |= BT_USE; + cmd->forwardmove = FORWARDRUN; return true; } else return false; } else //Move forward. - cmd->ucmd.forwardmove = FORWARDRUN; + cmd->forwardmove = FORWARDRUN; return true; } -bool DBot::TryWalk (ticcmd_t *cmd) +bool DBot::TryWalk (usercmd_t *cmd) { if (!Move (cmd)) return false; @@ -158,7 +158,7 @@ bool DBot::TryWalk (ticcmd_t *cmd) return true; } -void DBot::NewChaseDir (ticcmd_t *cmd) +void DBot::NewChaseDir (usercmd_t *cmd) { dirtype_t d[3]; @@ -290,7 +290,7 @@ void DBot::NewChaseDir (ticcmd_t *cmd) // This is also a traverse function for // bots pre-rocket fire (preventing suicide) // -bool FCajunMaster::CleanAhead (AActor *thing, double x, double y, ticcmd_t *cmd) +bool FCajunMaster::CleanAhead (AActor *thing, double x, double y, usercmd_t *cmd) { FCheckPosition tm; @@ -310,7 +310,7 @@ bool FCajunMaster::CleanAhead (AActor *thing, double x, double y, ticcmd_t *cmd) //Jumpable if(tm.floorz > (thing->Sector->floorplane.ZatPoint(x, y)+thing->MaxStepHeight)) - cmd->ucmd.buttons |= BT_JUMP; + cmd->buttons |= BT_JUMP; if ( !(thing->flags & MF_TELEPORT) && diff --git a/src/playsim/bots/b_think.cpp b/src/playsim/bots/b_think.cpp index 9fba192d5..405d2719b 100644 --- a/src/playsim/bots/b_think.cpp +++ b/src/playsim/bots/b_think.cpp @@ -58,7 +58,7 @@ static FRandom pr_botmove ("BotMove"); //so this is what the bot does. void DBot::Think () { - ticcmd_t *cmd = &netcmds[player - players][((gametic + 1)/ticdup)%BACKUPTICS]; + usercmd_t *cmd = &player->cmd; memset (cmd, 0, sizeof(*cmd)); @@ -78,13 +78,12 @@ void DBot::Think () ThinkForMove (cmd); TurnToAng (); - cmd->ucmd.yaw = (short)((actor->Angles.Yaw - oldyaw).Degrees() * (65536 / 360.f)) / ticdup; - cmd->ucmd.pitch = (short)((oldpitch - actor->Angles.Pitch).Degrees() * (65536 / 360.f)); - if (cmd->ucmd.pitch == -32768) - cmd->ucmd.pitch = -32767; - cmd->ucmd.pitch /= ticdup; - actor->Angles.Yaw = oldyaw + DAngle::fromDeg(cmd->ucmd.yaw * ticdup * (360 / 65536.f)); - actor->Angles.Pitch = oldpitch - DAngle::fromDeg(cmd->ucmd.pitch * ticdup * (360 / 65536.f)); + cmd->yaw = (short)((actor->Angles.Yaw - oldyaw).Degrees() * (65536 / 360.f)); + cmd->pitch = (short)((oldpitch - actor->Angles.Pitch).Degrees() * (65536 / 360.f)); + if (cmd->pitch == -32768) + cmd->pitch = -32767; + actor->Angles.Yaw = oldyaw + DAngle::fromDeg(cmd->yaw * (360 / 65536.f)); + actor->Angles.Pitch = oldpitch - DAngle::fromDeg(cmd->pitch * (360 / 65536.f)); } if (t_active) t_active--; @@ -101,14 +100,14 @@ void DBot::Think () } else if (player->mo->health <= 0) { // Time to respawn - cmd->ucmd.buttons |= BT_USE; + cmd->buttons |= BT_USE; } } #define THINKDISTSQ (50000.*50000./(65536.*65536.)) //how the bot moves. //MAIN movement function. -void DBot::ThinkForMove (ticcmd_t *cmd) +void DBot::ThinkForMove (usercmd_t *cmd) { double dist; bool stuck; @@ -136,8 +135,8 @@ void DBot::ThinkForMove (ticcmd_t *cmd) { Pitch (missile); Angle = player->mo->AngleTo(missile); - cmd->ucmd.sidemove = sleft ? -SIDERUN : SIDERUN; - cmd->ucmd.forwardmove = -FORWARDRUN; //Back IS best. + cmd->sidemove = sleft ? -SIDERUN : SIDERUN; + cmd->forwardmove = -FORWARDRUN; //Back IS best. if ((player->mo->Pos() - old).LengthSquared() < THINKDISTSQ && t_strafe<=0) @@ -208,22 +207,22 @@ void DBot::ThinkForMove (ticcmd_t *cmd) GetBotInfo(player->ReadyWeapon).MoveCombatDist) { // If a monster, use lower speed (just for cooler apperance while strafing down doomed monster) - cmd->ucmd.forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? FORWARDWALK : FORWARDRUN; + cmd->forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? FORWARDWALK : FORWARDRUN; } else if (!stuck) //Too close, so move away. { // If a monster, use lower speed (just for cooler apperance while strafing down doomed monster) - cmd->ucmd.forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? -FORWARDWALK : -FORWARDRUN; + cmd->forwardmove = (enemy->flags3 & MF3_ISMONSTER) ? -FORWARDWALK : -FORWARDRUN; } //Strafing. if (enemy->flags3 & MF3_ISMONSTER) //It's just a monster so take it down cool. { - cmd->ucmd.sidemove = sleft ? -SIDEWALK : SIDEWALK; + cmd->sidemove = sleft ? -SIDEWALK : SIDEWALK; } else { - cmd->ucmd.sidemove = sleft ? -SIDERUN : SIDERUN; + cmd->sidemove = sleft ? -SIDERUN : SIDERUN; } Dofire (cmd); //Order bot to fire current weapon } @@ -246,11 +245,11 @@ void DBot::ThinkForMove (ticcmd_t *cmd) matedist = player->mo->Distance2D(mate); if (matedist > (FRIEND_DIST*2)) - cmd->ucmd.forwardmove = FORWARDRUN; + cmd->forwardmove = FORWARDRUN; else if (matedist > FRIEND_DIST) - cmd->ucmd.forwardmove = FORWARDWALK; //Walk, when starting to get close. + cmd->forwardmove = FORWARDWALK; //Walk, when starting to get close. else if (matedist < FRIEND_DIST-(FRIEND_DIST/3)) //Got too close, so move away. - cmd->ucmd.forwardmove = -FORWARDWALK; + cmd->forwardmove = -FORWARDWALK; } else //Roam after something. { diff --git a/src/playsim/d_player.h b/src/playsim/d_player.h index 862890f07..025355d95 100644 --- a/src/playsim/d_player.h +++ b/src/playsim/d_player.h @@ -321,7 +321,7 @@ public: AActor *mo = nullptr; uint8_t playerstate = 0; - ticcmd_t cmd = {}; + usercmd_t cmd = {}; usercmd_t original_cmd = {}; uint32_t original_oldbuttons = 0; diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 6eb98c0ba..216d13db7 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -2572,7 +2572,7 @@ static double P_XYMovement (AActor *mo, DVector2 scroll) { // slide against wall if (BlockingLine != NULL && mo->player && mo->waterlevel && mo->waterlevel < 3 && - (mo->player->cmd.ucmd.forwardmove | mo->player->cmd.ucmd.sidemove) && + (mo->player->cmd.forwardmove | mo->player->cmd.sidemove) && mo->BlockingLine->sidedef[1] != NULL) { double spd = mo->FloatVar(NAME_WaterClimbSpeed); @@ -2811,7 +2811,7 @@ static double P_XYMovement (AActor *mo, DVector2 scroll) // moving corresponding player: if (fabs(mo->Vel.X) < STOPSPEED && fabs(mo->Vel.Y) < STOPSPEED && (!player || (player->mo != mo) - || !(player->cmd.ucmd.forwardmove | player->cmd.ucmd.sidemove))) + || !(player->cmd.forwardmove | player->cmd.sidemove))) { // if in a walking frame, stop moving // killough 10/98: @@ -3273,7 +3273,7 @@ void AActor::FallAndSink(double grav, double oldfloorz) double startvelz = Vel.Z; if (waterlevel == 0 || (player && - !(player->cmd.ucmd.forwardmove | player->cmd.ucmd.sidemove))) + !(player->cmd.forwardmove | player->cmd.sidemove))) { // [RH] Double gravity only if running off a ledge. Coming down from // an upward thrust (e.g. a jump) should not double it. @@ -6101,7 +6101,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag p->cheats = CF_CHASECAM; // setup gun psprite - if (!(flags & SPF_TEMPPLAYER)) + if (!(flags & SPF_TEMPPLAYER) || oldactor == nullptr) { // This can also start a script so don't do it for the dummy player. P_SetupPsprites (p, !!(flags & SPF_WEAPONFULLYUP)); } @@ -6156,7 +6156,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag } // [BC] Do script stuff - if (!(flags & SPF_TEMPPLAYER)) + if (!(flags & SPF_TEMPPLAYER) || oldactor == nullptr) { if (state == PST_ENTER || (state == PST_LIVE && !savegamerestore)) { diff --git a/src/playsim/p_pspr.cpp b/src/playsim/p_pspr.cpp index 923923420..df09b58c3 100644 --- a/src/playsim/p_pspr.cpp +++ b/src/playsim/p_pspr.cpp @@ -715,7 +715,7 @@ static void P_CheckWeaponButtons (player_t *player) for (size_t i = 0; i < countof(ButtonChecks); ++i) { if ((player->WeaponState & ButtonChecks[i].StateFlag) && - (player->cmd.ucmd.buttons & ButtonChecks[i].ButtonFlag)) + (player->cmd.buttons & ButtonChecks[i].ButtonFlag)) { FState *state = weapon->FindState(ButtonChecks[i].StateName); // [XA] don't change state if still null, so if the modder diff --git a/src/playsim/p_things.cpp b/src/playsim/p_things.cpp index 3391030aa..462c758c2 100644 --- a/src/playsim/p_things.cpp +++ b/src/playsim/p_things.cpp @@ -561,13 +561,13 @@ int P_Thing_CheckInputNum(player_t *p, int inputnum) case INPUT_UPMOVE: renum = p->original_cmd.upmove; break; case MODINPUT_OLDBUTTONS: renum = p->oldbuttons; break; - case MODINPUT_BUTTONS: renum = p->cmd.ucmd.buttons; break; - case MODINPUT_PITCH: renum = p->cmd.ucmd.pitch; break; - case MODINPUT_YAW: renum = p->cmd.ucmd.yaw; break; - case MODINPUT_ROLL: renum = p->cmd.ucmd.roll; break; - case MODINPUT_FORWARDMOVE: renum = p->cmd.ucmd.forwardmove; break; - case MODINPUT_SIDEMOVE: renum = p->cmd.ucmd.sidemove; break; - case MODINPUT_UPMOVE: renum = p->cmd.ucmd.upmove; break; + case MODINPUT_BUTTONS: renum = p->cmd.buttons; break; + case MODINPUT_PITCH: renum = p->cmd.pitch; break; + case MODINPUT_YAW: renum = p->cmd.yaw; break; + case MODINPUT_ROLL: renum = p->cmd.roll; break; + case MODINPUT_FORWARDMOVE: renum = p->cmd.forwardmove; break; + case MODINPUT_SIDEMOVE: renum = p->cmd.sidemove; break; + case MODINPUT_UPMOVE: renum = p->cmd.upmove; break; default: renum = 0; break; } diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 65f892903..61fd17cee 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -95,6 +95,8 @@ #include "s_music.h" #include "d_main.h" +extern int paused; + static FRandom pr_skullpop ("SkullPop"); // [SP] Allows respawn in single player @@ -1236,7 +1238,7 @@ DEFINE_ACTION_FUNCTION(APlayerPawn, CheckEnvironment) void P_CheckUse(player_t *player) { // check for use - if (player->cmd.ucmd.buttons & BT_USE) + if (player->cmd.buttons & BT_USE) { if (!player->usedown) { @@ -1269,7 +1271,7 @@ DEFINE_ACTION_FUNCTION(APlayerPawn, CheckUse) void P_PlayerThink (player_t *player) { - ticcmd_t *cmd = &player->cmd; + usercmd_t *cmd = &player->cmd; if (player->mo == NULL) { @@ -1309,14 +1311,14 @@ void P_PlayerThink (player_t *player) { fprintf (debugfile, "tic %d for pl %d: (%f, %f, %f, %f) b:%02x p:%d y:%d f:%d s:%d u:%d\n", gametic, (int)(player-players), player->mo->X(), player->mo->Y(), player->mo->Z(), - player->mo->Angles.Yaw.Degrees(), player->cmd.ucmd.buttons, - player->cmd.ucmd.pitch, player->cmd.ucmd.yaw, player->cmd.ucmd.forwardmove, - player->cmd.ucmd.sidemove, player->cmd.ucmd.upmove); + player->mo->Angles.Yaw.Degrees(), player->cmd.buttons, + player->cmd.pitch, player->cmd.yaw, player->cmd.forwardmove, + player->cmd.sidemove, player->cmd.upmove); } // Make unmodified copies for ACS's GetPlayerInput. player->original_oldbuttons = player->original_cmd.buttons; - player->original_cmd = cmd->ucmd; + player->original_cmd = *cmd; // Don't interpolate the view for more than one tic player->cheats &= ~CF_INTERPVIEW; player->cheats &= ~CF_INTERPVIEWANGLES; @@ -1453,8 +1455,7 @@ void P_PredictPlayer (player_t *player) { int maxtic; - if (singletics || - demoplayback || + if (demoplayback || player->mo == NULL || player != player->mo->Level->GetConsolePlayer() || player->playerstate != PST_LIVE || @@ -1465,7 +1466,7 @@ void P_PredictPlayer (player_t *player) return; } - maxtic = maketic; + maxtic = ClientTic; if (gametic == maxtic) { @@ -1557,8 +1558,11 @@ void P_PredictPlayer (player_t *player) } } - player->oldbuttons = player->cmd.ucmd.buttons; - player->cmd = localcmds[i % LOCALCMDTICS]; + player->oldbuttons = player->cmd.buttons; + player->cmd = LocalCmds[i % LOCALCMDTICS]; + if (paused) + continue; + player->mo->ClearInterpolation(); player->mo->ClearFOVInterpolation(); P_PlayerThink(player); @@ -1591,6 +1595,10 @@ void P_PredictPlayer (player_t *player) player->viewz = snapPos.Z + zOfs; } } + else if (paused) + { + r_NoInterpolate = true; + } // This is intentionally done after rubberbanding starts since it'll automatically smooth itself towards // the right spot until it reaches it. @@ -1900,11 +1908,11 @@ DEFINE_FIELD_X(PlayerInfo, player_t, ConversationNPC) DEFINE_FIELD_X(PlayerInfo, player_t, ConversationPC) DEFINE_FIELD_X(PlayerInfo, player_t, ConversationNPCAngle) DEFINE_FIELD_X(PlayerInfo, player_t, ConversationFaceTalker) -DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd.ucmd, cmd) +DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd, cmd) DEFINE_FIELD_X(PlayerInfo, player_t, original_cmd) DEFINE_FIELD_X(PlayerInfo, player_t, userinfo) DEFINE_FIELD_X(PlayerInfo, player_t, weapons) -DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd.ucmd.buttons, buttons) +DEFINE_FIELD_NAMED_X(PlayerInfo, player_t, cmd.buttons, buttons) DEFINE_FIELD_X(PlayerInfo, player_t, SoundClass) DEFINE_FIELD_X(UserCmd, usercmd_t, buttons) diff --git a/src/serializer_doom.h b/src/serializer_doom.h index 1ce88c0e0..bc87df74a 100644 --- a/src/serializer_doom.h +++ b/src/serializer_doom.h @@ -8,7 +8,6 @@ struct sector_t; struct line_t; struct side_t; struct vertex_t; -struct ticcmd_t; struct usercmd_t; class PClassActor; struct FStrifeDialogueNode; @@ -40,7 +39,6 @@ FSerializer &SerializeArgs(FSerializer &arc, const char *key, int *args, int *de FSerializer &SerializeTerrain(FSerializer &arc, const char *key, int &terrain, int *def = nullptr); FSerializer& Serialize(FSerializer& arc, const char* key, char& value, char* defval); -FSerializer &Serialize(FSerializer &arc, const char *key, ticcmd_t &sid, ticcmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, usercmd_t &cmd, usercmd_t *def); FSerializer &Serialize(FSerializer &arc, const char *key, FInterpolator &rs, FInterpolator *def); FSerializer& Serialize(FSerializer& arc, const char* key, struct FStandaloneAnimation& value, struct FStandaloneAnimation* defval); diff --git a/src/version.h b/src/version.h index 7a04c37dc..91f3319b5 100644 --- a/src/version.h +++ b/src/version.h @@ -57,11 +57,6 @@ const char *GetVersionString(); #define ENG_MINOR 15 #define ENG_REVISION 1 -// Version identifier for network games. -// Bump it every time you do a release unless you're certain you -// didn't change anything that will affect sync. -#define NETGAMEVERSION 235 - // Version stored in the ini's [LastRun] section. // Bump it if you made some configuration change that you want to // be able to migrate in FGameConfigFile::DoGlobalSetup(). diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 759cc900d..05a83d98d 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -654,6 +654,8 @@ OptionMenu "OtherControlsMenu" protected StaticText "" Control "$CNTRLMNU_CHASECAM" , "chase" Control "$CNTRLMNU_COOPSPY" , "spynext" + Control "$CNTRLMNU_COOPSPYPREV" , "spyprev" + Control "$CNTRLMNU_COOPSPYCANCEL" , "spycancel" StaticText "" Control "$CNTRLMNU_SCREENSHOT" , "screenshot" @@ -2378,21 +2380,25 @@ OptionMenu NetworkOptions protected { Title "$NETMNU_TITLE" StaticText "$NETMNU_LOCALOPTIONS", 1 + Option "$NETMNU_CHATTYPE", "cl_showchat", "ChatTypes" + Option "$NETMNU_NOBOLDCHAT", "cl_noboldchat", "OnOff" + Option "$NETMNU_NOCHATSND", "cl_nochatsound", "OnOff" Option "$NETMNU_LINESPECIALPREDICTION", "cl_predict_specials", "OnOff" Slider "$NETMNU_PREDICTIONLERPSCALE", "cl_rubberband_scale", 0.0, 1.0, 0.05, 2 Slider "$NETMNU_LERPTHRESHOLD", "cl_rubberband_threshold", 0.0, 256.0, 8.0 StaticText " " StaticText "$NETMNU_HOSTOPTIONS", 1 - Option "$NETMNU_EXTRATICS", "net_extratic", "ExtraTicMode" - Option "$NETMNU_TICBALANCE", "net_ticbalance", "OnOff" + Option "$NETMNU_EXTRATICS", "net_extratic", "OnOff" + Option "$NETMNU_DISABLEPAUSE", "net_disablepause", "OnOff" + NumberField "$NETMNU_CHATSLOMO", "net_chatslowmode", 0, 300, 5 } -OptionValue ExtraTicMode +OptionValue "ChatTypes" { - 0, "$OPTVAL_NONE" - 1, "1" - 2, "$OPTVAL_ALLUNACKNOWLEDGED" + 0, "$OPTVAL_DISABLECHAT" + 1, "$OPTVAL_TEAMONLYCHAT" + 2, "$OPTVAL_GLOBALCHAT" } OptionValue "LookupOrder" diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index 22454dc65..c91410b0b 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -18,6 +18,7 @@ class PlayerPawn : Actor // 16 pixels of bob const MAXBOB = 16.; + int BobTimer; // Use a local timer for this so it can be predicted correctly. int crouchsprite; int MaxHealth; int BonusHealth; @@ -633,7 +634,7 @@ class PlayerPawn : Actor { if (player.health > 0) { - angle = Level.maptime / (120 * TICRATE / 35.) * 360.; + angle = BobTimer / (120 * TICRATE / 35.) * 360.; bob = player.GetStillBob() * sin(angle); } else @@ -643,7 +644,7 @@ class PlayerPawn : Actor } else { - angle = Level.maptime / (ViewBobSpeed * TICRATE / 35.) * 360.; + angle = BobTimer / (ViewBobSpeed * TICRATE / 35.) * 360.; bob = player.bob * sin(angle) * (waterlevel > 1 ? 0.25f : 0.5f); } @@ -1667,6 +1668,7 @@ class PlayerPawn : Actor PlayerFlags |= PF_VOODOO_ZOMBIE; } + ++BobTimer; CheckFOV(); if (player.inventorytics) @@ -2545,7 +2547,7 @@ class PlayerPawn : Actor for (int i = 0; i < 2; i++) { // Bob the weapon based on movement speed. ([SP] And user's bob speed setting) - double angle = (BobSpeed * player.GetWBobSpeed() * 35 / TICRATE*(Level.maptime - 1 + i)) * (360. / 8192.); + double angle = (BobSpeed * player.GetWBobSpeed() * 35 / TICRATE*(BobTimer - 1 + i)) * (360. / 8192.); // [RH] Smooth transitions between bobbing and not-bobbing frames. // This also fixes the bug where you can "stick" a weapon off-center by diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index cd3eceb15..c73ed76ab 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1,6 +1,6 @@ // for flag changer functions. const FLAG_NO_CHANGE = -1; -const MAXPLAYERS = 8; +const MAXPLAYERS = 16; enum EStateUseFlags { From 232b93534de2f0396bdf8e29bc0618196e17ad7e Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sun, 19 Jan 2025 17:19:54 -0700 Subject: [PATCH 040/384] Better flat visibility checks for Ortho projection. --- src/rendering/hwrenderer/scene/hw_flats.cpp | 12 ++++++------ src/rendering/r_utility.cpp | 3 +++ src/rendering/r_utility.h | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_flats.cpp b/src/rendering/hwrenderer/scene/hw_flats.cpp index f423c73fc..f14757ade 100644 --- a/src/rendering/hwrenderer/scene/hw_flats.cpp +++ b/src/rendering/hwrenderer/scene/hw_flats.cpp @@ -518,7 +518,7 @@ void HWFlat::ProcessSector(HWDrawInfo *di, sector_t * frontsector, int which) // // // - if (((which & SSRF_RENDERFLOOR) && frontsector->floorplane.ZatPoint(vp.Pos) <= vp.Pos.Z && (!section || !(section->flags & FSection::DONTRENDERFLOOR)))&& !(vp.IsOrtho() && (vp.PitchSin < 0.0))) + if ((which & SSRF_RENDERFLOOR) && (vp.IsOrtho() ? vp.ViewVector3D.dot(frontsector->floorplane.Normal()) < 0.0 : frontsector->floorplane.ZatPoint(vp.Pos) <= vp.Pos.Z) && (!section || !(section->flags & FSection::DONTRENDERFLOOR))) { // process the original floor first. @@ -576,7 +576,7 @@ void HWFlat::ProcessSector(HWDrawInfo *di, sector_t * frontsector, int which) // // // - if (((which & SSRF_RENDERCEILING) && frontsector->ceilingplane.ZatPoint(vp.Pos) >= vp.Pos.Z && (!section || !(section->flags & FSection::DONTRENDERCEILING))) && !(vp.IsOrtho() && (vp.PitchSin > 0.0))) + if ((which & SSRF_RENDERCEILING) && (vp.IsOrtho() ? vp.ViewVector3D.dot(frontsector->ceilingplane.Normal()) < 0.0 : frontsector->ceilingplane.ZatPoint(vp.Pos) >= vp.Pos.Z) && (!section || !(section->flags & FSection::DONTRENDERCEILING))) { // process the original ceiling first. @@ -661,7 +661,7 @@ void HWFlat::ProcessSector(HWDrawInfo *di, sector_t * frontsector, int which) double ff_top = rover->top.plane->ZatPoint(sector->centerspot); if (ff_top < lastceilingheight) { - if (vp.Pos.Z <= rover->top.plane->ZatPoint(vp.Pos)) + if ((vp.IsOrtho() ? vp.ViewVector3D.dot(rover->top.plane->Normal()) > 0.0 : vp.Pos.Z <= rover->top.plane->ZatPoint(vp.Pos))) { SetFrom3DFloor(rover, true, !!(rover->flags&FF_FOG)); Colormap.FadeColor = frontsector->Colormap.FadeColor; @@ -675,7 +675,7 @@ void HWFlat::ProcessSector(HWDrawInfo *di, sector_t * frontsector, int which) double ff_bottom = rover->bottom.plane->ZatPoint(sector->centerspot); if (ff_bottom < lastceilingheight) { - if (vp.Pos.Z <= rover->bottom.plane->ZatPoint(vp.Pos)) + if ((vp.IsOrtho() ? vp.ViewVector3D.dot(rover->bottom.plane->Normal()) > 0.0 : vp.Pos.Z <= rover->bottom.plane->ZatPoint(vp.Pos))) { SetFrom3DFloor(rover, false, !(rover->flags&FF_FOG)); Colormap.FadeColor = frontsector->Colormap.FadeColor; @@ -701,7 +701,7 @@ void HWFlat::ProcessSector(HWDrawInfo *di, sector_t * frontsector, int which) double ff_bottom = rover->bottom.plane->ZatPoint(sector->centerspot); if (ff_bottom > lastfloorheight || (rover->flags&FF_FIX)) { - if (vp.Pos.Z >= rover->bottom.plane->ZatPoint(vp.Pos)) + if ((vp.IsOrtho() ? vp.ViewVector3D.dot(rover->bottom.plane->Normal()) > 0.0 : vp.Pos.Z >= rover->bottom.plane->ZatPoint(vp.Pos))) { SetFrom3DFloor(rover, false, !(rover->flags&FF_FOG)); Colormap.FadeColor = frontsector->Colormap.FadeColor; @@ -722,7 +722,7 @@ void HWFlat::ProcessSector(HWDrawInfo *di, sector_t * frontsector, int which) double ff_top = rover->top.plane->ZatPoint(sector->centerspot); if (ff_top > lastfloorheight) { - if (vp.Pos.Z >= rover->top.plane->ZatPoint(vp.Pos)) + if ((vp.IsOrtho() ? vp.ViewVector3D.dot(rover->top.plane->Normal()) > 0.0 : vp.Pos.Z >= rover->top.plane->ZatPoint(vp.Pos))) { SetFrom3DFloor(rover, true, !!(rover->flags&FF_FOG)); Colormap.FadeColor = frontsector->Colormap.FadeColor; diff --git a/src/rendering/r_utility.cpp b/src/rendering/r_utility.cpp index 3a3fdd70e..cd0c3d0c2 100644 --- a/src/rendering/r_utility.cpp +++ b/src/rendering/r_utility.cpp @@ -704,6 +704,9 @@ void FRenderViewpoint::SetViewAngle(const FViewWindow& viewWindow) ViewVector.X = v.X; ViewVector.Y = v.Y; HWAngles.Yaw = FAngle::fromDeg(270.0 - Angles.Yaw.Degrees()); + ViewVector3D.X = v.X * PitchCos; + ViewVector3D.Y = v.Y * PitchCos; + ViewVector3D.Z = -PitchSin; if (IsOrtho() && (camera->ViewPos->Offset.XY().Length() > 0.0)) { diff --git a/src/rendering/r_utility.h b/src/rendering/r_utility.h index f7e4a1e1d..d82b03e68 100644 --- a/src/rendering/r_utility.h +++ b/src/rendering/r_utility.h @@ -25,6 +25,7 @@ struct FRenderViewpoint DRotator Angles; // Camera angles FRotator HWAngles; // Actual rotation angles for the hardware renderer DVector2 ViewVector; // HWR only: direction the camera is facing. + DVector3 ViewVector3D; // 3D direction the camera is facing. AActor *ViewActor; // either the same as camera or nullptr FLevelLocals *ViewLevel; // The level this viewpoint is on. From 2223ea6227256183f5f739aaca312cb435008aeb Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Thu, 30 Jan 2025 22:10:45 -0700 Subject: [PATCH 041/384] 3D floor flats now respect r_dithertransparency flag (how did this make it into vkdoom but not gzdoom?) --- .../hwrenderer/scene/hw_drawinfo.cpp | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 3453b6e50..4068fbbb8 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -698,10 +698,50 @@ static ETraceStatus TraceCallbackForDitherTransparency(FTraceResults& res, void* } break; case TRACE_HitFloor: - res.Sector->floorplane.dithertransflag = true; + if (res.HitPos.Z == res.Sector->floorplane.ZatPoint(res.HitPos)) + { + res.Sector->floorplane.dithertransflag = true; + } + else if (res.Sector->e->XFloor.ffloors.Size()) // Maybe it was 3D floors + { + F3DFloor *rover; + int kk; + for (kk = 0; kk < (int)res.Sector->e->XFloor.ffloors.Size(); kk++) + { + rover = res.Sector->e->XFloor.ffloors[kk]; + if ((rover->flags&(FF_EXISTS | FF_RENDERPLANES | FF_THISINSIDE)) == (FF_EXISTS | FF_RENDERPLANES)) + { + if (res.HitPos.Z == rover->top.plane->ZatPoint(res.HitPos)) + { + rover->top.plane->dithertransflag = true; + break; // Out of for loop + } + } + } + } break; case TRACE_HitCeiling: - res.Sector->ceilingplane.dithertransflag = true; + if (res.HitPos.Z == res.Sector->ceilingplane.ZatPoint(res.HitPos)) + { + res.Sector->ceilingplane.dithertransflag = true; + } + else if (res.Sector->e->XFloor.ffloors.Size()) // Maybe it was 3D floors + { + F3DFloor *rover; + int kk; + for (kk = 0; kk < (int)res.Sector->e->XFloor.ffloors.Size(); kk++) + { + rover = res.Sector->e->XFloor.ffloors[kk]; + if ((rover->flags&(FF_EXISTS | FF_RENDERPLANES | FF_THISINSIDE)) == (FF_EXISTS | FF_RENDERPLANES)) + { + if (res.HitPos.Z == rover->bottom.plane->ZatPoint(res.HitPos)) + { + rover->bottom.plane->dithertransflag = true; + break; // Out of for loop + } + } + } + } break; case TRACE_HitActor: default: From 42947d04dcdc32094f89ab8f90c0359db1f997a1 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Tue, 11 Feb 2025 19:51:21 -0700 Subject: [PATCH 042/384] Added visual rendering for LinePortals and SkyPortals for OoB viewpoints. SkyPortals will be stenciled, and will always use perspective projection. Disabled interpolation when portalgroup changes (portal transition occurs) if viewpoint is OoB (was necessary for fog of war when r_radarclipper is set to true). Tightened up radar clipper by making it more aggressive. Voided walls wont' get filled in by a floor or ceiling sky (because of the stencil). Ceiling sky will be half-infinitely tall upwards, and floor sky will be half-infinitely tall downwards. Use only floor skies and a good GLSKYBOX for top-down/isometric cameras. Level.ReplaceTextures("F_SKY1", "SKY1", TexMan.NOT_FLOOR); (zscript) is a nice trick for WorldLoaded(). --- src/rendering/hwrenderer/hw_entrypoint.cpp | 1 + src/rendering/hwrenderer/scene/hw_bsp.cpp | 75 ++++++----------- src/rendering/hwrenderer/scene/hw_clipper.cpp | 18 ++--- .../hwrenderer/scene/hw_drawinfo.cpp | 81 ++++++++++--------- src/rendering/hwrenderer/scene/hw_drawinfo.h | 1 + src/rendering/hwrenderer/scene/hw_portal.cpp | 24 ++++-- src/rendering/hwrenderer/scene/hw_portal.h | 4 +- src/rendering/hwrenderer/scene/hw_sky.cpp | 2 +- src/rendering/hwrenderer/scene/hw_walls.cpp | 2 - src/rendering/r_utility.cpp | 20 ++++- src/rendering/r_utility.h | 1 + 11 files changed, 118 insertions(+), 111 deletions(-) diff --git a/src/rendering/hwrenderer/hw_entrypoint.cpp b/src/rendering/hwrenderer/hw_entrypoint.cpp index e55c041a6..0252fbc40 100644 --- a/src/rendering/hwrenderer/hw_entrypoint.cpp +++ b/src/rendering/hwrenderer/hw_entrypoint.cpp @@ -167,6 +167,7 @@ sector_t* RenderViewpoint(FRenderViewpoint& mainvp, AActor* camera, IntRect* bou bool iso_ortho = (camera->ViewPos != NULL) && (camera->ViewPos->Flags & VPSF_ORTHOGRAPHIC); if (iso_ortho && (camera->ViewPos->Offset.Length() > 0)) inv_iso_dist = 1.0/camera->ViewPos->Offset.Length(); di->VPUniforms.mProjectionMatrix = eye.GetProjection(fov, ratio, fovratio * inv_iso_dist, iso_ortho); + di->ProjectionMatrix2 = eye.GetProjection(fov, ratio, fovratio, false); // Regular ol' perspective projection matrix // Stereo mode specific viewpoint adjustment vp.Pos += eye.GetViewShift(vp.HWAngles.Yaw.Degrees()); diff --git a/src/rendering/hwrenderer/scene/hw_bsp.cpp b/src/rendering/hwrenderer/scene/hw_bsp.cpp index 2c1abdf7c..ced71a6fa 100644 --- a/src/rendering/hwrenderer/scene/hw_bsp.cpp +++ b/src/rendering/hwrenderer/scene/hw_bsp.cpp @@ -287,15 +287,16 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) auto &clipperr = *rClipper; angle_t startAngleR = clipperr.PointToPseudoAngle(seg->v2->fX(), seg->v2->fY()); angle_t endAngleR = clipperr.PointToPseudoAngle(seg->v1->fX(), seg->v1->fY()); + angle_t paddingR = 0x00200000; // Make radar clipping more aggressive (reveal less) if(Viewpoint.IsAllowedOoB() && r_radarclipper && !(Level->flags3 & LEVEL3_NOFOGOFWAR) && (startAngleR - endAngleR >= ANGLE_180)) { - if (!seg->backsector) clipperr.SafeAddClipRange(startAngleR, endAngleR); + if (!seg->backsector) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); else if((seg->sidedef != nullptr) && !uint8_t(seg->sidedef->Flags & WALLF_POLYOBJ) && (currentsector->sectornum != seg->backsector->sectornum)) { if (in_area == area_default) in_area = hw_CheckViewArea(seg->v1, seg->v2, seg->frontsector, seg->backsector); backsector = hw_FakeFlat(seg->backsector, in_area, true); - if (hw_CheckClip(seg->sidedef, currentsector, backsector)) clipperr.SafeAddClipRange(startAngleR, endAngleR); + if (hw_CheckClip(seg->sidedef, currentsector, backsector)) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); } } @@ -313,7 +314,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) { currentsubsector->flags |= SSECMF_DRAWN; } - if ((r_radarclipper || !(Level->flags3 & LEVEL3_NOFOGOFWAR)) && clipperr.SafeCheckRange(startAngleR, endAngleR)) + if (Viewpoint.IsAllowedOoB() && (r_radarclipper && !(Level->flags3 & LEVEL3_NOFOGOFWAR)) && clipperr.SafeCheckRange(startAngleR, endAngleR)) { currentsubsector->flags |= SSECMF_DRAWN; } @@ -326,31 +327,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) return; } - if (Viewpoint.IsAllowedOoB()) // No need for vertical clipping if viewpoint not allowed out of bounds - { - auto &clipperv = *vClipper; - angle_t startPitch = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), currentsector->floorplane.ZatPoint(seg->v1)); - angle_t endPitch = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), currentsector->ceilingplane.ZatPoint(seg->v1)); - angle_t startPitch2 = clipperv.PointToPseudoPitch(seg->v2->fX(), seg->v2->fY(), currentsector->floorplane.ZatPoint(seg->v2)); - angle_t endPitch2 = clipperv.PointToPseudoPitch(seg->v2->fX(), seg->v2->fY(), currentsector->ceilingplane.ZatPoint(seg->v2)); - angle_t temp; - // Wall can be tilted from viewpoint perspective. Find vertical extent on screen in psuedopitch units (0 to 2, bottom to top) - if(int(startPitch) > int(startPitch2)) // Handle zero crossing - { - temp = startPitch; startPitch = startPitch2; startPitch2 = temp; // exchange - } - if(int(endPitch) > int(endPitch2)) // Handle zero crossing - { - temp = endPitch; endPitch = endPitch2; endPitch2 = temp; // exchange - } - - if (!clipperv.SafeCheckRange(startPitch, endPitch2)) - { - return; - } - } - - if (!r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR) || clipperr.SafeCheckRange(startAngleR, endAngleR)) + if (Viewpoint.IsAllowedOoB() && (!r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR) || clipperr.SafeCheckRange(startAngleR, endAngleR))) currentsubsector->flags |= SSECMF_DRAWN; uint8_t ispoly = uint8_t(seg->sidedef->Flags & WALLF_POLYOBJ); @@ -729,8 +706,8 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) if(Viewpoint.IsAllowedOoB() && sector->isSecret() && sector->wasSecret() && !r_radarclipper) return; - // cull everything if subsector outside vertical clipper - if ((sub->polys == nullptr) && (!Viewpoint.IsOrtho() || !((Level->flags3 & LEVEL3_NOFOGOFWAR) || !r_radarclipper))) + // cull everything if subsector outside all relevant clippers + if ((sub->polys == nullptr)) { auto &clipper = *mClipper; auto &clipperv = *vClipper; @@ -739,7 +716,7 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) seg_t * seg = sub->firstline; bool anglevisible = false; bool pitchvisible = !(Viewpoint.IsAllowedOoB()); // No vertical clipping if viewpoint is not allowed out of bounds - bool radarvisible = false; + bool radarvisible = !(Viewpoint.IsAllowedOoB()) || !r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR) || ((sub->flags & SSECMF_DRAWN) && !deathmatch); angle_t pitchtemp; angle_t pitchmin = ANGLE_90; angle_t pitchmax = 0; @@ -748,13 +725,19 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) { if((seg->v1 != nullptr) && (seg->v2 != nullptr)) { - angle_t startAngle = clipper.GetClipAngle(seg->v2); - angle_t endAngle = clipper.GetClipAngle(seg->v1); - if (startAngle-endAngle >= ANGLE_180) anglevisible |= clipper.SafeCheckRange(startAngle, endAngle); - angle_t startAngleR = clipperr.PointToPseudoAngle(seg->v2->fX(), seg->v2->fY()); - angle_t endAngleR = clipperr.PointToPseudoAngle(seg->v1->fX(), seg->v1->fY()); - if (startAngleR-endAngleR >= ANGLE_180) - radarvisible |= (clipperr.SafeCheckRange(startAngleR, endAngleR) || (Level->flags3 & LEVEL3_NOFOGOFWAR) || ((sub->flags & SSECMF_DRAWN) && !deathmatch)); + if (!anglevisible) + { + angle_t startAngle = clipper.GetClipAngle(seg->v2); + angle_t endAngle = clipper.GetClipAngle(seg->v1); + if (startAngle-endAngle >= ANGLE_180) anglevisible |= clipper.SafeCheckRange(startAngle, endAngle); + } + if (!radarvisible) + { + angle_t startAngleR = clipperr.PointToPseudoAngle(seg->v2->fX(), seg->v2->fY()); + angle_t endAngleR = clipperr.PointToPseudoAngle(seg->v1->fX(), seg->v1->fY()); + if (startAngleR-endAngleR >= ANGLE_180) radarvisible |= clipperr.SafeCheckRange(startAngleR, endAngleR); + } + if (!pitchvisible) { pitchmin = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), sector->floorplane.ZatPoint(seg->v1)); @@ -988,8 +971,8 @@ void HWDrawInfo::RenderOrthoNoFog() { if (Viewpoint.IsOrtho() && ((Level->flags3 & LEVEL3_NOFOGOFWAR) || !r_radarclipper)) { - double vxdbl = Viewpoint.camera->X(); - double vydbl = Viewpoint.camera->Y(); + double vxdbl = Viewpoint.OffPos.X; + double vydbl = Viewpoint.OffPos.Y; double ext = Viewpoint.camera->ViewPos->Offset.Length() ? 3.0 * Viewpoint.camera->ViewPos->Offset.Length() * tan (Viewpoint.FieldOfView.Radians()*0.5) : 100.0; FBoundingBox viewbox(vxdbl, vydbl, ext); @@ -1014,16 +997,8 @@ void HWDrawInfo::RenderBSP(void *node, bool drawpsprites) viewy = FLOAT2FIXED(Viewpoint.Pos.Y); if (r_radarclipper && !(Level->flags3 & LEVEL3_NOFOGOFWAR) && Viewpoint.IsAllowedOoB()) { - if (Viewpoint.camera->tracer != NULL) - { - viewx = FLOAT2FIXED(Viewpoint.camera->tracer->X()); - viewy = FLOAT2FIXED(Viewpoint.camera->tracer->Y()); - } - else - { - viewx = FLOAT2FIXED(Viewpoint.camera->X()); - viewy = FLOAT2FIXED(Viewpoint.camera->Y()); - } + viewx = FLOAT2FIXED(Viewpoint.OffPos.X); + viewy = FLOAT2FIXED(Viewpoint.OffPos.Y); } validcount++; // used for processing sidedefs only once by the renderer. diff --git a/src/rendering/hwrenderer/scene/hw_clipper.cpp b/src/rendering/hwrenderer/scene/hw_clipper.cpp index 059a39981..7444af309 100644 --- a/src/rendering/hwrenderer/scene/hw_clipper.cpp +++ b/src/rendering/hwrenderer/scene/hw_clipper.cpp @@ -394,18 +394,10 @@ angle_t Clipper::PointToPseudoAngle(double x, double y) { double vecx = x - viewpoint->Pos.X; double vecy = y - viewpoint->Pos.Y; - if ((viewpoint->camera != NULL) && amRadar) + if (amRadar) { - if (viewpoint->camera->tracer != NULL) - { - vecx = x - viewpoint->camera->tracer->X(); - vecy = y - viewpoint->camera->tracer->Y(); - } - else - { - vecx = x - viewpoint->camera->X(); - vecy = y - viewpoint->camera->Y(); - } + vecx = x - viewpoint->OffPos.X; + vecy = y - viewpoint->OffPos.Y; } if (vecx == 0 && vecy == 0) @@ -467,7 +459,7 @@ angle_t Clipper::PointToPseudoPitch(double x, double y, double z) angle_t Clipper::PointToPseudoOrthoAngle(double x, double y) { - DVector3 disp = DVector3( x, y, 0 ) - viewpoint->camera->Pos(); + DVector3 disp = DVector3(x, y, 0) - viewpoint->OffPos; if (viewpoint->camera->ViewPos->Offset.XY().Length() == 0) { return AngleToPseudo( viewpoint->Angles.Yaw.BAMs() ); @@ -491,7 +483,7 @@ angle_t Clipper::PointToPseudoOrthoAngle(double x, double y) angle_t Clipper::PointToPseudoOrthoPitch(double x, double y, double z) { - DVector3 disp = DVector3( x, y, z ) - viewpoint->camera->Pos(); + DVector3 disp = DVector3(x, y, z) - viewpoint->OffPos; if (viewpoint->camera->ViewPos->Offset.XY().Length() > 0) { double yproj = viewpoint->PitchSin * disp.XY().Length() * deltaangle(disp.Angle(), viewpoint->Angles.Yaw).Cos(); diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 4068fbbb8..10ee57b88 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -160,6 +160,7 @@ void HWDrawInfo::StartScene(FRenderViewpoint &parentvp, HWViewpointUniforms *uni VPUniforms.mProjectionMatrix.loadIdentity(); VPUniforms.mViewMatrix.loadIdentity(); VPUniforms.mNormalViewMatrix.loadIdentity(); + ProjectionMatrix2.loadIdentity(); VPUniforms.mViewHeight = viewheight; if (lightmode == ELightMode::Build) { @@ -684,60 +685,70 @@ void HWDrawInfo::DrawCorona(FRenderState& state, ACorona* corona, double dist) static ETraceStatus TraceCallbackForDitherTransparency(FTraceResults& res, void* userdata) { - int* count = (int*)userdata; + BitArray* CurMapSections = (BitArray*)userdata; double bf, bc; - (*count)++; + switch(res.HitType) { case TRACE_HitWall: if (!(res.Line->sidedef[res.Side]->Flags & WALLF_DITHERTRANS)) { - bf = res.Line->sidedef[res.Side]->sector->floorplane.ZatPoint(res.HitPos.XY()); - bc = res.Line->sidedef[res.Side]->sector->ceilingplane.ZatPoint(res.HitPos.XY()); - if ((res.HitPos.Z <= bc) && (res.HitPos.Z >= bf)) res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS; + sector_t* linesec = res.Line->sidedef[res.Side]->sector; + if (linesec->subsectorcount > 0 && (*CurMapSections)[linesec->subsectors[0]->mapsection]) + { + bf = res.Line->sidedef[res.Side]->sector->floorplane.ZatPoint(res.HitPos.XY()); + bc = res.Line->sidedef[res.Side]->sector->ceilingplane.ZatPoint(res.HitPos.XY()); + if ((res.HitPos.Z <= bc) && (res.HitPos.Z >= bf)) res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS; + } } break; case TRACE_HitFloor: - if (res.HitPos.Z == res.Sector->floorplane.ZatPoint(res.HitPos)) + if (res.Sector->subsectorcount > 0 && (*CurMapSections)[res.Sector->subsectors[0]->mapsection]) { - res.Sector->floorplane.dithertransflag = true; - } - else if (res.Sector->e->XFloor.ffloors.Size()) // Maybe it was 3D floors - { - F3DFloor *rover; - int kk; - for (kk = 0; kk < (int)res.Sector->e->XFloor.ffloors.Size(); kk++) + if (res.HitPos.Z == res.Sector->floorplane.ZatPoint(res.HitPos)) { - rover = res.Sector->e->XFloor.ffloors[kk]; - if ((rover->flags&(FF_EXISTS | FF_RENDERPLANES | FF_THISINSIDE)) == (FF_EXISTS | FF_RENDERPLANES)) + res.Sector->floorplane.dithertransflag = true; + } + else if (res.Sector->e->XFloor.ffloors.Size()) // Maybe it was 3D floors + { + F3DFloor *rover; + int kk; + for (kk = 0; kk < (int)res.Sector->e->XFloor.ffloors.Size(); kk++) { - if (res.HitPos.Z == rover->top.plane->ZatPoint(res.HitPos)) + rover = res.Sector->e->XFloor.ffloors[kk]; + if ((rover->flags&(FF_EXISTS | FF_RENDERPLANES | FF_THISINSIDE)) == (FF_EXISTS | FF_RENDERPLANES)) { - rover->top.plane->dithertransflag = true; - break; // Out of for loop + if (res.HitPos.Z == rover->top.plane->ZatPoint(res.HitPos)) + { + rover->top.plane->dithertransflag = true; + break; // Out of for loop + } } } } } break; case TRACE_HitCeiling: - if (res.HitPos.Z == res.Sector->ceilingplane.ZatPoint(res.HitPos)) + if (res.Sector->subsectorcount > 0 && (*CurMapSections)[res.Sector->subsectors[0]->mapsection]) { - res.Sector->ceilingplane.dithertransflag = true; - } - else if (res.Sector->e->XFloor.ffloors.Size()) // Maybe it was 3D floors - { - F3DFloor *rover; - int kk; - for (kk = 0; kk < (int)res.Sector->e->XFloor.ffloors.Size(); kk++) + if (res.HitPos.Z == res.Sector->ceilingplane.ZatPoint(res.HitPos)) { - rover = res.Sector->e->XFloor.ffloors[kk]; - if ((rover->flags&(FF_EXISTS | FF_RENDERPLANES | FF_THISINSIDE)) == (FF_EXISTS | FF_RENDERPLANES)) + res.Sector->ceilingplane.dithertransflag = true; + } + else if (res.Sector->e->XFloor.ffloors.Size()) // Maybe it was 3D floors + { + F3DFloor *rover; + int kk; + for (kk = 0; kk < (int)res.Sector->e->XFloor.ffloors.Size(); kk++) { - if (res.HitPos.Z == rover->bottom.plane->ZatPoint(res.HitPos)) + rover = res.Sector->e->XFloor.ffloors[kk]; + if ((rover->flags&(FF_EXISTS | FF_RENDERPLANES | FF_THISINSIDE)) == (FF_EXISTS | FF_RENDERPLANES)) { - rover->bottom.plane->dithertransflag = true; - break; // Out of for loop + if (res.HitPos.Z == rover->bottom.plane->ZatPoint(res.HitPos)) + { + rover->bottom.plane->dithertransflag = true; + break; // Out of for loop + } } } } @@ -763,13 +774,11 @@ void HWDrawInfo::SetDitherTransFlags(AActor* actor) DVector3 vvec = actorpos - Viewpoint.Pos; if (Viewpoint.IsOrtho()) { - vvec += Viewpoint.camera->Pos() - actorpos; - vvec *= 5.0; // Should be 4.0? (since zNear is behind screen by 3*dist in VREyeInfo::GetProjection()) + vvec = 5.0 * Viewpoint.camera->ViewPos->Offset.Length() * Viewpoint.ViewVector3D; // Should be 4.0? (since zNear is behind screen by 3*dist in VREyeInfo::GetProjection()) } double distance = vvec.Length() - actor->radius; DVector3 campos = actorpos - vvec; sector_t* startsec; - int count = 0; vvec = vvec.Unit(); campos.X -= horix; campos.Y += horiy; campos.Z += actor->Height * 0.25; @@ -777,10 +786,10 @@ void HWDrawInfo::SetDitherTransFlags(AActor* actor) { startsec = Level->PointInRenderSubsector(campos)->sector; Trace(campos, startsec, vvec, distance, - 0, 0, actor, results, 0, TraceCallbackForDitherTransparency, &count); + 0, 0, actor, results, TRACE_PortalRestrict, TraceCallbackForDitherTransparency, &CurrentMapSections); campos.Z += actor->Height * 0.5; Trace(campos, startsec, vvec, distance, - 0, 0, actor, results, 0, TraceCallbackForDitherTransparency, &count); + 0, 0, actor, results, TRACE_PortalRestrict, TraceCallbackForDitherTransparency, &CurrentMapSections); campos.Z -= actor->Height * 0.5; campos.X += horix; campos.Y -= horiy; } diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.h b/src/rendering/hwrenderer/scene/hw_drawinfo.h index ce848e297..e4cbeb98e 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.h +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.h @@ -150,6 +150,7 @@ struct HWDrawInfo Clipper *rClipper; // Radar clipper FRenderViewpoint Viewpoint; HWViewpointUniforms VPUniforms; // per-viewpoint uniform state + VSMatrix ProjectionMatrix2; TArray Portals; TArray Decals[2]; // the second slot is for mirrors which get rendered in a separate pass. TArray hudsprites; // These may just be stored by value. diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index b0ea3a8c9..b103f0f81 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -125,6 +125,7 @@ bool FPortalSceneState::RenderFirstSkyPortal(int recursion, HWDrawInfo *outer_di { HWPortal * best = nullptr; unsigned bestindex = 0; + bool usestencil = outer_di->Viewpoint.IsAllowedOoB(); // Find the one with the highest amount of lines. // Normally this is also the one that saves the largest amount @@ -145,7 +146,7 @@ bool FPortalSceneState::RenderFirstSkyPortal(int recursion, HWDrawInfo *outer_di } // If the portal area contains the current camera viewpoint, let's always use it because it's likely to give the largest area. - if (p->boundingBox.contains(outer_di->Viewpoint.Pos)) + if (p->boundingBox.contains(usestencil ? outer_di->Viewpoint.OffPos : outer_di->Viewpoint.Pos)) { best = p; bestindex = i; @@ -157,7 +158,13 @@ bool FPortalSceneState::RenderFirstSkyPortal(int recursion, HWDrawInfo *outer_di if (best) { portals.Delete(bestindex); - RenderPortal(best, state, false, outer_di); + if (usestencil) + { + tempmatrix = outer_di->VPUniforms.mProjectionMatrix; // ensure perspective projection matrix for skies + outer_di->VPUniforms.mProjectionMatrix = outer_di->ProjectionMatrix2; + } + RenderPortal(best, state, usestencil, outer_di); + if (usestencil) outer_di->VPUniforms.mProjectionMatrix = tempmatrix; delete best; return true; } @@ -410,20 +417,23 @@ void HWScenePortalBase::ClearClipper(HWDrawInfo *di, Clipper *clipper) // Set the clipper to the minimal visible area clipper->SafeAddClipRange(0, 0xffffffff); + auto outvp = outer_di->Viewpoint; + DVector3 outPos = clipper->amRadar ? outvp.OffPos : outvp.Pos; + angle_t padding = clipper->amRadar ? 0x00200000 : 0x00000000; // Make radar clipping more aggressive (reveal less) for (unsigned int i = 0; i < lines.Size(); i++) { - DAngle startAngle = (DVector2(lines[i].glseg.x2, lines[i].glseg.y2) - outer_di->Viewpoint.Pos).Angle() + angleOffset; - DAngle endAngle = (DVector2(lines[i].glseg.x1, lines[i].glseg.y1) - outer_di->Viewpoint.Pos).Angle() + angleOffset; + DAngle startAngle = (DVector2(lines[i].glseg.x2, lines[i].glseg.y2) - outPos).Angle() + angleOffset; + DAngle endAngle = (DVector2(lines[i].glseg.x1, lines[i].glseg.y1) - outPos).Angle() + angleOffset; if (deltaangle(endAngle, startAngle) < nullAngle) { - clipper->SafeRemoveClipRangeRealAngles(startAngle.BAMs(), endAngle.BAMs()); + clipper->SafeRemoveClipRangeRealAngles(startAngle.BAMs() + padding, endAngle.BAMs() - padding); } } // and finally clip it to the visible area angle_t a1 = di->FrustumAngle(); - if (a1 < ANGLE_180) clipper->SafeAddClipRangeRealAngles(di->Viewpoint.Angles.Yaw.BAMs() + a1, di->Viewpoint.Angles.Yaw.BAMs() - a1); + if (!clipper->amRadar && a1 < ANGLE_180) clipper->SafeAddClipRangeRealAngles(di->Viewpoint.Angles.Yaw.BAMs() + a1, di->Viewpoint.Angles.Yaw.BAMs() - a1); // lock the parts that have just been clipped out. clipper->SetSilhouette(); @@ -608,6 +618,8 @@ bool HWLineToLinePortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *cl P_TranslatePortalZ(origin, vp.Pos.Z); P_TranslatePortalXY(origin, vp.Path[0].X, vp.Path[0].Y); P_TranslatePortalXY(origin, vp.Path[1].X, vp.Path[1].Y); + P_TranslatePortalXY(origin, vp.OffPos.X, vp.OffPos.Y); + P_TranslatePortalZ(origin, vp.OffPos.Z); if (!vp.showviewer && vp.camera != nullptr && P_PointOnLineSidePrecise(vp.Path[0], glport->lines[0]->mDestination) != P_PointOnLineSidePrecise(vp.Path[1], glport->lines[0]->mDestination)) { double distp = (vp.Path[0] - vp.Path[1]).Length(); diff --git a/src/rendering/hwrenderer/scene/hw_portal.h b/src/rendering/hwrenderer/scene/hw_portal.h index 29481f9e3..760b8c435 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.h +++ b/src/rendering/hwrenderer/scene/hw_portal.h @@ -112,6 +112,8 @@ struct FPortalSceneState int skyboxrecursion = 0; + VSMatrix tempmatrix; + void BeginScene() { UniqueSkies.Clear(); @@ -145,7 +147,7 @@ public: virtual bool NeedDepthBuffer() { return true; } virtual void DrawContents(HWDrawInfo *di, FRenderState &state) { - if (Setup(di, state, di->mClipper)) + if (Setup(di, state, (di->Viewpoint.IsAllowedOoB() ? di->rClipper : di->mClipper))) { di->DrawScene(DM_PORTAL); Shutdown(di, state); diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index 6d77a275d..93fc827cc 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -130,7 +130,6 @@ void HWSkyInfo::init(HWDrawInfo *di, sector_t* sec, int skypos, int sky1, PalEnt void HWWall::SkyPlane(HWWallDispatcher *di, sector_t *sector, int plane, bool allowreflect) { int ptype = -1; - if (di->di && di->di->Viewpoint.IsAllowedOoB()) return; // Couldn't prevent sky portal occlusion. Skybox is bad in ortho too. FSectorPortal *sportal = sector->ValidatePortal(plane); if (sportal != nullptr && sportal->mFlags & PORTSF_INSKYBOX) sportal = nullptr; // no recursions, delete it here to simplify the following code @@ -155,6 +154,7 @@ void HWWall::SkyPlane(HWWallDispatcher *di, sector_t *sector, int plane, bool al case PORTS_PORTAL: case PORTS_LINKEDPORTAL: { + if (di->di && di->di->Viewpoint.IsAllowedOoB()) return; auto glport = sector->GetPortalGroup(plane); if (glport != NULL) { diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index 5c5e357d5..97b0c91a6 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -2181,8 +2181,6 @@ void HWWall::Process(HWWallDispatcher *di, seg_t *seg, sector_t * frontsector, s } bool isportal = seg->linedef->isVisualPortal() && seg->sidedef == seg->linedef->sidedef[0]; - // Don't render portal insides if in orthographic mode - if (di->di) isportal &= !(di->di->Viewpoint.IsOrtho()); //return; // [GZ] 3D middle textures are necessarily two-sided, even if they lack the explicit two-sided flag diff --git a/src/rendering/r_utility.cpp b/src/rendering/r_utility.cpp index cd0c3d0c2..035ac6de7 100644 --- a/src/rendering/r_utility.cpp +++ b/src/rendering/r_utility.cpp @@ -550,8 +550,12 @@ void R_InterpolateView(FRenderViewpoint& viewPoint, const player_t* const player const int prevPortalGroup = viewLvl->PointInRenderSubsector(iView->Old.Pos)->sector->PortalGroup; const int curPortalGroup = viewLvl->PointInRenderSubsector(iView->New.Pos)->sector->PortalGroup; - const DVector2 portalOffset = viewLvl->Displacements.getOffset(prevPortalGroup, curPortalGroup); - viewPoint.Pos = iView->Old.Pos * inverseTicFrac + (iView->New.Pos - portalOffset) * ticFrac; + if (viewPoint.IsAllowedOoB() && prevPortalGroup != curPortalGroup) viewPoint.Pos = iView->New.Pos; + else + { + const DVector2 portalOffset = viewLvl->Displacements.getOffset(prevPortalGroup, curPortalGroup); + viewPoint.Pos = iView->Old.Pos * inverseTicFrac + (iView->New.Pos - portalOffset) * ticFrac; + } viewPoint.Path[0] = viewPoint.Path[1] = iView->New.Pos; } } @@ -708,6 +712,18 @@ void FRenderViewpoint::SetViewAngle(const FViewWindow& viewWindow) ViewVector3D.Y = v.Y * PitchCos; ViewVector3D.Z = -PitchSin; + if (IsOrtho() || IsAllowedOoB()) // These auto-ensure that camera and camera->ViewPos exist + { + if (camera->tracer != NULL) + { + OffPos = camera->tracer->Pos(); + } + else + { + OffPos = Pos + ViewVector3D * camera->ViewPos->Offset.Length(); + } + } + if (IsOrtho() && (camera->ViewPos->Offset.XY().Length() > 0.0)) { ScreenProj = 1.34396 / camera->ViewPos->Offset.Length() / tan (FieldOfView.Radians()*0.5); // [DVR] Estimated. +/-1 should be top/bottom of screen. diff --git a/src/rendering/r_utility.h b/src/rendering/r_utility.h index d82b03e68..0c2f47833 100644 --- a/src/rendering/r_utility.h +++ b/src/rendering/r_utility.h @@ -26,6 +26,7 @@ struct FRenderViewpoint FRotator HWAngles; // Actual rotation angles for the hardware renderer DVector2 ViewVector; // HWR only: direction the camera is facing. DVector3 ViewVector3D; // 3D direction the camera is facing. + DVector3 OffPos; // Viewpoint position to use for Ortho and OoB calculations AActor *ViewActor; // either the same as camera or nullptr FLevelLocals *ViewLevel; // The level this viewpoint is on. From 0acfd9661bfcfc646bdc0adab6d1498350ad9899 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Thu, 13 Feb 2025 21:54:36 -0700 Subject: [PATCH 043/384] Making 3D-floors respond to r_dithertransparency properly. --- src/gamedata/r_defs.h | 5 ++++ src/rendering/hwrenderer/scene/hw_bsp.cpp | 5 ++-- .../hwrenderer/scene/hw_drawinfo.cpp | 30 +++++++++++++++++-- src/rendering/hwrenderer/scene/hw_walls.cpp | 22 ++++++++++++-- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/gamedata/r_defs.h b/src/gamedata/r_defs.h index a6f4c2c4f..663f3aa38 100644 --- a/src/gamedata/r_defs.h +++ b/src/gamedata/r_defs.h @@ -1196,6 +1196,9 @@ enum WALLF_ABSLIGHTING_BOTTOM = WALLF_ABSLIGHTING_TIER << 2, // Bottom tier light is absolute instead of relative WALLF_DITHERTRANS = 8192, // Render with dithering transparency shader (gets reset every frame) + WALLF_DITHERTRANS_TOP = WALLF_DITHERTRANS << 0, // Top tier (gets reset every frame) + WALLF_DITHERTRANS_MID = WALLF_DITHERTRANS << 1, // Mid tier (gets reset every frame) + WALLF_DITHERTRANS_BOTTOM = WALLF_DITHERTRANS << 2, // Bottom tier (gets reset every frame) }; struct side_t @@ -1271,6 +1274,8 @@ struct side_t int numsegs; int sidenum; + int dithertranscount; + int GetLightLevel (bool foggy, int baselight, int which, bool is3dlight=false, int *pfakecontrast_usedbygzdoom=NULL) const; void SetLight(int16_t l) diff --git a/src/rendering/hwrenderer/scene/hw_bsp.cpp b/src/rendering/hwrenderer/scene/hw_bsp.cpp index ced71a6fa..5e64c8fdd 100644 --- a/src/rendering/hwrenderer/scene/hw_bsp.cpp +++ b/src/rendering/hwrenderer/scene/hw_bsp.cpp @@ -297,6 +297,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) if (in_area == area_default) in_area = hw_CheckViewArea(seg->v1, seg->v2, seg->frontsector, seg->backsector); backsector = hw_FakeFlat(seg->backsector, in_area, true); if (hw_CheckClip(seg->sidedef, currentsector, backsector)) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); + backsector = nullptr; } } @@ -335,7 +336,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) if (!seg->backsector) { if(!Viewpoint.IsAllowedOoB()) - if (!(seg->sidedef->Flags & WALLF_DITHERTRANS)) clipper.SafeAddClipRange(startAngle, endAngle); + if (!(seg->sidedef->Flags & WALLF_DITHERTRANS_MID)) clipper.SafeAddClipRange(startAngle, endAngle); } else if (!ispoly) // Two-sided polyobjects never obstruct the view { @@ -362,7 +363,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) if (hw_CheckClip(seg->sidedef, currentsector, backsector)) { - if(!Viewpoint.IsAllowedOoB() && !(seg->sidedef->Flags & WALLF_DITHERTRANS)) + if(!Viewpoint.IsAllowedOoB() && !(seg->sidedef->Flags & WALLF_DITHERTRANS_MID)) clipper.SafeAddClipRange(startAngle, endAngle); } } diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 10ee57b88..0fc7bc896 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -691,14 +691,28 @@ static ETraceStatus TraceCallbackForDitherTransparency(FTraceResults& res, void* switch(res.HitType) { case TRACE_HitWall: - if (!(res.Line->sidedef[res.Side]->Flags & WALLF_DITHERTRANS)) { sector_t* linesec = res.Line->sidedef[res.Side]->sector; if (linesec->subsectorcount > 0 && (*CurMapSections)[linesec->subsectors[0]->mapsection]) { bf = res.Line->sidedef[res.Side]->sector->floorplane.ZatPoint(res.HitPos.XY()); bc = res.Line->sidedef[res.Side]->sector->ceilingplane.ZatPoint(res.HitPos.XY()); - if ((res.HitPos.Z <= bc) && (res.HitPos.Z >= bf)) res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS; + if (res.Line->sidedef[!res.Side]) + { + // Two sided line! So let's find out if mid, top, or bottom texture needs dithered transparency + bf = max(bf, res.Line->sidedef[!res.Side]->sector->floorplane.ZatPoint(res.HitPos.XY())); + bc = min(bc, res.Line->sidedef[!res.Side]->sector->ceilingplane.ZatPoint(res.HitPos.XY())); + if (res.HitPos.Z <= bf) res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS_BOTTOM; + else if (res.HitPos.Z < bc) res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS_MID; + else res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS_TOP; + + res.Line->sidedef[res.Side]->dithertranscount = max(1, res.Line->sidedef[!res.Side]->sector->e->XFloor.ffloors.Size()); + } + else if ((res.HitPos.Z <= bc) && (res.HitPos.Z >= bf)) + { + res.Line->sidedef[res.Side]->Flags |= WALLF_DITHERTRANS_MID; + res.Line->sidedef[res.Side]->dithertranscount = 1; + } } } break; @@ -793,6 +807,18 @@ void HWDrawInfo::SetDitherTransFlags(AActor* actor) campos.Z -= actor->Height * 0.5; campos.X += horix; campos.Y -= horiy; } + + // Tracers don't work on 3D floors when you are starting in the same sector (standing under them, for example) + if (actor->Sector->e->XFloor.ffloors.Size()) // 3D floor + { + F3DFloor *rover; + for (int kk = 0; kk < (int)actor->Sector->e->XFloor.ffloors.Size(); kk++) + { + rover = actor->Sector->e->XFloor.ffloors[kk]; + rover->top.plane->dithertransflag = true; + rover->bottom.plane->dithertransflag = true; + } + } } } diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index 97b0c91a6..47e651bd4 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -83,15 +83,31 @@ void SetSplitPlanes(FRenderState& state, const secplane_t& top, const secplane_t void HWWall::RenderWall(FRenderState &state, int textured) { - if (seg->sidedef->Flags & WALLF_DITHERTRANS) state.SetEffect(EFF_DITHERTRANS); + bool ditherT = (type == RENDERWALL_BOTTOM) && (seg->sidedef->Flags & WALLF_DITHERTRANS_BOTTOM); + ditherT |= (type == RENDERWALL_TOP) && (seg->sidedef->Flags & WALLF_DITHERTRANS_TOP); + ditherT |= seg->sidedef->Flags & WALLF_DITHERTRANS_MID; + if (ditherT) + { + state.SetEffect(EFF_DITHERTRANS); + } assert(vertcount > 0); state.SetLightIndex(dynlightindex); state.Draw(DT_TriangleFan, vertindex, vertcount); vertexcount += vertcount; - if (seg->sidedef->Flags & WALLF_DITHERTRANS) + if (ditherT) { state.SetEffect(EFF_NONE); - seg->sidedef->Flags &= ~WALLF_DITHERTRANS; // reset this every frame + switch(type) // reset this every frame + { + case RENDERWALL_TOP: + seg->sidedef->Flags &= ~WALLF_DITHERTRANS_TOP; + break; + case RENDERWALL_BOTTOM: + seg->sidedef->Flags &= ~WALLF_DITHERTRANS_BOTTOM; + break; + default: + if (seg->sidedef->dithertranscount-- <= 0) seg->sidedef->Flags &= ~WALLF_DITHERTRANS_MID; + } } } From dc5a25079728c90001c9334fce61e08bbe897290 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sat, 15 Feb 2025 21:44:15 -0700 Subject: [PATCH 044/384] Reflective flats now work with OoB viewpoints, including ortho. Had to create a new type of portal stencil for the HWPlaneMirrorPortal. Stacked sector portals could be made to work the same way, but there are clipper issues, revealing out-of-view sections of the map on the other side. Hence sector portal rendering is still disabled in OoB viewpoints. --- src/rendering/hwrenderer/scene/hw_bsp.cpp | 25 +++++++++++++++---- .../hwrenderer/scene/hw_drawinfo.cpp | 5 ++-- src/rendering/hwrenderer/scene/hw_flats.cpp | 6 +++++ src/rendering/hwrenderer/scene/hw_portal.cpp | 25 +++++++++++++++++-- src/rendering/hwrenderer/scene/hw_portal.h | 4 ++- src/rendering/hwrenderer/scene/hw_sky.cpp | 2 +- src/rendering/hwrenderer/scene/hw_walls.cpp | 3 ++- 7 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_bsp.cpp b/src/rendering/hwrenderer/scene/hw_bsp.cpp index 5e64c8fdd..3f834fcf6 100644 --- a/src/rendering/hwrenderer/scene/hw_bsp.cpp +++ b/src/rendering/hwrenderer/scene/hw_bsp.cpp @@ -718,6 +718,9 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) bool anglevisible = false; bool pitchvisible = !(Viewpoint.IsAllowedOoB()); // No vertical clipping if viewpoint is not allowed out of bounds bool radarvisible = !(Viewpoint.IsAllowedOoB()) || !r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR) || ((sub->flags & SSECMF_DRAWN) && !deathmatch); + bool ceilreflect = (mCurrentPortal && strcmp(mCurrentPortal->GetName(), "Planemirror ceiling")); + bool floorreflect = (mCurrentPortal && strcmp(mCurrentPortal->GetName(), "Planemirror floor")); + double planez = (ceilreflect ? sector->ceilingplane.ZatPoint(Viewpoint.Pos) : sector->floorplane.ZatPoint(Viewpoint.Pos)); angle_t pitchtemp; angle_t pitchmin = ANGLE_90; angle_t pitchmax = 0; @@ -741,16 +744,28 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) if (!pitchvisible) { - pitchmin = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), sector->floorplane.ZatPoint(seg->v1)); - pitchmax = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), sector->ceilingplane.ZatPoint(seg->v1)); + pitchmin = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), + (ceilreflect || floorreflect) ? + 2 * planez - sector->floorplane.ZatPoint(seg->v1) : + sector->floorplane.ZatPoint(seg->v1)); + pitchmax = clipperv.PointToPseudoPitch(seg->v1->fX(), seg->v1->fY(), + (ceilreflect || floorreflect) ? + 2 * planez - sector->ceilingplane.ZatPoint(seg->v1) : + sector->ceilingplane.ZatPoint(seg->v1)); pitchvisible |= clipperv.SafeCheckRange(pitchmin, pitchmax); } if (pitchvisible && anglevisible && radarvisible) break; if (!pitchvisible) { - pitchtemp = clipperv.PointToPseudoPitch(seg->v2->fX(), seg->v2->fY(), sector->floorplane.ZatPoint(seg->v2)); + pitchtemp = clipperv.PointToPseudoPitch(seg->v2->fX(), seg->v2->fY(), + (ceilreflect || floorreflect) ? + 2 * planez - sector->floorplane.ZatPoint(seg->v2) : + sector->floorplane.ZatPoint(seg->v2)); if (int(pitchmin) > int(pitchtemp)) pitchmin = pitchtemp; - pitchtemp = clipperv.PointToPseudoPitch(seg->v2->fX(), seg->v2->fY(), sector->ceilingplane.ZatPoint(seg->v2)); + pitchtemp = clipperv.PointToPseudoPitch(seg->v2->fX(), seg->v2->fY(), + (ceilreflect || floorreflect) ? + 2 * planez - sector->ceilingplane.ZatPoint(seg->v2) : + sector->ceilingplane.ZatPoint(seg->v2)); if (int(pitchmax) < int(pitchtemp)) pitchmax = pitchtemp; pitchvisible |= clipperv.SafeCheckRange(pitchmin, pitchmax); } @@ -819,7 +834,7 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) SetupSprite.Unclock(); } } - if (r_dithertransparency && Viewpoint.IsAllowedOoB() && (RTnum < MAXDITHERACTORS)) + if (r_dithertransparency && Viewpoint.IsAllowedOoB() && (RTnum < MAXDITHERACTORS) && mCurrentPortal == nullptr) { // [DVR] Not parallelizable due to variables RTnum and RenderedTargets[] for (auto p = sector->touching_renderthings; p != nullptr; p = p->m_snext) diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 0fc7bc896..404203933 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -717,7 +717,7 @@ static ETraceStatus TraceCallbackForDitherTransparency(FTraceResults& res, void* } break; case TRACE_HitFloor: - if (res.Sector->subsectorcount > 0 && (*CurMapSections)[res.Sector->subsectors[0]->mapsection]) + if (res.Sector->subsectorcount > 0 && (*CurMapSections)[res.Sector->subsectors[0]->mapsection] && res.HitVector.dot(res.Sector->floorplane.Normal()) < 0.0) { if (res.HitPos.Z == res.Sector->floorplane.ZatPoint(res.HitPos)) { @@ -743,7 +743,7 @@ static ETraceStatus TraceCallbackForDitherTransparency(FTraceResults& res, void* } break; case TRACE_HitCeiling: - if (res.Sector->subsectorcount > 0 && (*CurMapSections)[res.Sector->subsectors[0]->mapsection]) + if (res.Sector->subsectorcount > 0 && (*CurMapSections)[res.Sector->subsectors[0]->mapsection] && res.HitVector.dot(res.Sector->ceilingplane.Normal()) < 0.0) { if (res.HitPos.Z == res.Sector->ceilingplane.ZatPoint(res.HitPos)) { @@ -779,6 +779,7 @@ static ETraceStatus TraceCallbackForDitherTransparency(FTraceResults& res, void* void HWDrawInfo::SetDitherTransFlags(AActor* actor) { + // This should really be moved to a shader and have the GPU do some shape-tracing. if (actor && actor->Sector) { FTraceResults results; diff --git a/src/rendering/hwrenderer/scene/hw_flats.cpp b/src/rendering/hwrenderer/scene/hw_flats.cpp index f14757ade..317ba9399 100644 --- a/src/rendering/hwrenderer/scene/hw_flats.cpp +++ b/src/rendering/hwrenderer/scene/hw_flats.cpp @@ -47,6 +47,7 @@ #include "hw_drawstructs.h" #include "hw_renderstate.h" #include "texturemanager.h" +#include "hw_viewpointbuffer.h" #ifdef _DEBUG CVAR(Int, gl_breaksec, -1, 0) @@ -314,6 +315,7 @@ void HWFlat::DrawFlat(HWDrawInfo *di, FRenderState &state, bool translucent) int rel = getExtraLight(); state.SetNormal(plane.plane.Normal().X, plane.plane.Normal().Z, plane.plane.Normal().Y); + double zshift = (plane.plane.Normal().Z > 0.0 ? 0.01f : -0.01f); // The HWPlaneMirrorPortal::DrawPortalStencil() z-fights with flats SetColor(state, di->Level, di->lightmode, lightlevel, rel, di->isFullbrightScene(), Colormap, alpha); SetFog(state, di->Level, di->lightmode, lightlevel, rel, di->isFullbrightScene(), &Colormap, false); @@ -364,7 +366,11 @@ void HWFlat::DrawFlat(HWDrawInfo *di, FRenderState &state, bool translucent) else state.AlphaFunc(Alpha_GEqual, 0.f); state.SetMaterial(texture, UF_Texture, 0, CLAMP_NONE, NO_TRANSLATION, -1); SetPlaneTextureRotation(state, &plane, texture); + di->VPUniforms.mViewMatrix.translate(0.0, zshift, 0.0); + screen->mViewpoints->SetViewpoint(state, &di->VPUniforms); DrawSubsectors(di, state); + di->VPUniforms.mViewMatrix.translate(0.0, -zshift, 0.0); + screen->mViewpoints->SetViewpoint(state, &di->VPUniforms); state.EnableTextureMatrix(false); } state.SetRenderStyle(DefaultRenderStyle()); diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index b103f0f81..01ab23b8a 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -158,13 +158,14 @@ bool FPortalSceneState::RenderFirstSkyPortal(int recursion, HWDrawInfo *outer_di if (best) { portals.Delete(bestindex); - if (usestencil) + if (usestencil && ((strcmp(best->GetName(), "Sky") == 0) || (strcmp(best->GetName(), "Skybox") == 0))) { tempmatrix = outer_di->VPUniforms.mProjectionMatrix; // ensure perspective projection matrix for skies outer_di->VPUniforms.mProjectionMatrix = outer_di->ProjectionMatrix2; } RenderPortal(best, state, usestencil, outer_di); - if (usestencil) outer_di->VPUniforms.mProjectionMatrix = tempmatrix; + if (usestencil && ((strcmp(best->GetName(), "Sky") == 0) || (strcmp(best->GetName(), "Skybox") == 0))) + outer_di->VPUniforms.mProjectionMatrix = tempmatrix; delete best; return true; } @@ -332,6 +333,7 @@ void HWPortal::RemoveStencil(HWDrawInfo *di, FRenderState &state, bool usestenci bool needdepth = NeedDepthBuffer(); // Restore the old view + auto &vp = di->Viewpoint; if (vp.camera != nullptr) vp.camera->renderflags = (vp.camera->renderflags & ~RF_MAYBEINVISIBLE) | savedvisibility; @@ -870,12 +872,31 @@ bool HWPlaneMirrorPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c state->PlaneMirrorFlag++; di->SetClipHeight(planez, state->PlaneMirrorMode < 0 ? -1.f : 1.f); di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1)); + vp.ViewVector3D.Z = - vp.ViewVector3D.Z; + vp.OffPos.Z = 2 * planez - vp.OffPos.Z; ClearClipper(di, clipper); di->UpdateCurrentMapSection(); return true; } + +void HWPlaneMirrorPortal::DrawPortalStencil(FRenderState &state, int pass) +{ + bool isceiling = planesused & (1 << sector_t::ceiling); + for (unsigned int i = 0; i < lines.Size(); i++) + { + flat.section = lines[i].sub->section; + flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; + flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); + // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection + + state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); + state.DrawIndexed(DT_Triangles, flat.iboindex + flat.section->vertexindex, flat.section->vertexcount, i == 0); + } +} + + void HWPlaneMirrorPortal::Shutdown(HWDrawInfo *di, FRenderState &rstate) { auto state = mState; diff --git a/src/rendering/hwrenderer/scene/hw_portal.h b/src/rendering/hwrenderer/scene/hw_portal.h index 760b8c435..ab9fec945 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.h +++ b/src/rendering/hwrenderer/scene/hw_portal.h @@ -59,13 +59,14 @@ class HWPortal TArray mPrimIndices; unsigned int mTopCap = ~0u, mBottomCap = ~0u; - void DrawPortalStencil(FRenderState &state, int pass); + virtual void DrawPortalStencil(FRenderState &state, int pass); public: FPortalSceneState * mState; TArray lines; BoundingRect boundingBox; int planesused = 0; + HWFlat flat; HWPortal(FPortalSceneState *s, bool local = false) : mState(s), boundingBox(false) { @@ -295,6 +296,7 @@ struct HWPlaneMirrorPortal : public HWScenePortalBase protected: bool Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper) override; void Shutdown(HWDrawInfo *di, FRenderState &rstate) override; + void DrawPortalStencil(FRenderState &state, int pass) override; virtual void * GetSource() const { return origin; } virtual const char *GetName(); secplane_t * origin; diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index 93fc827cc..4610f00da 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -154,7 +154,7 @@ void HWWall::SkyPlane(HWWallDispatcher *di, sector_t *sector, int plane, bool al case PORTS_PORTAL: case PORTS_LINKEDPORTAL: { - if (di->di && di->di->Viewpoint.IsAllowedOoB()) return; + if (di->di && di->di->Viewpoint.IsAllowedOoB()) return; // Almost works (with planemirrorportal stencil), but no quite auto glport = sector->GetPortalGroup(plane); if (glport != NULL) { diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index 47e651bd4..d5271d05f 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -644,7 +644,8 @@ void HWWall::PutPortal(HWWallDispatcher *di, int ptype, int plane) break; case PORTALTYPE_PLANEMIRROR: - if (portalState.PlaneMirrorMode * planemirror->fC() <= 0) + if (ddi->Viewpoint.IsOrtho() ? (ddi->Viewpoint.ViewVector3D.dot(planemirror->Normal()) < 0) + : (portalState.PlaneMirrorMode * planemirror->fC() <= 0)) { planemirror = portalState.UniquePlaneMirrors.Get(planemirror); portal = ddi->FindPortal(planemirror); From 2c4ac886aa0f71fd30d3ae13a7c826299a6cea8a Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Mon, 17 Feb 2025 19:23:26 -0700 Subject: [PATCH 045/384] Stacked sector portals now render for OoB viewpoints. --- src/rendering/hwrenderer/scene/hw_portal.cpp | 27 +++++++++++++++++++- src/rendering/hwrenderer/scene/hw_portal.h | 1 + src/rendering/hwrenderer/scene/hw_sky.cpp | 7 +++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index 01ab23b8a..7746aa338 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -805,10 +805,18 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c vp.Pos += origin->mDisplacement; vp.ActorPos += origin->mDisplacement; vp.ViewActor = nullptr; + vp.OffPos += origin->mDisplacement; // avoid recursions! if (origin->plane != -1) screen->instack[origin->plane]++; - + if (lines.Size() > 0) + { + flat.plane.GetFromSector(lines[0].sub->sector, + lines[0].sub->sector->GetPortal(sector_t::ceiling)->mType & (PORTS_STACKEDSECTORTHING | PORTS_PORTAL | PORTS_LINKEDPORTAL) ? + sector_t::ceiling : sector_t::floor); + di->SetClipHeight(flat.plane.plane.ZatPoint(vp.Pos), + flat.plane.plane.Normal().Z > 0 ? -1.f : 1.f); + } di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1)); SetupCoverage(di); ClearClipper(di, clipper); @@ -816,6 +824,7 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c // If the viewpoint is not within the portal, we need to invalidate the entire clip area. // The portal will re-validate the necessary parts when its subsectors get traversed. subsector_t *sub = di->Level->PointInRenderSubsector(vp.Pos); + if (vp.IsAllowedOoB()) sub = di->Level->PointInRenderSubsector(vp.OffPos); if (!(di->ss_renderflags[sub->Index()] & SSRF_SEEN)) { clipper->SafeAddClipRange(0, ANGLE_MAX); @@ -825,6 +834,22 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c } +void HWSectorStackPortal::DrawPortalStencil(FRenderState &state, int pass) +{ + bool isceiling = planesused & (1 << sector_t::ceiling); + for (unsigned int i = 0; i < lines.Size(); i++) + { + flat.section = lines[i].sub->section; + flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; + flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); + // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection + + state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); + state.DrawIndexed(DT_Triangles, flat.iboindex + flat.section->vertexindex, flat.section->vertexcount, i == 0); + } +} + + void HWSectorStackPortal::Shutdown(HWDrawInfo *di, FRenderState &rstate) { if (origin->plane != -1) screen->instack[origin->plane]--; diff --git a/src/rendering/hwrenderer/scene/hw_portal.h b/src/rendering/hwrenderer/scene/hw_portal.h index ab9fec945..50034df6a 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.h +++ b/src/rendering/hwrenderer/scene/hw_portal.h @@ -270,6 +270,7 @@ struct HWSectorStackPortal : public HWScenePortalBase TArray subsectors; protected: bool Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper) override; + void DrawPortalStencil(FRenderState &state, int pass) override; void Shutdown(HWDrawInfo *di, FRenderState &rstate) override; virtual void * GetSource() const { return origin; } virtual bool IsSky() { return true; } // although this isn't a real sky it can be handled as one. diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index 4610f00da..b39df6695 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -154,12 +154,15 @@ void HWWall::SkyPlane(HWWallDispatcher *di, sector_t *sector, int plane, bool al case PORTS_PORTAL: case PORTS_LINKEDPORTAL: { - if (di->di && di->di->Viewpoint.IsAllowedOoB()) return; // Almost works (with planemirrorportal stencil), but no quite + if (di->di && di->di->Viewpoint.IsAllowedOoB()) + { + secplane_t myplane = plane ? sector->ceilingplane : sector->floorplane; + if (di->di->Viewpoint.ViewVector3D.dot(myplane.Normal()) > 0.0) return; + } auto glport = sector->GetPortalGroup(plane); if (glport != NULL) { if (sector->PortalBlocksView(plane)) return; - if (di->di && screen->instack[1 - plane]) return; ptype = PORTALTYPE_SECTORSTACK; portal = glport; From 093d3e32e61a3da93c8ff2b2dece1a4b458a2fc0 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Mon, 17 Feb 2025 20:18:33 -0700 Subject: [PATCH 046/384] Small correction to OoB viewpoint stacked-sector portal visibility. OoB is not the same as Ortho. --- src/rendering/hwrenderer/scene/hw_sky.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index b39df6695..2aef16c8f 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -157,7 +157,9 @@ void HWWall::SkyPlane(HWWallDispatcher *di, sector_t *sector, int plane, bool al if (di->di && di->di->Viewpoint.IsAllowedOoB()) { secplane_t myplane = plane ? sector->ceilingplane : sector->floorplane; - if (di->di->Viewpoint.ViewVector3D.dot(myplane.Normal()) > 0.0) return; + if (di->di->Viewpoint.IsOrtho() && di->di->Viewpoint.ViewVector3D.dot(myplane.Normal()) > 0.0) return; + else if (plane==1 && di->di->Viewpoint.Pos.Z >= myplane.ZatPoint(di->di->Viewpoint.Pos)) return; + else if (plane==0 && di->di->Viewpoint.Pos.Z <= myplane.ZatPoint(di->di->Viewpoint.Pos)) return; } auto glport = sector->GetPortalGroup(plane); if (glport != NULL) From b80572e400dc23cad9a79ac9577f4cca49cc1410 Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:32:19 +0200 Subject: [PATCH 047/384] Exposed DDoor to ZScript. Also added a ZScript-only enum for the movement direction.. --- src/playsim/mapthinkers/a_doors.cpp | 12 ++++++++++ src/playsim/mapthinkers/a_doors.h | 14 +++++------ wadsrc/static/zscript/doombase.zs | 36 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/playsim/mapthinkers/a_doors.cpp b/src/playsim/mapthinkers/a_doors.cpp index 2d8d2c70c..e32068565 100644 --- a/src/playsim/mapthinkers/a_doors.cpp +++ b/src/playsim/mapthinkers/a_doors.cpp @@ -40,6 +40,7 @@ #include "g_levellocals.h" #include "animations.h" #include "texturemanager.h" +#include "vm.h" //============================================================================ // @@ -64,6 +65,17 @@ void DDoor::Serialize(FSerializer &arc) ("lighttag", m_LightTag); } +DEFINE_FIELD(DDoor, m_Type) +DEFINE_FIELD(DDoor, m_TopDist) +DEFINE_FIELD(DDoor, m_BotSpot) +DEFINE_FIELD(DDoor, m_BotDist) +DEFINE_FIELD(DDoor, m_OldFloorDist) +DEFINE_FIELD(DDoor, m_Speed) +DEFINE_FIELD(DDoor, m_Direction) +DEFINE_FIELD(DDoor, m_TopWait) +DEFINE_FIELD(DDoor, m_TopCountdown) +DEFINE_FIELD(DDoor, m_LightTag) + //============================================================================ // // T_VerticalDoor diff --git a/src/playsim/mapthinkers/a_doors.h b/src/playsim/mapthinkers/a_doors.h index 88c0c5667..e815b55ae 100644 --- a/src/playsim/mapthinkers/a_doors.h +++ b/src/playsim/mapthinkers/a_doors.h @@ -17,16 +17,10 @@ public: doorWaitClose, }; - void Construct(sector_t *sector); - void Construct(sector_t *sec, EVlDoor type, double speed, int delay, int lightTag, int topcountdown); - - void Serialize(FSerializer &arc); - void Tick (); -protected: EVlDoor m_Type; double m_TopDist; double m_BotDist, m_OldFloorDist; - vertex_t *m_BotSpot; + vertex_t* m_BotSpot; double m_Speed; // 1 = up, 0 = waiting at top, -1 = down @@ -40,6 +34,12 @@ protected: int m_LightTag; + void Construct(sector_t *sector); + void Construct(sector_t *sec, EVlDoor type, double speed, int delay, int lightTag, int topcountdown); + + void Serialize(FSerializer &arc); + void Tick (); +protected: void DoorSound (bool raise, class DSeqNode *curseq=NULL) const; private: diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index bb76cfb65..181bdae04 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -649,6 +649,42 @@ class MovingFloor : Mover native class MovingCeiling : Mover native {} +class Door : MovingCeiling native +{ + enum EVlDoor + { + doorClose, + doorOpen, + doorRaise, + doorWaitRaise, + doorCloseWaitOpen, + doorWaitClose, + }; + + native readonly EVlDoor m_Type; + native readonly double m_TopDist; + native readonly double m_BotDist, m_OldFloorDist; + native readonly Vertex m_BotSpot; + native readonly double m_Speed; + + // 1 = up, 0 = waiting at top, -1 = down + enum EDirection + { + dirUp, + dirWait, + dirDown + } + native readonly int m_Direction; + + // tics to wait at the top + native readonly int m_TopWait; + // (keep in case a door going down is reset) + // when it reaches 0, start going down + native readonly int m_TopCountdown; + + native readonly int m_LightTag; +} + class Floor : MovingFloor native { // only here so that some constants and functions can be added. Not directly usable yet. From b0e7a698f6cca4021d151499488511a6b1e06289 Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:41:27 +0200 Subject: [PATCH 048/384] Exposed DPlat to ZScript. --- src/playsim/mapthinkers/a_plats.cpp | 12 +++++++++ src/playsim/mapthinkers/a_plats.h | 3 +-- wadsrc/static/zscript/doombase.zs | 41 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/playsim/mapthinkers/a_plats.cpp b/src/playsim/mapthinkers/a_plats.cpp index c688295e4..fb41a6559 100644 --- a/src/playsim/mapthinkers/a_plats.cpp +++ b/src/playsim/mapthinkers/a_plats.cpp @@ -36,6 +36,7 @@ #include "serializer.h" #include "p_spec.h" #include "g_levellocals.h" +#include "vm.h" static FRandom pr_doplat ("DoPlat"); @@ -62,6 +63,17 @@ void DPlat::Serialize(FSerializer &arc) ("tag", m_Tag); } +DEFINE_FIELD(DPlat, m_Type) +DEFINE_FIELD(DPlat, m_Speed) +DEFINE_FIELD(DPlat, m_Low) +DEFINE_FIELD(DPlat, m_High) +DEFINE_FIELD(DPlat, m_Wait) +DEFINE_FIELD(DPlat, m_Count) +DEFINE_FIELD(DPlat, m_Status) +DEFINE_FIELD(DPlat, m_OldStatus) +DEFINE_FIELD(DPlat, m_Crush) +DEFINE_FIELD(DPlat, m_Tag) + //----------------------------------------------------------------------------- // // diff --git a/src/playsim/mapthinkers/a_plats.h b/src/playsim/mapthinkers/a_plats.h index fd8d4e975..2427021ef 100644 --- a/src/playsim/mapthinkers/a_plats.h +++ b/src/playsim/mapthinkers/a_plats.h @@ -38,8 +38,6 @@ public: bool IsLift() const { return m_Type == platDownWaitUpStay || m_Type == platDownWaitUpStayStone; } void Construct(sector_t *sector); -protected: - double m_Speed; double m_Low; double m_High; @@ -50,6 +48,7 @@ protected: int m_Crush; int m_Tag; EPlatType m_Type; +protected: void PlayPlatSound (const char *sound); const char *GetSoundByType () const; diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 181bdae04..c1de2203b 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -646,6 +646,47 @@ class Mover : SectorEffect native class MovingFloor : Mover native {} +class Plat : MovingFloor native +{ + enum EPlatState + { + up, + down, + waiting, + in_stasis + }; + + enum EPlatType + { + platPerpetualRaise, + platDownWaitUpStay, + platDownWaitUpStayStone, + platUpWaitDownStay, + platUpNearestWaitDownStay, + platDownByValue, + platUpByValue, + platUpByValueStay, + platRaiseAndStay, + platToggle, + platDownToNearestFloor, + platDownToLowestCeiling, + platRaiseAndStayLockout, + }; + + bool IsLift() const { return m_Type == platDownWaitUpStay || m_Type == platDownWaitUpStayStone; } + + native double m_Speed; + native double m_Low; + native double m_High; + native int m_Wait; + native int m_Count; + native EPlatState m_Status; + native readonly EPlatState m_OldStatus; + native int m_Crush; + native int m_Tag; + native EPlatType m_Type; +} + class MovingCeiling : Mover native {} From 8299f91cd17f315a4bcdaa2930a679565945e594 Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Sat, 22 Feb 2025 15:07:45 +0200 Subject: [PATCH 049/384] Exposed more of the Ceiling thinker. - Exposed the rest of the ceiling member fields and getters. - Added an IsCrusher() method. - Added getOldDirection() getter. - Fixed Door direction enum. - Forgot to make Plat readonly on previous commit. --- src/playsim/mapthinkers/a_ceiling.cpp | 25 +++++++++++++++ src/playsim/mapthinkers/a_ceiling.h | 17 +++++----- wadsrc/static/zscript/doombase.zs | 46 ++++++++++++++++++++------- 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/playsim/mapthinkers/a_ceiling.cpp b/src/playsim/mapthinkers/a_ceiling.cpp index 1afdf4509..c2205fca9 100644 --- a/src/playsim/mapthinkers/a_ceiling.cpp +++ b/src/playsim/mapthinkers/a_ceiling.cpp @@ -72,6 +72,31 @@ void DCeiling::Serialize(FSerializer &arc) .Enum("crushmode", m_CrushMode); } +DEFINE_FIELD(DCeiling, m_Type) +DEFINE_FIELD(DCeiling, m_BottomHeight) +DEFINE_FIELD(DCeiling, m_TopHeight) +DEFINE_FIELD(DCeiling, m_Speed) +DEFINE_FIELD(DCeiling, m_Speed1) +DEFINE_FIELD(DCeiling, m_Speed2) +DEFINE_FIELD(DCeiling, m_Silent) +DEFINE_FIELD(DCeiling, m_CrushMode) + +DEFINE_ACTION_FUNCTION(DCeiling, getCrush) +{ + PARAM_SELF_PROLOGUE(DCeiling); + ACTION_RETURN_INT(self->getCrush()); +} +DEFINE_ACTION_FUNCTION(DCeiling, getDirection) +{ + PARAM_SELF_PROLOGUE(DCeiling); + ACTION_RETURN_INT(self->getDirection()); +} +DEFINE_ACTION_FUNCTION(DCeiling, getOldDirection) +{ + PARAM_SELF_PROLOGUE(DCeiling); + ACTION_RETURN_INT(self->getOldDirection()); +} + //============================================================================ // // diff --git a/src/playsim/mapthinkers/a_ceiling.h b/src/playsim/mapthinkers/a_ceiling.h index 2954fb118..92b0c22bb 100644 --- a/src/playsim/mapthinkers/a_ceiling.h +++ b/src/playsim/mapthinkers/a_ceiling.h @@ -47,6 +47,14 @@ public: crushSlowdown = 2 }; + ECeiling m_Type; + double m_BottomHeight; + double m_TopHeight; + double m_Speed; + double m_Speed1; // [RH] dnspeed of crushers + double m_Speed2; // [RH] upspeed of crushers + ECrushMode m_CrushMode; + int m_Silent; void Construct(sector_t *sec); void Construct(sector_t *sec, double speed1, double speed2, int silent); @@ -56,17 +64,10 @@ public: int getCrush() const { return m_Crush; } int getDirection() const { return m_Direction; } + int getOldDirection() const { return m_OldDirection; } protected: - ECeiling m_Type; - double m_BottomHeight; - double m_TopHeight; - double m_Speed; - double m_Speed1; // [RH] dnspeed of crushers - double m_Speed2; // [RH] upspeed of crushers int m_Crush; - ECrushMode m_CrushMode; - int m_Silent; int m_Direction; // 1 = up, 0 = waiting, -1 = down // [RH] Need these for BOOM-ish transferring ceilings diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index c1de2203b..0f153628a 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -675,16 +675,16 @@ class Plat : MovingFloor native bool IsLift() const { return m_Type == platDownWaitUpStay || m_Type == platDownWaitUpStayStone; } - native double m_Speed; - native double m_Low; - native double m_High; - native int m_Wait; - native int m_Count; - native EPlatState m_Status; + native readonly double m_Speed; + native readonly double m_Low; + native readonly double m_High; + native readonly int m_Wait; + native readonly int m_Count; + native readonly EPlatState m_Status; native readonly EPlatState m_OldStatus; - native int m_Crush; - native int m_Tag; - native EPlatType m_Type; + native readonly int m_Crush; + native readonly int m_Tag; + native readonly EPlatType m_Type; } class MovingCeiling : Mover native @@ -711,9 +711,9 @@ class Door : MovingCeiling native // 1 = up, 0 = waiting at top, -1 = down enum EDirection { - dirUp, + dirDown = -1, dirWait, - dirDown + dirUp, } native readonly int m_Direction; @@ -813,7 +813,29 @@ class Ceiling : MovingCeiling native crushHexen = 1, crushSlowdown = 2 } - + + // 1 = up, 0 = waiting, -1 = down + enum EDirection + { + dirDown = -1, + dirWait, + dirUp, + } + + native readonly ECeiling m_Type; + native readonly double m_BottomHeight; + native readonly double m_TopHeight; + native readonly double m_Speed; + native readonly double m_Speed1; // [RH] dnspeed of crushers + native readonly double m_Speed2; // [RH] upspeed of crushers + native readonly ECrushMode m_CrushMode; + native readonly int m_Silent; + + bool IsCrusher() const { return m_Type == ceilCrushAndRaise || m_Type == ceilLowerAndCrush || m_Type == ceilCrushRaiseAndStay; } + native int getCrush() const; + native int getDirection() const; + native int getOldDirection() const; + deprecated("3.8", "Use Level.CreateCeiling() instead") static bool CreateCeiling(sector sec, int type, line ln, double speed, double speed2, double height = 0, int crush = -1, int silent = 0, int change = 0, int crushmode = crushDoom) { return level.CreateCeiling(sec, type, ln, speed, speed2, height, crush, silent, change, crushmode); From f0d0f259f75e47d5a8484edc2cafddcb4386b23b Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Sun, 23 Feb 2025 05:02:10 +0200 Subject: [PATCH 050/384] Exposed more of the Floor thinker. --- src/playsim/mapthinkers/a_floor.cpp | 16 +++++++++++++++ wadsrc/static/zscript/doombase.zs | 31 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/playsim/mapthinkers/a_floor.cpp b/src/playsim/mapthinkers/a_floor.cpp index 0d330e4e9..e81c10713 100644 --- a/src/playsim/mapthinkers/a_floor.cpp +++ b/src/playsim/mapthinkers/a_floor.cpp @@ -96,6 +96,22 @@ void DFloor::Serialize(FSerializer &arc) ("instant", m_Instant); } +DEFINE_FIELD(DFloor, m_Type) +DEFINE_FIELD(DFloor, m_Crush) +DEFINE_FIELD(DFloor, m_Direction) +DEFINE_FIELD(DFloor, m_NewSpecial) +DEFINE_FIELD(DFloor, m_Texture) +DEFINE_FIELD(DFloor, m_FloorDestDist) +DEFINE_FIELD(DFloor, m_Speed) +DEFINE_FIELD(DFloor, m_ResetCount) +DEFINE_FIELD(DFloor, m_OrgDist) +DEFINE_FIELD(DFloor, m_Delay) +DEFINE_FIELD(DFloor, m_PauseTime) +DEFINE_FIELD(DFloor, m_StepTime) +DEFINE_FIELD(DFloor, m_PerStepTime) +DEFINE_FIELD(DFloor, m_Hexencrush) +DEFINE_FIELD(DFloor, m_Instant) + //========================================================================== // // MOVE A FLOOR TO ITS DESTINATION (UP OR DOWN) diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 0f153628a..46fdc4239 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -768,6 +768,37 @@ class Floor : MovingFloor native genFloorChg }; + enum EStair + { + buildUp, + buildDown + }; + + enum EStairType + { + stairUseSpecials = 1, + stairSync = 2, + stairCrush = 4, + }; + + native readonly EFloor m_Type; + native readonly int m_Crush; + native readonly bool m_Hexencrush; + native readonly bool m_Instant; + native readonly int m_Direction; + native readonly SecSpecial m_NewSpecial; + native readonly TextureID m_Texture; + native readonly double m_FloorDestDist; + native readonly double m_Speed; + + // [RH] New parameters used to reset and delay stairs + native readonly double m_OrgDist; + native readonly int m_ResetCount; + native readonly int m_Delay; + native readonly int m_PauseTime; + native readonly int m_StepTime; + native readonly int m_PerStepTime; + deprecated("3.8", "Use Level.CreateFloor() instead") static bool CreateFloor(sector sec, int floortype, line ln, double speed, double height = 0, int crush = -1, int change = 0, bool crushmode = false, bool hereticlower = false) { return level.CreateFloor(sec, floortype, ln, speed, height, crush, change, crushmode, hereticlower); From ecdfe39ddf74fc43a849f47bdcda3883d9304c1a Mon Sep 17 00:00:00 2001 From: inkoalawetrust <56005600+inkoalawetrust@users.noreply.github.com> Date: Sun, 23 Feb 2025 05:19:16 +0200 Subject: [PATCH 051/384] Exposed DElevator to ZScript. --- src/playsim/mapthinkers/a_floor.cpp | 8 +++++++- src/playsim/mapthinkers/a_floor.h | 12 +++++++----- wadsrc/static/zscript/doombase.zs | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/playsim/mapthinkers/a_floor.cpp b/src/playsim/mapthinkers/a_floor.cpp index e81c10713..d4492d9e0 100644 --- a/src/playsim/mapthinkers/a_floor.cpp +++ b/src/playsim/mapthinkers/a_floor.cpp @@ -903,6 +903,12 @@ void DElevator::Serialize(FSerializer &arc) ("interp_ceiling", m_Interp_Ceiling); } +DEFINE_FIELD(DElevator, m_Type) +DEFINE_FIELD(DElevator, m_Direction) +DEFINE_FIELD(DElevator, m_FloorDestDist) +DEFINE_FIELD(DElevator, m_CeilingDestDist) +DEFINE_FIELD(DElevator, m_Speed) + //========================================================================== // // @@ -973,7 +979,7 @@ void DElevator::Tick () } } - if (res == EMoveResult::pastdest) // if destination height acheived + if (res == EMoveResult::pastdest) // if destination height achieved { // make floor stop sound SN_StopSequence (m_Sector, CHAN_FLOOR); diff --git a/src/playsim/mapthinkers/a_floor.h b/src/playsim/mapthinkers/a_floor.h index e0331bca6..891ce948e 100644 --- a/src/playsim/mapthinkers/a_floor.h +++ b/src/playsim/mapthinkers/a_floor.h @@ -105,6 +105,13 @@ public: elevateLower }; + EElevator m_Type; + int m_Direction; + double m_FloorDestDist; + double m_CeilingDestDist; + double m_Speed; + + void Construct(sector_t *sec); void OnDestroy() override; @@ -112,11 +119,6 @@ public: void Tick (); protected: - EElevator m_Type; - int m_Direction; - double m_FloorDestDist; - double m_CeilingDestDist; - double m_Speed; TObjPtr m_Interp_Ceiling; TObjPtr m_Interp_Floor; diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 46fdc4239..c4582c15e 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -643,6 +643,25 @@ class SectorEffect : Thinker native class Mover : SectorEffect native {} +class Elevator : Mover native +{ + enum EElevator + { + elevateUp, + elevateDown, + elevateCurrent, + // [RH] For FloorAndCeiling_Raise/Lower + elevateRaise, + elevateLower + }; + + native readonly EElevator m_Type; + native readonly int m_Direction; + native readonly double m_FloorDestDist; + native readonly double m_CeilingDestDist; + native readonly double m_Speed; +} + class MovingFloor : Mover native {} From b0889b98147612f1a4363561b763025ba76540df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 6 Mar 2025 08:59:02 -0300 Subject: [PATCH 052/384] Fix deprecation version checks on class fields/pointers --- src/common/scripting/core/symbols.cpp | 2 +- src/common/scripting/core/types.cpp | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/scripting/core/symbols.cpp b/src/common/scripting/core/symbols.cpp index d293cbbee..7544a4561 100644 --- a/src/common/scripting/core/symbols.cpp +++ b/src/common/scripting/core/symbols.cpp @@ -170,7 +170,7 @@ VersionInfo PField::GetVersion() { VersionInfo Highest = { 0,0,0 }; if (!(Flags & VARF_Deprecated)) Highest = mVersion; - if (Type->mVersion > Highest) Highest = Type->mVersion; + if (Type->mVersion > Highest && !Type->TypeDeprecated) Highest = Type->mVersion; return Highest; } diff --git a/src/common/scripting/core/types.cpp b/src/common/scripting/core/types.cpp index 20ce7e05d..271567986 100644 --- a/src/common/scripting/core/types.cpp +++ b/src/common/scripting/core/types.cpp @@ -1493,6 +1493,7 @@ PPointer::PPointer(PType *pointsat, bool isconst) { mDescriptiveName.Format("Pointer<%s%s>", pointsat->DescriptiveName(), isconst ? "readonly " : ""); mVersion = pointsat->mVersion; + TypeDeprecated = pointsat->TypeDeprecated; } else { @@ -1674,7 +1675,11 @@ PClassPointer::PClassPointer(PClass *restrict) loadOp = OP_LP; storeOp = OP_SP; Flags |= TYPE_ClassPointer; - if (restrict) mVersion = restrict->VMType->mVersion; + if (restrict) + { + mVersion = restrict->VMType->mVersion; + TypeDeprecated = restrict->VMType->TypeDeprecated; + } else mVersion = 0; } From 0b30b4a4935d32eceff011a017ce9d9e5e45f941 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Thu, 6 Mar 2025 10:55:11 -0700 Subject: [PATCH 053/384] Revert using older stencil method for stacked sectors (and reflective flats) if viewpoint is not allowed OoB. There was some bug with nearby skyplanes otherwise. --- src/rendering/hwrenderer/scene/hw_portal.cpp | 51 ++++++++++++++------ src/rendering/hwrenderer/scene/hw_portal.h | 4 +- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index 7746aa338..6877f7982 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -507,6 +507,7 @@ bool HWMirrorPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clippe } auto &vp = di->Viewpoint; + state->vpIsAllowedOoB = vp.IsAllowedOoB(); di->UpdateCurrentMapSection(); di->mClipPortal = this; @@ -611,6 +612,7 @@ bool HWLineToLinePortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *cl return false; } auto &vp = di->Viewpoint; + state->vpIsAllowedOoB = vp.IsAllowedOoB(); di->mClipPortal = this; line_t *origin = glport->lines[0]->mOrigin; @@ -690,6 +692,7 @@ bool HWSkyboxPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clippe return false; } auto &vp = di->Viewpoint; + state->vpIsAllowedOoB = vp.IsAllowedOoB(); state->skyboxrecursion++; state->PlaneMirrorMode = 0; @@ -801,6 +804,7 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c FSectorPortalGroup *portal = origin; auto &vp = di->Viewpoint; + state->vpIsAllowedOoB = vp.IsAllowedOoB(); vp.Pos += origin->mDisplacement; vp.ActorPos += origin->mDisplacement; @@ -836,16 +840,23 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c void HWSectorStackPortal::DrawPortalStencil(FRenderState &state, int pass) { - bool isceiling = planesused & (1 << sector_t::ceiling); - for (unsigned int i = 0; i < lines.Size(); i++) + if (mState->vpIsAllowedOoB) { - flat.section = lines[i].sub->section; - flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; - flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); - // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection + bool isceiling = planesused & (1 << sector_t::ceiling); + for (unsigned int i = 0; i < lines.Size(); i++) + { + flat.section = lines[i].sub->section; + flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; + flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); + // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection - state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); - state.DrawIndexed(DT_Triangles, flat.iboindex + flat.section->vertexindex, flat.section->vertexcount, i == 0); + state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); + state.DrawIndexed(DT_Triangles, flat.iboindex + flat.section->vertexindex, flat.section->vertexcount, i == 0); + } + } + else + { + HWPortal::DrawPortalStencil(state, pass); } } @@ -884,6 +895,7 @@ bool HWPlaneMirrorPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c std::swap(screen->instack[sector_t::floor], screen->instack[sector_t::ceiling]); auto &vp = di->Viewpoint; + state->vpIsAllowedOoB = vp.IsAllowedOoB(); old_pm = state->PlaneMirrorMode; // the player is always visible in a mirror. @@ -908,16 +920,23 @@ bool HWPlaneMirrorPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c void HWPlaneMirrorPortal::DrawPortalStencil(FRenderState &state, int pass) { - bool isceiling = planesused & (1 << sector_t::ceiling); - for (unsigned int i = 0; i < lines.Size(); i++) + if (mState->vpIsAllowedOoB) { - flat.section = lines[i].sub->section; - flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; - flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); - // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection + bool isceiling = planesused & (1 << sector_t::ceiling); + for (unsigned int i = 0; i < lines.Size(); i++) + { + flat.section = lines[i].sub->section; + flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; + flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); + // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection - state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); - state.DrawIndexed(DT_Triangles, flat.iboindex + flat.section->vertexindex, flat.section->vertexcount, i == 0); + state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); + state.DrawIndexed(DT_Triangles, flat.iboindex + flat.section->vertexindex, flat.section->vertexcount, i == 0); + } + } + else + { + HWPortal::DrawPortalStencil(state, pass); } } diff --git a/src/rendering/hwrenderer/scene/hw_portal.h b/src/rendering/hwrenderer/scene/hw_portal.h index 50034df6a..207cefc2f 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.h +++ b/src/rendering/hwrenderer/scene/hw_portal.h @@ -59,8 +59,6 @@ class HWPortal TArray mPrimIndices; unsigned int mTopCap = ~0u, mBottomCap = ~0u; - virtual void DrawPortalStencil(FRenderState &state, int pass); - public: FPortalSceneState * mState; TArray lines; @@ -84,6 +82,7 @@ public: virtual bool NeedDepthBuffer() { return true; } virtual void DrawContents(HWDrawInfo *di, FRenderState &state) = 0; virtual void RenderAttached(HWDrawInfo *di) {} + virtual void DrawPortalStencil(FRenderState &state, int pass); void SetupStencil(HWDrawInfo *di, FRenderState &state, bool usestencil); void RemoveStencil(HWDrawInfo *di, FRenderState &state, bool usestencil); @@ -106,6 +105,7 @@ struct FPortalSceneState int PlaneMirrorMode = 0; bool inskybox = 0; + bool vpIsAllowedOoB = 0; UniqueList UniqueSkies; UniqueList UniqueHorizons; From 2e05e196dbacb7783f22a88fd3460018fce2d5a4 Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Fri, 7 Mar 2025 13:47:56 +0800 Subject: [PATCH 054/384] Interpolate turning 180 degrees --- wadsrc/static/zscript/actors/player/player.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index c91410b0b..1c87c515c 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -1306,7 +1306,7 @@ class PlayerPawn : Actor if (player.turnticks) { player.turnticks--; - Angle += (180. / TURN180_TICKS); + A_SetAngle(Angle + (180. / TURN180_TICKS), SPF_INTERPOLATE); } else { From c82c6bff164a11089e81a2c1b09bbdb742eca749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 7 Mar 2025 17:22:24 -0300 Subject: [PATCH 055/384] fix function-pointer cast parsing --- src/common/scripting/frontend/zcc-parse.lemon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/scripting/frontend/zcc-parse.lemon b/src/common/scripting/frontend/zcc-parse.lemon index 2affe274b..67a38eff7 100644 --- a/src/common/scripting/frontend/zcc-parse.lemon +++ b/src/common/scripting/frontend/zcc-parse.lemon @@ -1553,7 +1553,7 @@ primary(X) ::= LPAREN CLASS LT IDENTIFIER(A) GT RPAREN LPAREN func_expr_list(B) X = expr; } -primary(X) ::= LPAREN func_ptr_type(A) RPAREN LPAREN expr(B) RPAREN. [DOT] // function pointer type cast +primary(X) ::= LPAREN func_ptr_type(A) GT RPAREN LPAREN expr(B) RPAREN. [DOT] // function pointer type cast { NEW_AST_NODE(FunctionPtrCast, expr, A); expr->Operation = PEX_FunctionPtrCast; From 80d5450af95b5287f931a263474ffc9610728f7f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 6 Mar 2025 19:41:40 -0500 Subject: [PATCH 056/384] Updated ZWidget ListView Added column support for consistent spacing between elements. Improved item adding functionality. Added Update and Remove item functionalities. Update ListView scrollbar on item add/remove. --- .../zwidget/widgets/listview/listview.h | 10 +- .../ZWidget/src/widgets/listview/listview.cpp | 92 ++++++++++++++++++- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/libraries/ZWidget/include/zwidget/widgets/listview/listview.h b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h index 2b150abc0..550cc07f9 100644 --- a/libraries/ZWidget/include/zwidget/widgets/listview/listview.h +++ b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h @@ -12,7 +12,12 @@ class ListView : public Widget public: ListView(Widget* parent = nullptr); - void AddItem(const std::string& text); + void SetColumnWidths(const std::vector& widths); + void AddItem(const std::string& text, int index = -1, int column = 0); + void UpdateItem(const std::string& text, int index, int column = 0); + void RemoveItem(int index = -1); + size_t GetItemAmount() const { return items.size(); } + size_t GetColumnAmount() const { return columnwidths.size(); } int GetSelectedItem() const { return selectedItem; } void SetSelectedItem(int index); void ScrollToItem(int index); @@ -34,6 +39,7 @@ protected: Scrollbar* scrollbar = nullptr; - std::vector items; + std::vector> items; + std::vector columnwidths; int selectedItem = 0; }; diff --git a/libraries/ZWidget/src/widgets/listview/listview.cpp b/libraries/ZWidget/src/widgets/listview/listview.cpp index 717f95827..a8dd066d0 100644 --- a/libraries/ZWidget/src/widgets/listview/listview.cpp +++ b/libraries/ZWidget/src/widgets/listview/listview.cpp @@ -8,11 +8,90 @@ ListView::ListView(Widget* parent) : Widget(parent) scrollbar = new Scrollbar(this); scrollbar->FuncScroll = [=]() { OnScrollbarScroll(); }; + + SetColumnWidths({ 0.0 }); } -void ListView::AddItem(const std::string& text) +void ListView::SetColumnWidths(const std::vector& widths) { - items.push_back(text); + columnwidths = widths; + + bool updated = false; + const size_t newWidth = columnwidths.size(); + for (std::vector& column : items) + { + while (column.size() < newWidth) + { + updated = true; + column.push_back(""); + } + while (column.size() > newWidth) + { + updated = true; + column.erase(column.end()); + } + } + + if (updated) + Update(); +} + +void ListView::AddItem(const std::string& text, int index, int column) +{ + if (column < 0 || column >= columnwidths.size()) + return; + + std::vector newEntry; + for (size_t i = 0u; i < columnwidths.size(); ++i) + newEntry.push_back(""); + + newEntry[column] = text; + if (index >= 0) + { + if (index >= items.size()) + { + newEntry[column] = ""; + while (items.size() < index) + items.push_back(newEntry); + + newEntry[column] = text; + items.push_back(newEntry); + } + else + { + items.insert(items.begin() + index, newEntry); + } + } + else + { + items.push_back(newEntry); + } + scrollbar->SetRanges(GetHeight(), items.size() * 20.0); + Update(); +} + +void ListView::UpdateItem(const std::string& text, int index, int column) +{ + if (index < 0 || index >= items.size() || column < 0 || column >= columnwidths.size()) + return; + + items[index][column] = text; + Update(); +} + +void ListView::RemoveItem(int index) +{ + if (!items.size() || index >= items.size()) + return; + + if (index < 0) + index = items.size() - 1; + + if (selectedItem == index) + SetSelectedItem(0); + + items.erase(items.begin() + index); + scrollbar->SetRanges(GetHeight(), items.size() * 20.0); Update(); } @@ -68,7 +147,7 @@ void ListView::OnPaint(Canvas* canvas) double h = 20.0; int index = 0; - for (const std::string& item : items) + for (const std::vector& item : items) { double itemY = y; if (itemY + h >= 0.0 && itemY < GetHeight()) @@ -77,7 +156,12 @@ void ListView::OnPaint(Canvas* canvas) { canvas->fillRect(Rect::xywh(x - 2.0, itemY, w, h), Colorf::fromRgba8(100, 100, 100)); } - canvas->drawText(Point(x, y + 15.0), Colorf::fromRgba8(255, 255, 255), item); + double cx = x; + for (size_t entry = 0u; entry < item.size(); ++entry) + { + canvas->drawText(Point(cx, y + 15.0), Colorf::fromRgba8(255, 255, 255), item[entry]); + cx += columnwidths[entry]; + } } y += h; index++; From ad3bcfddbaeba7ae5c80790c15014a7e0746a84b Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 5 Mar 2025 17:52:13 -0500 Subject: [PATCH 057/384] Lobby Overhaul Rewrote lobby to unify common and Doom-specific packet structure, allowing for saner handling of in-game joining. Added a new per-client stage system that allows individual clients to be handled at a time when gathering and sharing info. Reworked lobby UI to display user info and added kick/ban functionalities. Bans are only a temporary per-game IP ban (use passwords to keep unwanted users out). Increased max player count to 64 and unified engine constant. --- src/common/engine/i_net.cpp | 1947 +++++++++-------- src/common/engine/i_net.h | 91 +- src/common/engine/st_start.h | 37 +- src/common/platform/posix/cocoa/st_console.h | 11 +- src/common/platform/posix/cocoa/st_console.mm | 62 +- src/common/platform/posix/cocoa/st_start.mm | 42 +- src/common/platform/posix/sdl/st_start.cpp | 143 +- src/common/platform/win32/i_mainwindow.cpp | 52 +- src/common/platform/win32/i_mainwindow.h | 18 +- src/common/platform/win32/st_start.cpp | 95 +- src/common/widgets/netstartwindow.cpp | 212 +- src/common/widgets/netstartwindow.h | 29 +- src/d_net.cpp | 516 ++--- src/d_net.h | 99 +- src/d_protocol.cpp | 2 +- src/doomdef.h | 4 +- src/doomstat.cpp | 3 - src/doomstat.h | 2 - src/g_game.cpp | 6 +- src/playsim/p_acs.cpp | 2 + wadsrc/static/zscript/constants.zs | 2 +- 21 files changed, 1753 insertions(+), 1622 deletions(-) diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index e85e38789..82668a0b5 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -76,14 +76,9 @@ #include "printf.h" #include "i_interface.h" #include "c_cvars.h" - - +#include "version.h" #include "i_net.h" -// As per http://support.microsoft.com/kb/q192599/ the standard -// size for network buffers is 8k. -#define TRANSMIT_SIZE 8000 - /* [Petteri] Get more portable: */ #ifndef __WIN32__ typedef int SOCKET; @@ -103,14 +98,83 @@ typedef int SOCKET; #ifdef __WIN32__ typedef int socklen_t; +const char* neterror(void); +#else +#define neterror() strerror(errno) #endif -constexpr int MaxPlayers = 16; // TODO: This needs to be put in some kind of unified header later +// As per http://support.microsoft.com/kb/q192599/ the standard +// size for network buffers is 8k. +constexpr size_t MaxTransmitSize = 8000u; +constexpr size_t MinCompressionSize = 10u; constexpr size_t MaxPasswordSize = 256u; -bool netgame, multiplayer; -int consoleplayer; // i.e. myconnectindex in Build. -doomcom_t doomcom; +enum ENetConnectType : uint8_t +{ + PRE_HEARTBEAT, // Host and guests are keep each other's connections alive + PRE_CONNECT, // Sent from guest to host for initial connection + PRE_CONNECT_ACK, // Sent from host to guest to confirm they've been connected + PRE_DISCONNECT, // Sent from host to guest when another guest leaves + PRE_USER_INFO, // Host and guests are sending each other user infos + PRE_USER_INFO_ACK, // Host and guests are confirming sent user infos + PRE_GAME_INFO, // Sent from host to guest containing general game info + PRE_GAME_INFO_ACK, // Sent from guest to host confirming game info was gotten + PRE_GO, // Sent from host to guest telling them to start the game + + PRE_FULL, // Sent from host to guest if the lobby is full + PRE_IN_PROGRESS, // Sent from host to guest if the game has already started + PRE_WRONG_PASSWORD, // Sent from host to guest if their provided password was wrong + PRE_WRONG_ENGINE, // Sent from host to guest if their engine version doesn't match the host's + PRE_INVALID_FILES, // Sent from host to guest if their files do not match the host's + PRE_KICKED, // Sent from hsot to guest if the host kicked them from the game + PRE_BANNED, // Sent from host to guest if the host banned them from the game +}; + +enum EConnectionStatus +{ + CSTAT_NONE, // Guest isn't connected + CSTAT_CONNECTING, // Guest is trying to connect + CSTAT_WAITING, // Guest is waiting for game info + CSTAT_READY, // Guest is ready to start the game +}; + +// These need to be synced with the window backends so information about each +// client can be properly displayed. +enum EConnectionFlags : unsigned int +{ + CFL_NONE = 0, + CFL_CONSOLEPLAYER = 1, + CFL_HOST = 1 << 1, +}; + +struct FConnection +{ + EConnectionStatus Status = CSTAT_NONE; + sockaddr_in Address = {}; + uint64_t InfoAck = 0u; + bool bHasGameInfo = false; +}; + +bool netgame = false; +bool multiplayer = false; +ENetMode NetMode = NET_PeerToPeer; +int consoleplayer = -1; +int Net_Arbitrator = 0; +FClientStack NetworkClients = {}; + +uint32_t GameID = DEFAULT_GAME_ID; +uint8_t TicDup = 1u; +uint8_t MaxClients = 1u; +int RemoteClient = -1; +size_t NetBufferLength = 0u; +uint8_t NetBuffer[MAX_MSGLEN] = {}; + +static u_short GamePort = (IPPORT_USERRESERVED + 29); +static SOCKET MySocket = INVALID_SOCKET; +static FConnection Connected[MAXPLAYERS] = {}; +static uint8_t TransmitBuffer[MaxTransmitSize] = {}; +static TArray BannedConnections = {}; +static bool bGameStarted = false; CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE) { @@ -121,857 +185,115 @@ CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE) } } -FClientStack NetworkClients; +void Net_SetupUserInfo(); +const char* Net_GetClientName(int client, unsigned int charLimit); +int Net_SetUserInfo(int client, uint8_t*& stream); +int Net_ReadUserInfo(int client, uint8_t*& stream); +int Net_ReadGameInfo(uint8_t*& stream); +int Net_SetGameInfo(uint8_t*& stream); -// -// NETWORKING -// - -static u_short DOOMPORT = (IPPORT_USERRESERVED + 29); -static SOCKET mysocket = INVALID_SOCKET; -static sockaddr_in sendaddress[MaxPlayers]; - -#ifdef __WIN32__ -const char *neterror (void); -#else -#define neterror() strerror(errno) -#endif - -enum +static SOCKET CreateUDPSocket() { - PRE_CONNECT, // Sent from guest to host for initial connection - PRE_KEEPALIVE, - PRE_DISCONNECT, // Sent from guest that aborts the game - PRE_ALLHERE, // Sent from host to guest when everybody has connected - PRE_CONACK, // Sent from host to guest to acknowledge PRE_CONNECT receipt - PRE_ALLFULL, // Sent from host to an unwanted guest - PRE_ALLHEREACK, // Sent from guest to host to acknowledge PRE_ALLHEREACK receipt - PRE_GO, // Sent from host to guest to continue game startup - PRE_IN_PROGRESS, // Sent from host to guest if the game has already started - PRE_WRONG_PASSWORD, // Sent from host to guest if their provided password was wrong -}; - -// Set PreGamePacket.fake to this so that the game rejects any pregame packets -// after it starts. This translates to NCMD_SETUP|NCMD_MULTI. -#define PRE_FAKE 0x30 - -struct PreGameConnectPacket -{ - uint8_t Fake; - uint8_t Message; - char Password[MaxPasswordSize]; -}; - -struct PreGamePacket -{ - uint8_t Fake; - uint8_t Message; - uint8_t NumNodes; - union - { - uint8_t ConsoleNum; - uint8_t NumPresent; - }; - struct - { - uint32_t address; - uint16_t port; - uint16_t pad; - } machines[MaxPlayers]; -}; - -uint8_t TransmitBuffer[TRANSMIT_SIZE]; - -FString GetPlayerName(int num) -{ - if (sysCallbacks.GetPlayerName) return sysCallbacks.GetPlayerName(num); - else return FStringf("Player %d", num + 1); -} - -// -// UDPsocket -// -SOCKET UDPsocket (void) -{ - SOCKET s; - - // allocate a socket - s = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + SOCKET s = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (s == INVALID_SOCKET) - I_FatalError ("can't create socket: %s", neterror ()); + I_FatalError("Couldn't create socket: %s", neterror()); return s; } -// -// BindToLocalPort -// -void BindToLocalPort (SOCKET s, u_short port) +static void BindToLocalPort(SOCKET s, u_short port) { - int v; - sockaddr_in address; - - memset (&address, 0, sizeof(address)); + sockaddr_in address = {}; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(port); - v = bind (s, (sockaddr *)&address, sizeof(address)); + int v = bind(s, (sockaddr *)&address, sizeof(address)); if (v == SOCKET_ERROR) - I_FatalError ("BindToPort: %s", neterror ()); + I_FatalError("Couldn't bind to port: %s", neterror()); } -void I_ClearNode(int node) +static void BuildAddress(sockaddr_in& address, const char* addrName) { - memset(&sendaddress[node], 0, sizeof(sendaddress[node])); -} - -static bool I_ShouldStartNetGame() -{ - if (doomcom.consoleplayer != 0) - return false; - - return StartWindow->ShouldStartNet(); -} - -int FindNode (const sockaddr_in *address) -{ - int i = 0; - - // find remote node number - for (; i < doomcom.numplayers; ++i) + FString target = {}; + u_short port = GamePort; + const char* portName = strchr(addrName, ':'); + if (portName != nullptr) { - if (address->sin_addr.s_addr == sendaddress[i].sin_addr.s_addr - && address->sin_port == sendaddress[i].sin_port) - { - break; - } - } - - return (i == doomcom.numplayers) ? -1 : i; -} - -// -// PacketSend -// -void PacketSend (void) -{ - int c; - - // FIXME: Catch this before we've overflown the buffer. With long chat - // text and lots of backup tics, it could conceivably happen. (Though - // apparently it hasn't yet, which is good.) - if (doomcom.datalength > MAX_MSGLEN) - { - I_FatalError("Netbuffer overflow!"); - } - assert(!(doomcom.data[0] & NCMD_COMPRESSED)); - - uLong size = TRANSMIT_SIZE - 1; - if (doomcom.datalength >= 10) - { - TransmitBuffer[0] = doomcom.data[0] | NCMD_COMPRESSED; - c = compress2(TransmitBuffer + 1, &size, doomcom.data + 1, doomcom.datalength - 1, 9); - size += 1; + target = FString(addrName, portName - addrName); + u_short portConversion = atoi(portName + 1); + if (!portConversion) + Printf("Malformed port: %s (using %d)\n", portName + 1, GamePort); + else + port = portConversion; } else { - c = -1; // Just some random error code to avoid sending the compressed buffer. - } - if (c == Z_OK && size < (uLong)doomcom.datalength) - { -// Printf("send %lu/%d\n", size, doomcom.datalength); - c = sendto(mysocket, (char *)TransmitBuffer, size, - 0, (sockaddr *)&sendaddress[doomcom.remoteplayer], - sizeof(sendaddress[doomcom.remoteplayer])); - } - else - { - if (doomcom.datalength > TRANSMIT_SIZE) - { - I_Error("Net compression failed (zlib error %d)", c); - } - else - { -// Printf("send %d\n", doomcom.datalength); - c = sendto(mysocket, (char *)doomcom.data, doomcom.datalength, - 0, (sockaddr *)&sendaddress[doomcom.remoteplayer], - sizeof(sendaddress[doomcom.remoteplayer])); - } - } - // if (c == -1) - // I_Error ("SendPacket error: %s",strerror(errno)); -} - -void PreSend(const void* buffer, int bufferlen, const sockaddr_in* to); -void SendConAck(int num_connected, int num_needed); - -// -// PacketGet -// -void PacketGet (void) -{ - int c; - socklen_t fromlen; - sockaddr_in fromaddress; - int node; - - fromlen = sizeof(fromaddress); - c = recvfrom (mysocket, (char*)TransmitBuffer, TRANSMIT_SIZE, 0, - (sockaddr *)&fromaddress, &fromlen); - node = FindNode (&fromaddress); - - if (node >= 0 && c == SOCKET_ERROR) - { - int err = WSAGetLastError(); - - if (err == WSAECONNRESET) - { // The remote node aborted unexpectedly, so pretend it sent an exit packet - - if (StartWindow != NULL) - { - I_NetMessage ("The connection from %s was dropped.\n", - GetPlayerName(node).GetChars()); - } - else - { - Printf("The connection from %s was dropped.\n", - GetPlayerName(node).GetChars()); - } - - doomcom.data[0] = NCMD_EXIT; - c = 1; - } - else if (err != WSAEWOULDBLOCK) - { - I_Error ("GetPacket: %s", neterror ()); - } - else - { - doomcom.remoteplayer = -1; // no packet - return; - } - } - else if (node >= 0 && c > 0) - { - doomcom.data[0] = TransmitBuffer[0] & ~NCMD_COMPRESSED; - if (TransmitBuffer[0] & NCMD_COMPRESSED) - { - uLongf msgsize = MAX_MSGLEN - 1; - int err = uncompress(doomcom.data + 1, &msgsize, TransmitBuffer + 1, c - 1); -// Printf("recv %d/%lu\n", c, msgsize + 1); - if (err != Z_OK) - { - Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars()); - // Pretend no packet - doomcom.remoteplayer = -1; - return; - } - c = msgsize + 1; - } - else - { -// Printf("recv %d\n", c); - memcpy(doomcom.data + 1, TransmitBuffer + 1, c - 1); - } - } - else if (c > 0) - { //The packet is not from any in-game node, so we might as well discard it. - if (TransmitBuffer[0] == PRE_FAKE) - { - // If it's someone waiting in the lobby, let them know the game already started - uint8_t msg[] = { PRE_FAKE, PRE_IN_PROGRESS }; - PreSend(msg, 2, &fromaddress); - } - doomcom.remoteplayer = -1; - return; + target = addrName; } - doomcom.remoteplayer = node; - doomcom.datalength = (short)c; -} - -sockaddr_in *PreGet (void *buffer, int bufferlen, bool noabort) -{ - static sockaddr_in fromaddress; - socklen_t fromlen; - int c; - - fromlen = sizeof(fromaddress); - c = recvfrom (mysocket, (char *)buffer, bufferlen, 0, - (sockaddr *)&fromaddress, &fromlen); - - if (c == SOCKET_ERROR) - { - int err = WSAGetLastError(); - if (err == WSAEWOULDBLOCK || (noabort && err == WSAECONNRESET)) - return NULL; // no packet - } - return &fromaddress; -} - -void PreSend (const void *buffer, int bufferlen, const sockaddr_in *to) -{ - sendto (mysocket, (const char *)buffer, bufferlen, 0, (const sockaddr *)to, sizeof(*to)); -} - -void BuildAddress (sockaddr_in *address, const char *name) -{ - hostent *hostentry; // host information entry - u_short port; - const char *portpart; - bool isnamed = false; - int curchar; - char c; - FString target; - - address->sin_family = AF_INET; - - if ( (portpart = strchr (name, ':')) ) - { - target = FString(name, portpart - name); - port = atoi (portpart + 1); - if (!port) - { - Printf ("Weird port: %s (using %d)\n", portpart + 1, DOOMPORT); - port = DOOMPORT; - } - } - else - { - target = name; - port = DOOMPORT; - } - address->sin_port = htons(port); - - for (curchar = 0; (c = target[curchar]) ; curchar++) + bool isNamed = false; + char c = 0; + for (size_t curChar = 0u; (c = target[curChar]); ++curChar) { if ((c < '0' || c > '9') && c != '.') { - isnamed = true; + isNamed = true; break; } } - if (!isnamed) + address.sin_family = AF_INET; + address.sin_port = htons(port); + if (!isNamed) { - address->sin_addr.s_addr = inet_addr (target.GetChars()); - Printf ("Node number %d, address %s\n", doomcom.numplayers, target.GetChars()); + address.sin_addr.s_addr = inet_addr(target.GetChars()); } else { - hostentry = gethostbyname (target.GetChars()); - if (!hostentry) - I_FatalError ("gethostbyname: couldn't find %s\n%s", target.GetChars(), neterror()); - address->sin_addr.s_addr = *(int *)hostentry->h_addr_list[0]; - Printf ("Node number %d, hostname %s\n", - doomcom.numplayers, hostentry->h_name); + hostent* hostEntry = gethostbyname(target.GetChars()); + if (hostEntry == nullptr) + I_FatalError("gethostbyname: Couldn't find %s\n%s", target.GetChars(), neterror()); + + address.sin_addr.s_addr = *(int*)hostEntry->h_addr_list[0]; } } -void CloseNetwork (void) +static void StartNetwork(bool autoPort) { - if (mysocket != INVALID_SOCKET) - { - closesocket (mysocket); - mysocket = INVALID_SOCKET; - } #ifdef __WIN32__ - WSACleanup (); -#endif -} - -void StartNetwork (bool autoPort) -{ - u_long trueval = 1; -#ifdef __WIN32__ - WSADATA wsad; - - if (WSAStartup (0x0101, &wsad)) - { - I_FatalError ("Could not initialize Windows Sockets"); - } + WSADATA data; + if (WSAStartup(0x0101, &data)) + I_FatalError("Couldn't initialize Windows sockets"); #endif netgame = true; multiplayer = true; + MySocket = CreateUDPSocket(); + BindToLocalPort(MySocket, autoPort ? 0 : GamePort); - // create communication socket - mysocket = UDPsocket (); - BindToLocalPort (mysocket, autoPort ? 0 : DOOMPORT); + u_long trueVal = 1u; #ifndef __sun - ioctlsocket (mysocket, FIONBIO, &trueval); + ioctlsocket(MySocket, FIONBIO, &trueVal); #else fcntl(mysocket, F_SETFL, trueval | O_NONBLOCK); #endif } -void SendAbort (int connected) +void CloseNetwork() { - uint8_t dis[2] = { PRE_FAKE, PRE_DISCONNECT }; - int i, j; - - if (connected > 1) + if (MySocket != INVALID_SOCKET) { - if (doomcom.consoleplayer == 0) - { - // The host needs to let everyone know - for (i = 1; i < connected; ++i) - { - for (j = 4; j > 0; --j) - { - PreSend (dis, 2, &sendaddress[i]); - } - } - } - else - { - // Guests only need to let the host know. - for (i = 4; i > 0; --i) - { - PreSend (dis, 2, &sendaddress[0]); - } - } - } -} - -void SendConAck (int num_connected, int num_needed) -{ - PreGamePacket packet; - - packet.Fake = PRE_FAKE; - packet.Message = PRE_CONACK; - packet.NumNodes = num_needed; - packet.NumPresent = num_connected; - for (int node = 1; node < num_connected; ++node) - { - PreSend (&packet, 4, &sendaddress[node]); - } - I_NetProgress (num_connected); -} - -bool Host_CheckForConnects (void *userdata) -{ - PreGameConnectPacket packet; - int* connectedPlayers = (int*)userdata; - sockaddr_in *from; - int node; - - while ( (from = PreGet (&packet, sizeof(packet), false)) ) - { - if (packet.Fake != PRE_FAKE) - { - continue; - } - switch (packet.Message) - { - case PRE_CONNECT: - node = FindNode (from); - if (doomcom.numplayers == *connectedPlayers) - { - if (node == -1) - { - const uint8_t *s_addr_bytes = (const uint8_t *)&from->sin_addr; - I_NetMessage ("Got extra connect from %d.%d.%d.%d:%d", - s_addr_bytes[0], s_addr_bytes[1], s_addr_bytes[2], s_addr_bytes[3], - from->sin_port); - packet.Message = PRE_ALLFULL; - PreSend (&packet, 2, from); - } - } - else - { - if (node == -1) - { - if (strlen(net_password) > 0 && strcmp(net_password, packet.Password)) - { - packet.Message = PRE_WRONG_PASSWORD; - PreSend(&packet, 2, from); - break; - } - - node = *connectedPlayers; - ++*connectedPlayers; - sendaddress[node] = *from; - I_NetMessage ("Got connect from node %d.", node); - } - - // Let the new guest (and everyone else) know we got their message. - SendConAck (*connectedPlayers, doomcom.numplayers); - } - break; - - case PRE_DISCONNECT: - node = FindNode (from); - if (node >= 0) - { - I_ClearNode(node); - I_NetMessage ("Got disconnect from node %d.", node); - --*connectedPlayers; - while (node < *connectedPlayers) - { - sendaddress[node] = sendaddress[node+1]; - ++node; - } - - // Let remaining guests know that somebody left. - SendConAck (*connectedPlayers, doomcom.numplayers); - } - break; - } - } - if (*connectedPlayers < doomcom.numplayers && !I_ShouldStartNetGame()) - { - // Send message to everyone as a keepalive - SendConAck(*connectedPlayers, doomcom.numplayers); - return false; - } - - // It's possible somebody bailed out after all players were found. - // Unfortunately, this isn't guaranteed to catch all of them. - // Oh well. Better than nothing. - while ( (from = PreGet (&packet, sizeof(packet), false)) ) - { - if (packet.Fake == PRE_FAKE && packet.Message == PRE_DISCONNECT) - { - node = FindNode (from); - if (node >= 0) - { - I_ClearNode(node); - --*connectedPlayers; - while (node < *connectedPlayers) - { - sendaddress[node] = sendaddress[node+1]; - ++node; - } - // Let remaining guests know that somebody left. - SendConAck (*connectedPlayers, doomcom.numplayers); - } - break; - } - } - - // TODO: This will need a much better solution later. - if (I_ShouldStartNetGame()) - doomcom.numplayers = *connectedPlayers; - - return *connectedPlayers >= doomcom.numplayers; -} - -bool Host_SendAllHere (void *userdata) -{ - int* gotack = (int*)userdata; - const int mask = (1 << doomcom.numplayers) - 1; - PreGamePacket packet; - int node; - sockaddr_in *from; - - // Send out address information to all guests. This is will be the final order - // of the send addresses so each guest will need to rearrange their own in order - // to keep node -> player numbers synchronized. - packet.Fake = PRE_FAKE; - packet.Message = PRE_ALLHERE; - for (node = 1; node < doomcom.numplayers; node++) - { - int machine, spot = 0; - - packet.ConsoleNum = node; - if (!((*gotack) & (1 << node))) - { - for (spot = 0; spot < doomcom.numplayers; spot++) - { - packet.machines[spot].address = sendaddress[spot].sin_addr.s_addr; - packet.machines[spot].port = sendaddress[spot].sin_port; - } - packet.NumNodes = doomcom.numplayers; - } - else - { - packet.NumNodes = 0; - } - PreSend (&packet, 4 + spot*8, &sendaddress[node]); - } - - // Check for replies. - while ( (from = PreGet (&packet, sizeof(packet), false)) ) - { - if (packet.Fake != PRE_FAKE) - continue; - - if (packet.Message == PRE_ALLHEREACK) - { - node = FindNode (from); - if (node >= 0) - *gotack |= 1 << node; - } - else if (packet.Message == PRE_CONNECT) - { - // If someone is still trying to connect, let them know it's either too - // late or that they need to move on to the next stage. - node = FindNode(from); - if (node == -1) - { - packet.Message = PRE_ALLFULL; - PreSend(&packet, 2, from); - } - else - { - packet.Message = PRE_CONACK; - packet.NumNodes = packet.NumPresent = doomcom.numplayers; - PreSend(&packet, 4, from); - } - } - } - - // If everybody has replied, then this loop can end. - return ((*gotack) & mask) == mask; -} - -bool HostGame (int i) -{ - PreGamePacket packet; - int numplayers; - int node; - - if ((i == Args->NumArgs() - 1) || !(numplayers = atoi (Args->GetArg(i+1)))) - { // No player count specified, assume 2 - numplayers = 2; - } - - if (numplayers > MaxPlayers) - { - I_FatalError("You cannot host a game with %d players. The limit is currently %d.", numplayers, MaxPlayers); - } - - if (numplayers == 1) - { // Special case: Only 1 player, so don't bother starting the network - NetworkClients += 0; + closesocket(MySocket); + MySocket = INVALID_SOCKET; netgame = false; - multiplayer = true; - doomcom.id = DOOMCOM_ID; - doomcom.numplayers = 1; - doomcom.consoleplayer = 0; - return true; } - - StartNetwork (false); - - // [JC] - this computer is starting the game, therefore it should - // be the Net Arbitrator. - doomcom.consoleplayer = 0; - doomcom.numplayers = numplayers; - - I_NetInit ("Hosting game", numplayers); - - // Wait for the lobby to be full. - int connectedPlayers = 1; - if (!I_NetLoop (Host_CheckForConnects, (void *)&connectedPlayers)) - { - SendAbort(connectedPlayers); - throw CExitEvent(0); - return false; - } - - // If the player force started with only themselves in the lobby, start the game - // immediately. - if (doomcom.numplayers <= 1) - { - NetworkClients += 0; - netgame = false; - multiplayer = true; - doomcom.id = DOOMCOM_ID; - doomcom.numplayers = 1; - I_NetDone(); - return true; - } - - // Now inform everyone of all machines involved in the game - int gotack = 1; - I_NetMessage ("Sending all here."); - I_NetInit ("Done waiting", 1); - - if (!I_NetLoop (Host_SendAllHere, (void *)&gotack)) - { - SendAbort(doomcom.numplayers); - throw CExitEvent(0); - return false; - } - - // Now go - I_NetMessage ("Go"); - packet.Fake = PRE_FAKE; - packet.Message = PRE_GO; - for (node = 1; node < doomcom.numplayers; node++) - { - // If we send the packets eight times to each guest, - // hopefully at least one of them will get through. - for (int ii = 8; ii != 0; --ii) - { - PreSend (&packet, 2, &sendaddress[node]); - } - } - - I_NetMessage ("Total players: %d", doomcom.numplayers); - - doomcom.id = DOOMCOM_ID; - - return true; +#ifdef __WIN32__ + WSACleanup(); +#endif } -// This routine is used by a guest to notify the host of its presence. -// Once that host acknowledges receipt of the notification, this routine -// is never called again. - -bool Guest_ContactHost (void *userdata) -{ - sockaddr_in *from; - PreGamePacket packet; - PreGameConnectPacket sendPacket; - - // Let the host know we are here. - sendPacket.Fake = PRE_FAKE; - sendPacket.Message = PRE_CONNECT; - memcpy(sendPacket.Password, net_password, strlen(net_password) + 1); - PreSend (&sendPacket, sizeof(sendPacket), &sendaddress[0]); - - // Listen for a reply. - while ( (from = PreGet (&packet, sizeof(packet), true)) ) - { - if (packet.Fake == PRE_FAKE && !FindNode(from)) - { - if (packet.Message == PRE_CONACK) - { - I_NetMessage ("Total players: %d", packet.NumNodes); - I_NetInit ("Waiting for other players", packet.NumNodes); - I_NetProgress (packet.NumPresent); - return true; - } - else if (packet.Message == PRE_DISCONNECT) - { - I_NetError("The host cancelled the game."); - } - else if (packet.Message == PRE_ALLFULL) - { - I_NetError("The game is full."); - } - else if (packet.Message == PRE_IN_PROGRESS) - { - I_NetError("The game was already started."); - } - else if (packet.Message == PRE_WRONG_PASSWORD) - { - I_NetError("Invalid password."); - } - } - } - - // In case the progress bar could not be marqueed, bump it. - I_NetProgress (0); - - return false; -} - -bool Guest_WaitForOthers (void *userdata) -{ - sockaddr_in *from; - PreGamePacket packet; - - while ( (from = PreGet (&packet, sizeof(packet), false)) ) - { - if (packet.Fake != PRE_FAKE || FindNode(from)) - { - continue; - } - switch (packet.Message) - { - case PRE_CONACK: - I_NetProgress (packet.NumPresent); - break; - - case PRE_ALLHERE: - if (doomcom.consoleplayer == -1) - { - doomcom.numplayers = packet.NumNodes; - doomcom.consoleplayer = packet.ConsoleNum; - // Don't use the address sent from the host for our own machine. - sendaddress[doomcom.consoleplayer] = sendaddress[1]; - - I_NetMessage ("Console player number: %d", doomcom.consoleplayer); - - for (int node = 1; node < doomcom.numplayers; ++node) - { - if (node == doomcom.consoleplayer) - continue; - - sendaddress[node].sin_addr.s_addr = packet.machines[node].address; - sendaddress[node].sin_port = packet.machines[node].port; - - // [JC] - fixes problem of games not starting due to - // no address family being assigned to nodes stored in - // sendaddress[] from the All Here packet. - sendaddress[node].sin_family = AF_INET; - } - - I_NetMessage("Received All Here, sending ACK."); - } - - packet.Fake = PRE_FAKE; - packet.Message = PRE_ALLHEREACK; - PreSend (&packet, 2, &sendaddress[0]); - break; - - case PRE_GO: - I_NetMessage ("Received \"Go.\""); - return true; - - case PRE_DISCONNECT: - I_NetError("The host cancelled the game."); - break; - } - } - - return false; -} - -bool JoinGame (int i) -{ - if ((i == Args->NumArgs() - 1) || - (Args->GetArg(i+1)[0] == '-') || - (Args->GetArg(i+1)[0] == '+')) - I_FatalError ("You need to specify the host machine's address"); - - StartNetwork (true); - - // Host is always node 0 - BuildAddress (&sendaddress[0], Args->GetArg(i+1)); - doomcom.numplayers = 2; - doomcom.consoleplayer = -1; - - // Let host know we are here - I_NetInit ("Contacting host", 0); - - if (!I_NetLoop (Guest_ContactHost, nullptr)) - { - SendAbort(2); - throw CExitEvent(0); - return false; - } - - // Wait for everyone else to connect - if (!I_NetLoop (Guest_WaitForOthers, nullptr)) - { - SendAbort(2); - throw CExitEvent(0); - return false; - } - - I_NetMessage ("Total players: %d", doomcom.numplayers); - - doomcom.id = DOOMCOM_ID; - return true; -} - -static int PrivateNetOf(in_addr in) +static int PrivateNetOf(const in_addr& in) { int addr = ntohl(in.s_addr); if ((addr & 0xFFFF0000) == 0xC0A80000) // 192.168.0.0 @@ -994,116 +316,39 @@ static int PrivateNetOf(in_addr in) return 0; } -// -// NodesOnSameNetwork -// // The best I can really do here is check if the others are on the same // private network, since that means we (probably) are too. -// - -static bool NodesOnSameNetwork() +static bool ClientsOnSameNetwork() { - if (doomcom.consoleplayer != 0) + size_t start = 1u; + for (; start < MaxClients; ++start) + { + if (Connected[start].Status != CSTAT_NONE) + break; + } + + if (start >= MaxClients) return false; - const int firstClient = PrivateNetOf(sendaddress[1].sin_addr); -// Printf("net1 = %08x\n", net1); - if (firstClient == 0) - { + const int firstClient = PrivateNetOf(Connected[start].Address.sin_addr); + if (!firstClient) return false; - } - for (int i = 2; i < doomcom.numplayers; ++i) + + for (size_t i = 1u; i < MaxClients; ++i) { - const int net = PrivateNetOf(sendaddress[i].sin_addr); -// Printf("Net[%d] = %08x\n", i, net); - if (net != firstClient) - { + if (i == start) + continue; + + if (Connected[i].Status == CSTAT_NONE || PrivateNetOf(Connected[i].Address.sin_addr) != firstClient) return false; - } } + return true; } -// -// I_InitNetwork -// -// Returns true if packet server mode might be a good idea. -// -int I_InitNetwork (void) -{ - int i; - const char *v; - - memset (&doomcom, 0, sizeof(doomcom)); - - // set up for network - v = Args->CheckValue ("-dup"); - if (v) - { - doomcom.ticdup = clamp (atoi (v), 1, MAXTICDUP); - } - else - { - doomcom.ticdup = 1; - } - - v = Args->CheckValue ("-port"); - if (v) - { - DOOMPORT = atoi (v); - Printf ("using alternate port %i\n", DOOMPORT); - } - - net_password = Args->CheckValue("-password"); - - // parse network game options, - // player 1: -host - // player x: -join - if ( (i = Args->CheckParm ("-host")) ) - { - if (!HostGame (i)) return -1; - } - else if ( (i = Args->CheckParm ("-join")) ) - { - if (!JoinGame (i)) return -1; - } - else - { - // single player game - NetworkClients += 0; - netgame = false; - multiplayer = false; - doomcom.id = DOOMCOM_ID; - doomcom.ticdup = 1; - doomcom.numplayers = 1; - doomcom.consoleplayer = 0; - return false; - } - - if (doomcom.numplayers < 3) - { // Packet server mode with only two players is effectively the same as - // peer-to-peer but with some slightly larger packets. - return false; - } - return !NodesOnSameNetwork(); -} - - -void I_NetCmd (void) -{ - if (doomcom.command == CMD_SEND) - { - PacketSend (); - } - else if (doomcom.command == CMD_GET) - { - PacketGet (); - } - else - I_Error ("Bad net cmd: %i\n",doomcom.command); -} - -void I_NetMessage(const char* text, ...) +// Print a network-related message to the console. This doesn't print to the window so should +// not be used for that and is mainly for logging. +static void I_NetLog(const char* text, ...) { // todo: use better abstraction once everything is migrated to in-game start screens. #if defined _WIN32 || defined __APPLE__ @@ -1123,37 +368,909 @@ void I_NetMessage(const char* text, ...) #endif } -void I_NetError(const char* error) +// Gracefully closes the net window so that any error messaging can be properly displayed. +static void I_NetError(const char* error) { - doomcom.numplayers = 0; StartWindow->NetClose(); I_FatalError("%s", error); } +static void I_NetInit(const char* msg, bool host) +{ + StartWindow->NetInit(msg, host); +} + // todo: later these must be dispatched by the main menu, not the start screen. -void I_NetProgress(int val) +// Updates the general status of the lobby. +static void I_NetMessage(const char* msg) { - StartWindow->NetProgress(val); + StartWindow->NetMessage(msg); } -void I_NetInit(const char* msg, int num) + +// Listen for incoming connections while the lobby is active. The main thread needs to be locked up +// here to prevent the engine from continuing to start the game until everyone is ready. +static bool I_NetLoop(bool (*loopCallback)(void*), void* data) { - StartWindow->NetInit(msg, num); + return StartWindow->NetLoop(loopCallback, data); } -bool I_NetLoop(bool (*timer_callback)(void*), void* userdata) + +// A new client has just entered the game, so add them to the player list. +static void I_NetClientConnected(size_t client, unsigned int charLimit = 0u) { - return StartWindow->NetLoop(timer_callback, userdata); + const char* name = Net_GetClientName(client, charLimit); + unsigned int flags = CFL_NONE; + if (client == 0) + flags |= CFL_HOST; + if (client == consoleplayer) + flags |= CFL_CONSOLEPLAYER; + + StartWindow->NetConnect(client, name, flags, Connected[client].Status); } + +// A client changed ready state. +static void I_NetClientUpdated(size_t client) +{ + StartWindow->NetUpdate(client, Connected[client].Status); +} + +static void I_NetClientDisconnected(size_t client) +{ + StartWindow->NetDisconnect(client); +} + +static void I_NetUpdatePlayers(size_t current, size_t limit) +{ + StartWindow->NetProgress(current, limit); +} + +static bool I_ShouldStartNetGame() +{ + return StartWindow->ShouldStartNet(); +} + +static void I_GetKickClients(TArray& clients) +{ + clients.Clear(); + + int c = -1; + while ((c = StartWindow->GetNetKickClient()) != -1) + clients.Push(c); +} + +static void I_GetBanClients(TArray& clients) +{ + clients.Clear(); + + int c = -1; + while ((c = StartWindow->GetNetBanClient()) != -1) + clients.Push(c); +} + void I_NetDone() { StartWindow->NetDone(); } + +void I_ClearClient(size_t client) +{ + memset(&Connected[client], 0, sizeof(Connected[client])); +} + +static int FindClient(const sockaddr_in& address) +{ + int i = 0; + for (; i < MaxClients; ++i) + { + if (Connected[i].Status == CSTAT_NONE) + continue; + + if (address.sin_addr.s_addr == Connected[i].Address.sin_addr.s_addr + && address.sin_port == Connected[i].Address.sin_port) + { + break; + } + } + + return i >= MaxClients ? -1 : i; +} + +static void SendPacket(const sockaddr_in& to) +{ + // Huge packets should be sent out as sequences, not as one big packet, otherwise it's prone + // to high amounts of congestion and reordering needed. + if (NetBufferLength > MAX_MSGLEN) + I_FatalError("Netbuffer overflow: Tried to send %u bytes of data", NetBufferLength); + + assert(!(NetBuffer[0] & NCMD_COMPRESSED)); + + uLong size = MaxTransmitSize - 1u; + int res = -1; + if (NetBufferLength >= MinCompressionSize) + { + TransmitBuffer[0] = NetBuffer[0] | NCMD_COMPRESSED; + res = compress2(TransmitBuffer + 1, &size, NetBuffer + 1, NetBufferLength - 1u, 9); + ++size; + } + + if (res == Z_OK && size < static_cast(NetBufferLength)) + res = sendto(MySocket, (const char*)TransmitBuffer, size, 0, (const sockaddr*)&to, sizeof(to)); + else if (NetBufferLength > MaxTransmitSize) + I_Error("Net compression failed (zlib error %d)", res); + else + res = sendto(MySocket, (const char*)NetBuffer, NetBufferLength, 0, (const sockaddr*)&to, sizeof(to)); +} + +static void GetPacket(sockaddr_in* const from = nullptr) +{ + sockaddr_in fromAddress; + socklen_t fromSize = sizeof(fromAddress); + + int msgSize = recvfrom(MySocket, (char *)TransmitBuffer, MaxTransmitSize, 0, + (sockaddr *)&fromAddress, &fromSize); + + int client = FindClient(fromAddress); + if (client >= 0 && msgSize == SOCKET_ERROR) + { + int err = WSAGetLastError(); + if (err == WSAECONNRESET) + { + if (consoleplayer == -1) + { + client = -1; + msgSize = 0; + } + else + { + // The remote node aborted unexpectedly, so pretend it sent an exit packet. If in packet server + // mode and it was the host, just consider the game too bricked to continue since the host has + // to determine the new host properly. + if (NetMode == NET_PacketServer && client == Net_Arbitrator) + I_NetError("Host unexpectedly disconnected"); + + NetBuffer[0] = NCMD_EXIT; + msgSize = 1; + } + } + else if (err != WSAEWOULDBLOCK) + { + I_Error("Failed to get packet: %s", neterror()); + } + else + { + client = -1; + msgSize = 0; + } + } + else if (msgSize > 0) + { + if (client == -1 && !(TransmitBuffer[0] & NCMD_SETUP)) + { + msgSize = 0; + } + else if (client == -1 && bGameStarted) + { + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_IN_PROGRESS; + NetBufferLength = 2u; + SendPacket(fromAddress); + msgSize = 0; + } + else + { + NetBuffer[0] = (TransmitBuffer[0] & ~NCMD_COMPRESSED); + if (TransmitBuffer[0] & NCMD_COMPRESSED) + { + uLongf size = MAX_MSGLEN - 1; + int err = uncompress(NetBuffer + 1, &size, TransmitBuffer + 1, msgSize - 1); + if (err != Z_OK) + { + Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars()); + client = -1; + msgSize = 0; + } + else + { + msgSize = size + 1; + } + } + else + { + memcpy(NetBuffer + 1, TransmitBuffer + 1, msgSize - 1); + } + } + } + else + { + client = -1; + } + + RemoteClient = client; + NetBufferLength = max(msgSize, 0); + if (from != nullptr) + *from = fromAddress; +} + +void I_NetCmd(ENetCommand cmd) +{ + if (cmd == CMD_SEND) + { + if (RemoteClient >= 0) + SendPacket(Connected[RemoteClient].Address); + } + else if (cmd == CMD_GET) + { + GetPacket(); + } +} + +static void SetClientAck(size_t client, size_t from, bool add) +{ + const uint64_t bit = (uint64_t)1u << from; + if (add) + Connected[client].InfoAck |= bit; + else + Connected[client].InfoAck &= ~bit; +} + +static bool ClientGotAck(size_t client, size_t from) +{ + return (Connected[client].InfoAck & ((uint64_t)1u << from)); +} + +static bool GetConnection(sockaddr_in& from) +{ + GetPacket(&from); + return NetBufferLength > 0; +} + +static void RejectConnection(const sockaddr_in& to, ENetConnectType reason) +{ + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = reason; + NetBufferLength = 2u; + + SendPacket(to); +} + +static void AddClientConnection(const sockaddr_in& from, size_t client) +{ + Connected[client].Status = CSTAT_CONNECTING; + Connected[client].Address = from; + NetworkClients += client; + I_NetLog("Client %u joined the lobby", client); + I_NetClientUpdated(client); + + // Make sure any ready clients are marked as needing the new client's info. + for (size_t i = 1u; i < MaxClients; ++i) + { + if (Connected[i].Status == CSTAT_READY) + { + Connected[i].Status = CSTAT_WAITING; + I_NetClientUpdated(i); + } + } +} + +static void RemoveClientConnection(size_t client) +{ + I_NetClientDisconnected(client); + I_ClearClient(client); + NetworkClients -= client; + I_NetLog("Client %u left the lobby", client); + + // Let everyone else know the user left as well. + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_DISCONNECT; + NetBuffer[2] = client; + NetBufferLength = 3u; + + for (size_t i = 1u; i < MaxClients; ++i) + { + if (Connected[i].Status == CSTAT_NONE) + continue; + + SetClientAck(i, client, false); + for (int i = 0; i < 4; ++i) + SendPacket(Connected[i].Address); + } +} + +void HandleIncomingConnection() +{ + if (consoleplayer != Net_Arbitrator || RemoteClient == -1) + return; + + if (Connected[RemoteClient].Status == CSTAT_READY) + { + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_GO; + NetBuffer[2] = NetMode; + NetBufferLength = 3u; + SendPacket(Connected[RemoteClient].Address); + } +} + +static bool Host_CheckForConnections(void* connected) +{ + const bool forceStarting = I_ShouldStartNetGame(); + const bool hasPassword = strlen(net_password) > 0; + size_t* connectedPlayers = (size_t*)connected; + + TArray toBoot = {}; + I_GetKickClients(toBoot); + for (auto client : toBoot) + { + if (client <= 0 || Connected[client].Status == CSTAT_NONE) + continue; + + sockaddr_in booted = Connected[client].Address; + + RemoveClientConnection(client); + --*connectedPlayers; + I_NetUpdatePlayers(*connectedPlayers, MaxClients); + + RejectConnection(booted, PRE_KICKED); + } + + I_GetBanClients(toBoot); + for (auto client : toBoot) + { + if (client <= 0 || Connected[client].Status == CSTAT_NONE) + continue; + + sockaddr_in booted = Connected[client].Address; + BannedConnections.Push(booted); + + RemoveClientConnection(client); + --*connectedPlayers; + I_NetUpdatePlayers(*connectedPlayers, MaxClients); + + RejectConnection(booted, PRE_BANNED); + } + + sockaddr_in from; + while (GetConnection(from)) + { + if (NetBuffer[0] == NCMD_EXIT) + { + if (RemoteClient >= 0) + { + RemoveClientConnection(RemoteClient); + --*connectedPlayers; + I_NetUpdatePlayers(*connectedPlayers, MaxClients); + } + + continue; + } + + if (NetBuffer[0] != NCMD_SETUP) + continue; + + if (NetBuffer[1] == PRE_CONNECT) + { + if (RemoteClient >= 0) + continue; + + size_t banned = 0u; + for (; banned < BannedConnections.Size(); ++banned) + { + if (BannedConnections[banned].sin_addr.s_addr == from.sin_addr.s_addr) + break; + } + + if (banned < BannedConnections.Size()) + { + RejectConnection(from, PRE_BANNED); + } + else if (NetBuffer[2] % 256 != VER_MAJOR || NetBuffer[3] % 256 != VER_MINOR || NetBuffer[4] % 256 != VER_REVISION) + { + RejectConnection(from, PRE_WRONG_ENGINE); + } + else if (*connectedPlayers >= MaxClients) + { + RejectConnection(from, PRE_FULL); + } + else if (forceStarting) + { + RejectConnection(from, PRE_IN_PROGRESS); + } + else if (hasPassword && strcmp(net_password, (const char*)&NetBuffer[5])) + { + RejectConnection(from, PRE_WRONG_PASSWORD); + } + else + { + size_t free = 1u; + for (; free < MaxClients; ++free) + { + if (Connected[free].Status == CSTAT_NONE) + break; + } + + AddClientConnection(from, free); + ++*connectedPlayers; + I_NetUpdatePlayers(*connectedPlayers, MaxClients); + } + } + else if (NetBuffer[1] == PRE_USER_INFO) + { + if (Connected[RemoteClient].Status == CSTAT_CONNECTING) + { + uint8_t* stream = &NetBuffer[2]; + Net_ReadUserInfo(RemoteClient, stream); + Connected[RemoteClient].Status = CSTAT_WAITING; + I_NetClientConnected(RemoteClient, 16u); + } + } + else if (NetBuffer[1] == PRE_USER_INFO_ACK) + { + SetClientAck(RemoteClient, NetBuffer[2], true); + } + else if (NetBuffer[1] == PRE_GAME_INFO_ACK) + { + Connected[RemoteClient].bHasGameInfo = true; + } + } + + const size_t addrSize = sizeof(sockaddr_in); + bool ready = true; + NetBuffer[0] = NCMD_SETUP; + for (size_t client = 1u; client < MaxClients; ++client) + { + auto& con = Connected[client]; + // If we're starting before the lobby is full, only check against connected clients. + if (con.Status != CSTAT_READY && (!forceStarting || con.Status != CSTAT_NONE)) + ready = false; + + if (con.Status == CSTAT_CONNECTING) + { + NetBuffer[1] = PRE_CONNECT_ACK; + NetBuffer[2] = client; + NetBuffer[3] = *connectedPlayers; + NetBuffer[4] = MaxClients; + NetBufferLength = 5u; + SendPacket(con.Address); + } + else if (con.Status == CSTAT_WAITING) + { + bool clientReady = true; + if (!ClientGotAck(client, client)) + { + NetBuffer[1] = PRE_USER_INFO_ACK; + NetBufferLength = 2u; + SendPacket(con.Address); + clientReady = false; + } + + if (!con.bHasGameInfo) + { + NetBuffer[1] = PRE_GAME_INFO; + NetBuffer[2] = TicDup; + NetBufferLength = 3u; + + uint8_t* stream = &NetBuffer[NetBufferLength]; + NetBufferLength += Net_SetGameInfo(stream); + SendPacket(con.Address); + clientReady = false; + } + + NetBuffer[1] = PRE_USER_INFO; + for (size_t i = 0u; i < MaxClients; ++i) + { + if (i == client || Connected[i].Status == CSTAT_NONE) + continue; + + if (!ClientGotAck(client, i)) + { + if (Connected[i].Status >= CSTAT_WAITING) + { + NetBuffer[2] = i; + NetBufferLength = 3u; + // Client will already have the host connection information. + if (i > 0) + { + memcpy(&NetBuffer[NetBufferLength], &Connected[i].Address, addrSize); + NetBufferLength += addrSize; + } + + uint8_t* stream = &NetBuffer[NetBufferLength]; + NetBufferLength += Net_SetUserInfo(i, stream); + SendPacket(con.Address); + } + clientReady = false; + } + } + + if (clientReady) + { + con.Status = CSTAT_READY; + I_NetClientUpdated(client); + } + } + else if (con.Status == CSTAT_READY) + { + NetBuffer[1] = PRE_HEARTBEAT; + NetBuffer[2] = *connectedPlayers; + NetBuffer[3] = MaxClients; + NetBufferLength = 4u; + SendPacket(con.Address); + } + } + + return ready && (*connectedPlayers >= MaxClients || forceStarting); +} + +// Boon TODO: Add cool down between sends +static void SendAbort() +{ + NetBuffer[0] = NCMD_EXIT; + NetBufferLength = 1u; + + if (consoleplayer == 0) + { + for (size_t client = 1u; client < MaxClients; ++client) + { + if (Connected[client].Status != CSTAT_NONE) + SendPacket(Connected[client].Address); + } + } + else + { + SendPacket(Connected[0].Address); + } +} + +static bool HostGame(int arg, bool forcedNetMode) +{ + if (arg >= Args->NumArgs() || !(MaxClients = atoi(Args->GetArg(arg)))) + { // No player count specified, assume 2 + MaxClients = 2u; + } + + if (MaxClients > MAXPLAYERS) + I_FatalError("Cannot host a game with %u players. The limit is currently %u", MaxClients, MAXPLAYERS); + + consoleplayer = 0; + NetworkClients += 0; + Connected[consoleplayer].Status = CSTAT_READY; + Net_SetupUserInfo(); + + // If only 1 player, don't bother starting the network + if (MaxClients == 1u) + { + TicDup = 1u; + multiplayer = true; + return true; + } + + StartNetwork(false); + I_NetInit("Waiting for other players...", true); + I_NetUpdatePlayers(1u, MaxClients); + I_NetClientConnected(0u, 16u); + + // Wait for the lobby to be full. + size_t connectedPlayers = 1u; + if (!I_NetLoop(Host_CheckForConnections, (void*)&connectedPlayers)) + { + SendAbort(); + throw CExitEvent(0); + } + + // Now go + I_NetMessage("Starting game"); + I_NetDone(); + + // If the player force started with only themselves in the lobby, start the game + // immediately. + if (connectedPlayers == 1u) + { + CloseNetwork(); + MaxClients = TicDup = 1u; + return true; + } + + if (!forcedNetMode) + { + if (MaxClients < 3) + NetMode = NET_PeerToPeer; + else if (!ClientsOnSameNetwork()) + NetMode = NET_PacketServer; + } + + I_NetLog("Go"); + + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_GO; + NetBuffer[2] = NetMode; + NetBufferLength = 3u; + for (size_t client = 1u; client < MaxClients; ++client) + { + if (Connected[client].Status != CSTAT_NONE) + SendPacket(Connected[client].Address); + } + + I_NetLog("Total players: %u", connectedPlayers); + + return true; +} + +static bool Guest_ContactHost(void* unused) +{ + // Listen for a reply. + const size_t addrSize = sizeof(sockaddr_in); + sockaddr_in from; + while (GetConnection(from)) + { + if (RemoteClient != 0) + continue; + + if (NetBuffer[0] == NCMD_EXIT) + I_NetError("The host cancelled the game"); + + if (NetBuffer[0] != NCMD_SETUP) + continue; + + if (NetBuffer[1] == PRE_HEARTBEAT) + { + MaxClients = NetBuffer[3]; + I_NetUpdatePlayers(NetBuffer[2], MaxClients); + } + else if (NetBuffer[1] == PRE_DISCONNECT) + { + I_ClearClient(NetBuffer[2]); + NetworkClients -= NetBuffer[2]; + SetClientAck(consoleplayer, NetBuffer[2], false); + I_NetClientDisconnected(NetBuffer[2]); + } + else if (NetBuffer[1] == PRE_FULL) + { + I_NetError("The game is full"); + } + else if (NetBuffer[1] == PRE_IN_PROGRESS) + { + I_NetError("The game has already started"); + } + else if (NetBuffer[1] == PRE_WRONG_PASSWORD) + { + I_NetError("Invalid password"); + } + else if (NetBuffer[1] == PRE_WRONG_ENGINE) + { + I_NetError("Engine version does not match the host's engine version"); + } + else if (NetBuffer[1] == PRE_INVALID_FILES) + { + I_NetError("Files do not match the host's files"); + } + else if (NetBuffer[1] == PRE_KICKED) + { + I_NetError("You have been kicked from the game"); + } + else if (NetBuffer[1] == PRE_BANNED) + { + I_NetError("You have been banned from the game"); + } + else if (NetBuffer[1] == PRE_CONNECT_ACK) + { + if (consoleplayer == -1) + { + NetworkClients += 0; + Connected[0].Status = CSTAT_WAITING; + I_NetClientUpdated(0); + + consoleplayer = NetBuffer[2]; + NetworkClients += consoleplayer; + Connected[consoleplayer].Status = CSTAT_CONNECTING; + Net_SetupUserInfo(); + + MaxClients = NetBuffer[4]; + I_NetMessage("Sending game information"); + I_NetUpdatePlayers(NetBuffer[3], MaxClients); + I_NetClientConnected(consoleplayer, 16u); + } + } + else if (NetBuffer[1] == PRE_USER_INFO_ACK) + { + // The host will only ever send us this to confirm they've gotten our data. + SetClientAck(consoleplayer, consoleplayer, true); + if (Connected[consoleplayer].Status == CSTAT_CONNECTING) + { + Connected[consoleplayer].Status = CSTAT_WAITING; + I_NetClientUpdated(consoleplayer); + I_NetMessage("Waiting for game to start"); + } + + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_USER_INFO_ACK; + NetBuffer[2] = consoleplayer; + NetBufferLength = 3u; + SendPacket(from); + } + else if (NetBuffer[1] == PRE_GAME_INFO) + { + if (!Connected[consoleplayer].bHasGameInfo) + { + TicDup = clamp(NetBuffer[2], 1, MAXTICDUP); + uint8_t* stream = &NetBuffer[3]; + Net_ReadGameInfo(stream); + Connected[consoleplayer].bHasGameInfo = true; + } + + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_GAME_INFO_ACK; + NetBufferLength = 2u; + SendPacket(from); + } + else if (NetBuffer[1] == PRE_USER_INFO) + { + const size_t c = NetBuffer[2]; + if (!ClientGotAck(consoleplayer, c)) + { + NetworkClients += c; + size_t byte = 3u; + if (c > 0) + { + Connected[c].Status = CSTAT_WAITING; + memcpy(&Connected[c].Address, &NetBuffer[byte], addrSize); + byte += addrSize; + } + else + { + Connected[c].Status = CSTAT_READY; + } + uint8_t* stream = &NetBuffer[byte]; + Net_ReadUserInfo(c, stream); + SetClientAck(consoleplayer, c, true); + + I_NetClientConnected(c, 16u); + } + + NetBuffer[0] = NCMD_SETUP; + NetBuffer[1] = PRE_USER_INFO_ACK; + NetBuffer[2] = c; + NetBufferLength = 3u; + SendPacket(from); + } + else if (NetBuffer[1] == PRE_GO) + { + NetMode = static_cast(NetBuffer[2]); + I_NetMessage("Starting game"); + I_NetLog("Received GO"); + return true; + } + } + + NetBuffer[0] = NCMD_SETUP; + if (consoleplayer == -1) + { + NetBuffer[1] = PRE_CONNECT; + NetBuffer[2] = VER_MAJOR % 256; + NetBuffer[3] = VER_MINOR % 256; + NetBuffer[4] = VER_REVISION % 256; + const size_t passSize = strlen(net_password) + 1; + memcpy(&NetBuffer[5], net_password, passSize); + NetBufferLength = 5u + passSize; + SendPacket(Connected[0].Address); + } + else + { + auto& con = Connected[consoleplayer]; + if (con.Status == CSTAT_CONNECTING) + { + NetBuffer[1] = PRE_USER_INFO; + NetBufferLength = 2u; + + uint8_t* stream = &NetBuffer[NetBufferLength]; + NetBufferLength += Net_SetUserInfo(consoleplayer, stream); + SendPacket(Connected[0].Address); + } + else if (con.Status == CSTAT_WAITING) + { + NetBuffer[1] = PRE_HEARTBEAT; + NetBufferLength = 2u; + SendPacket(Connected[0].Address); + } + } + + return false; +} + +static bool JoinGame(int arg) +{ + if (arg >= Args->NumArgs() + || Args->GetArg(arg)[0] == '-' || Args->GetArg(arg)[0] == '+') + { + I_FatalError("You need to specify the host machine's address"); + } + + StartNetwork(true); + + // Host is always client 0. + BuildAddress(Connected[0].Address, Args->GetArg(arg)); + Connected[0].Status = CSTAT_CONNECTING; + + I_NetInit("Contacting host...", false); + I_NetUpdatePlayers(0u, MaxClients); + I_NetClientUpdated(0); + + if (!I_NetLoop(Guest_ContactHost, nullptr)) + { + SendAbort(); + throw CExitEvent(0); + } + + for (size_t i = 1u; i < MaxClients; ++i) + { + if (Connected[i].Status != CSTAT_NONE) + Connected[i].Status = CSTAT_READY; + } + + I_NetLog("Total players: %u", MaxClients); + I_NetDone(); + + return true; +} + +// +// I_InitNetwork +// +// Returns true if packet server mode might be a good idea. +// +bool I_InitNetwork() +{ + // set up for network + const char* v = Args->CheckValue("-dup"); + if (v != nullptr) + TicDup = clamp(atoi(v), 1, MAXTICDUP); + + v = Args->CheckValue("-port"); + if (v != nullptr) + { + GamePort = atoi(v); + Printf("Using alternate port %d\n", GamePort); + } + + v = Args->CheckValue("-netmode"); + if (v != nullptr) + NetMode = atoi(v) ? NET_PacketServer : NET_PeerToPeer; + + net_password = Args->CheckValue("-password"); + + // parse network game options, + // player 1: -host + // player x: -join + int arg = -1; + if ((arg = Args->CheckParm("-host"))) + { + if (!HostGame(arg + 1, v != nullptr)) + return false; + } + else if ((arg = Args->CheckParm("-join"))) + { + if (!JoinGame(arg + 1)) + return false; + } + else + { + // single player game + TicDup = 1; + consoleplayer = 0; + NetworkClients += 0; + Connected[0].Status = CSTAT_READY; + Net_SetupUserInfo(); + } + + bGameStarted = true; + return true; +} + #ifdef __WIN32__ -const char *neterror (void) +const char* neterror() { static char neterr[16]; int code; - switch (code = WSAGetLastError ()) { + switch (code = WSAGetLastError()) { case WSAEACCES: return "EACCES"; case WSAEADDRINUSE: return "EADDRINUSE"; case WSAEADDRNOTAVAIL: return "EADDRNOTAVAIL"; @@ -1198,7 +1315,7 @@ const char *neterror (void) case WSAEDISCON: return "EDISCON"; default: - mysnprintf (neterr, countof(neterr), "%d", code); + mysnprintf(neterr, countof(neterr), "%d", code); return neterr; } } diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h index 6fbf6c51e..7d9f40af0 100644 --- a/src/common/engine/i_net.h +++ b/src/common/engine/i_net.h @@ -2,45 +2,43 @@ #define __I_NET_H__ #include +#include "tarray.h" -// Called by D_DoomMain. -int I_InitNetwork (void); -void I_ClearNode(int node); -void I_NetCmd (void); -void I_NetMessage(const char*, ...); -void I_NetError(const char* error); -void I_NetProgress(int val); -void I_NetInit(const char* msg, int num); -bool I_NetLoop(bool (*timer_callback)(void*), void* userdata); -void I_NetDone(); +inline constexpr size_t MAXPLAYERS = 64u; enum ENetConstants { - DOOMCOM_ID = 0x12345678, + DEFAULT_GAME_ID = 0x12345678, BACKUPTICS = 35 * 5, // Remember up to 5 seconds of data. MAXTICDUP = 3, MAXSENDTICS = 35 * 1, // Only send up to 1 second of data at a time. - LOCALCMDTICS = (BACKUPTICS*MAXTICDUP), + LOCALCMDTICS = (BACKUPTICS * MAXTICDUP), MAX_MSGLEN = 14000, - - CMD_SEND = 1, - CMD_GET = 2, }; -enum ENCMD +enum ENetCommand { - NCMD_EXIT = 0x80, // Client has left the game - NCMD_RETRANSMIT = 0x40, // - NCMD_SETUP = 0x20, // Guest is letting the host know who it is - NCMD_LEVELREADY = 0x10, // After loading a level, guests send this over to the host who then sends it back after all are received - NCMD_QUITTERS = 0x08, // Client is getting info about one or more players quitting (packet server only) - NCMD_COMPRESSED = 0x04, // Remainder of packet is compressed - NCMD_LATENCYACK = 0x02, // A latency packet was just read, so let the sender know. - NCMD_LATENCY = 0x01, // Latency packet, used for measuring RTT. + CMD_NONE, + CMD_SEND, + CMD_GET, +}; - NCMD_USERINFO = NCMD_SETUP + 1, // Guest is getting another client's user info - NCMD_GAMEINFO = NCMD_SETUP + 2, // Guest is getting the state of the game from the host - NCMD_GAMEREADY = NCMD_SETUP + 3, // Host has verified the game is ready to be started +enum ENetFlags +{ + NCMD_EXIT = 0x80, // Client has left the game + NCMD_RETRANSMIT = 0x40, // + NCMD_SETUP = 0x20, // Guest is letting the host know who it is + NCMD_LEVELREADY = 0x10, // After loading a level, guests send this over to the host who then sends it back after all are received + NCMD_QUITTERS = 0x08, // Client is getting info about one or more players quitting (packet server only) + NCMD_COMPRESSED = 0x04, // Remainder of packet is compressed + NCMD_LATENCYACK = 0x02, // A latency packet was just read, so let the sender know. + NCMD_LATENCY = 0x01, // Latency packet, used for measuring RTT. +}; + +enum ENetMode +{ + NET_PeerToPeer, + NET_PacketServer }; struct FClientStack : public TArray @@ -59,31 +57,22 @@ struct FClientStack : public TArray } }; -extern FClientStack NetworkClients; - -// -// Network packet data. -// -struct doomcom_t -{ - // info common to all nodes - uint32_t id; // should be DOOMCOM_ID - int16_t ticdup; // 1 = no duplication, 2-3 = dup for slow nets - int16_t numplayers; - - // info specific to this node - int16_t consoleplayer; - - // communication between DOOM and the driver - int16_t command; // CMD_SEND or CMD_GET - int16_t remoteplayer; // dest for send, set by get (-1 = no packet). - // packet data to be sent - int16_t datalength; // bytes in data to be sent - uint8_t data[MAX_MSGLEN]; -}; - -extern doomcom_t doomcom; extern bool netgame, multiplayer; extern int consoleplayer; +extern int Net_Arbitrator; +extern FClientStack NetworkClients; +extern ENetMode NetMode; +extern uint8_t NetBuffer[MAX_MSGLEN]; +extern size_t NetBufferLength; +extern uint8_t TicDup; +extern int RemoteClient; +extern uint8_t MaxClients; +extern uint32_t GameID; + +bool I_InitNetwork(); +void I_ClearClient(size_t client); +void I_NetCmd(ENetCommand cmd); +void I_NetDone(); +void HandleIncomingConnection(); #endif diff --git a/src/common/engine/st_start.h b/src/common/engine/st_start.h index 07bc61469..e361a8bf1 100644 --- a/src/common/engine/st_start.h +++ b/src/common/engine/st_start.h @@ -51,15 +51,21 @@ public: virtual ~FStartupScreen() = default; virtual void Progress() {} + virtual void AppendStatusLine(const char* status) {} + virtual void LoadingStatus(const char* message, int colors) {} - virtual void NetInit(const char *message, int num_players) {} - virtual void NetProgress(int count) {} + virtual void NetInit(const char* message, bool host) {} + virtual void NetMessage(const char* message) {} + virtual void NetConnect(int client, const char* name, unsigned flags, int status) {} + virtual void NetUpdate(int client, int status) {} + virtual void NetDisconnect(int client) {} + virtual void NetProgress(int cur, int limit) {} virtual void NetDone() {} virtual void NetClose() {} virtual bool ShouldStartNet() { return false; } - virtual bool NetLoop(bool (*timer_callback)(void *), void *userdata) { return false; } - virtual void AppendStatusLine(const char* status) {} - virtual void LoadingStatus(const char* message, int colors) {} + virtual int GetNetKickClient() { return -1; } + virtual int GetNetBanClient() { return -1; } + virtual bool NetLoop(bool (*loopCallback)(void *), void *data) { return false; } protected: int MaxPos, CurPos, NotchPos; @@ -71,14 +77,21 @@ public: FBasicStartupScreen(int max_progress); ~FBasicStartupScreen(); - void Progress(); - void NetInit(const char* message, int num_players); - void NetProgress(int count); - void NetMessage(const char* format, ...); // cover for printf - void NetDone(); - void NetClose(); + void Progress() override; + + void NetInit(const char* message, bool host) override; + void NetMessage(const char* message) override; + void NetConnect(int client, const char* name, unsigned flags, int status) override; + void NetUpdate(int client, int status) override; + void NetDisconnect(int client) override; + void NetProgress(int cur, int limit) override; + void NetDone() override; + void NetClose() override; bool ShouldStartNet() override; - bool NetLoop(bool (*timer_callback)(void*), void* userdata); + int GetNetKickClient() override; + int GetNetBanClient() override; + bool NetLoop(bool (*loopCallback)(void*), void* data) override; + protected: int NetMaxPos, NetCurPos; }; diff --git a/src/common/platform/posix/cocoa/st_console.h b/src/common/platform/posix/cocoa/st_console.h index 8b35989f8..e56bb929e 100644 --- a/src/common/platform/posix/cocoa/st_console.h +++ b/src/common/platform/posix/cocoa/st_console.h @@ -63,11 +63,18 @@ public: // FStartupScreen functionality void Progress(int current, int maximum); - void NetInit(const char* message, int playerCount); - void NetProgress(int count); + + void NetInit(const char* const message, const bool host); + void NetMessage(const char* const message); + void NetConnect(const int client, const char* const name, const unsigned flags, const int status); + void NetUpdate(const int client, const int status); + void NetDisconnect(const int client); + void NetProgress(const int cur, const int limit); void NetDone(); void NetClose(); bool ShouldStartNet(); + int GetNetKickClient(); + int GetNetBanClient(); private: NSWindow* m_window; diff --git a/src/common/platform/posix/cocoa/st_console.mm b/src/common/platform/posix/cocoa/st_console.mm index ce352ca72..2c0611530 100644 --- a/src/common/platform/posix/cocoa/st_console.mm +++ b/src/common/platform/posix/cocoa/st_console.mm @@ -417,7 +417,7 @@ void FConsoleWindow::Progress(const int current, const int maximum) } -void FConsoleWindow::NetInit(const char* const message, const int playerCount) +void FConsoleWindow::NetInit(const char* const message, const bool host) { if (nil == m_netView) { @@ -442,19 +442,9 @@ void FConsoleWindow::NetInit(const char* const message, const int playerCount) // Connection progress m_netProgressBar = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(12.0f, 40.0f, 488.0f, 16.0f)]; [m_netProgressBar setAutoresizingMask:NSViewWidthSizable]; - [m_netProgressBar setMaxValue:playerCount]; - - if (0 == playerCount) - { - // Joining game - [m_netProgressBar setIndeterminate:YES]; - [m_netProgressBar startAnimation:nil]; - } - else - { - // Hosting game - [m_netProgressBar setIndeterminate:NO]; - } + [m_netProgressBar setMaxValue:0]; + [m_netProgressBar setIndeterminate:YES]; + [m_netProgressBar startAnimation:nil]; // Cancel network game button m_netAbortButton = [[NSButton alloc] initWithFrame:NSMakeRect(432.0f, 8.0f, 72.0f, 28.0f)]; @@ -486,22 +476,32 @@ void FConsoleWindow::NetInit(const char* const message, const int playerCount) [m_netMessageText setStringValue:[NSString stringWithUTF8String:message]]; m_netCurPos = 0; - m_netMaxPos = playerCount; - - NetProgress(1); // You always know about yourself } -void FConsoleWindow::NetProgress(const int count) +void FConsoleWindow::NetMessage(const char* const message) { - if (0 == count) - { - ++m_netCurPos; - } - else - { - m_netCurPos = count; - } + [m_netMessageText setStringValue:[NSString stringWithUTF8String:message]]; +} +void FConsoleWindow::NetConnect(const int client, const char* const name, const unsigned flags, const int status) +{ + +} + +void FConsoleWindow::NetUpdate(const int client, const int status) +{ + +} + +void FConsoleWindow::NetDisconnect(const int client) +{ + +} + +void FConsoleWindow::NetProgress(const int cur, const int limit) +{ + m_netCurPos = cur; + m_netMaxPos = limit; if (nil == m_netView) { return; @@ -541,3 +541,13 @@ bool FConsoleWindow::ShouldStartNet() { return false; } + +int FConsoleWindow::GetNetKickClient() +{ + return -1; +} + +int FConsoleWindow::GetNetBanClient() +{ + return -1; +} diff --git a/src/common/platform/posix/cocoa/st_start.mm b/src/common/platform/posix/cocoa/st_start.mm index 948e9f207..87eae4b4f 100644 --- a/src/common/platform/posix/cocoa/st_start.mm +++ b/src/common/platform/posix/cocoa/st_start.mm @@ -95,14 +95,34 @@ void FBasicStartupScreen::Progress() } -void FBasicStartupScreen::NetInit(const char* const message, const int playerCount) +void FBasicStartupScreen::NetInit(const char* const message, const bool host) { - FConsoleWindow::GetInstance().NetInit(message, playerCount); + FConsoleWindow::GetInstance().NetInit(message, host); } -void FBasicStartupScreen::NetProgress(const int count) +void FBasicStartupScreen::NetMessage(const char* const message) { - FConsoleWindow::GetInstance().NetProgress(count); + FConsoleWindow::GetInstance().NetMessage(message); +} + +void FBasicStartupScreen::NetConnect(const int client, const char* const name, const unsigned flags, const int status) +{ + FConsoleWindow::GetInstance().NetConnect(client, name, flags, status); +} + +void FBasicStartupScreen::NetUpdate(const int client, const int status) +{ + FConsoleWindow::GetInstance().NetUpdate(client, status); +} + +void FBasicStartupScreen::NetDisconnect(const int client) +{ + FConsoleWindow::GetInstance().NetDisconnect(client); +} + +void FBasicStartupScreen::NetProgress(const int cur, const int limit) +{ + FConsoleWindow::GetInstance().NetProgress(cur, limit); } void FBasicStartupScreen::NetDone() @@ -120,11 +140,21 @@ bool FBasicStartupScreen::ShouldStartNet() return FConsoleWindow::GetInstance().ShouldStartNet(); } -bool FBasicStartupScreen::NetLoop(bool (*timerCallback)(void*), void* const userData) +int FBasicStartupScreen::GetNetKickClient() +{ + return FConsoleWindow::GetInstance().GetNetKickClient(); +} + +int FBasicStartupScreen::GetNetBanClient() +{ + return FConsoleWindow::GetInstance().GetNetBanClient(); +} + +bool FBasicStartupScreen::NetLoop(bool (*loopCallback)(void*), void* const data) { while (true) { - if (timerCallback(userData)) + if (loopCallback(data)) { break; } diff --git a/src/common/platform/posix/sdl/st_start.cpp b/src/common/platform/posix/sdl/st_start.cpp index 5efd4200f..b1f0914a6 100644 --- a/src/common/platform/posix/sdl/st_start.cpp +++ b/src/common/platform/posix/sdl/st_start.cpp @@ -53,13 +53,21 @@ class FTTYStartupScreen : public FStartupScreen FTTYStartupScreen(int max_progress); ~FTTYStartupScreen(); - void Progress(); - void NetInit(const char *message, int num_players); - void NetProgress(int count); - void NetDone(); - void NetClose(); - bool ShouldStartNet(); - bool NetLoop(bool (*timer_callback)(void *), void *userdata); + void Progress() override; + + void NetInit(const char* message, bool host) override; + void NetMessage(const char* message) override; + void NetConnect(int client, const char* name, unsigned flags, int status) override; + void NetUpdate(int client, int status) override; + void NetDisconnect(int client) override; + void NetProgress(int cur, int limit) override; + void NetDone() override; + void NetClose() override; + bool ShouldStartNet() override; + int GetNetKickClient() override; + int GetNetBanClient() override; + bool NetLoop(bool (*loopCallback)(void*), void* data) override; + protected: bool DidNetInit; int NetMaxPos, NetCurPos; @@ -147,7 +155,7 @@ void FTTYStartupScreen::Progress() // //=========================================================================== -void FTTYStartupScreen::NetInit(const char *message, int numplayers) +void FTTYStartupScreen::NetInit(const char* message, bool host) { if (!DidNetInit) { @@ -163,20 +171,63 @@ void FTTYStartupScreen::NetInit(const char *message, int numplayers) tcsetattr (STDIN_FILENO, TCSANOW, &rawtermios); DidNetInit = true; } - if (numplayers == 1) - { - // Status message without any real progress info. - fprintf (stderr, "\n%s.", message); - } - else - { - fprintf (stderr, "\n%s: ", message); - } + fprintf(stderr, "\n%s.", message); fflush (stderr); TheNetMessage = message; - NetMaxPos = numplayers; NetCurPos = 0; - NetProgress(1); // You always know about yourself +} + +void FTTYStartupScreen::NetMessage(const char* message) +{ + TheNetMessage = message; +} + +void FTTYStartupScreen::NetConnect(int client, const char* name, unsigned flags, int status) +{ + +} + +void FTTYStartupScreen::NetUpdate(int client, int status) +{ + +} + +void FTTYStartupScreen::NetDisconnect(int client) +{ + +} + +//=========================================================================== +// +// FTTYStartupScreen :: NetProgress +// +// Sets the network progress meter. +// +//=========================================================================== + +void FTTYStartupScreen::NetProgress(int cur, int limit) +{ + int i; + + NetMaxPos = limit; + NetCurPos = cur; + if (NetMaxPos == 0) + { + // Spinny-type progress meter, because we're a guest waiting for the host. + fprintf(stderr, "\r%s: %c", TheNetMessage, SpinnyProgressChars[NetCurPos & 3]); + fflush(stderr); + } + else if (NetMaxPos > 1) + { + // Dotty-type progress meter. + fprintf(stderr, "\r%s: ", TheNetMessage); + for (i = 0; i < NetCurPos; ++i) + { + fputc('.', stderr); + } + fprintf(stderr, "%*c[%2d/%2d]", NetMaxPos + 1 - NetCurPos, ' ', NetCurPos, NetMaxPos); + fflush(stderr); + } } //=========================================================================== @@ -199,46 +250,6 @@ void FTTYStartupScreen::NetDone() } } -//=========================================================================== -// -// FTTYStartupScreen :: NetProgress -// -// Sets the network progress meter. If count is 0, it gets bumped by 1. -// Otherwise, it is set to count. -// -//=========================================================================== - -void FTTYStartupScreen::NetProgress(int count) -{ - int i; - - if (count == 0) - { - NetCurPos++; - } - else if (count > 0) - { - NetCurPos = count; - } - if (NetMaxPos == 0) - { - // Spinny-type progress meter, because we're a guest waiting for the host. - fprintf (stderr, "\r%s: %c", TheNetMessage, SpinnyProgressChars[NetCurPos & 3]); - fflush (stderr); - } - else if (NetMaxPos > 1) - { - // Dotty-type progress meter. - fprintf (stderr, "\r%s: ", TheNetMessage); - for (i = 0; i < NetCurPos; ++i) - { - fputc ('.', stderr); - } - fprintf (stderr, "%*c[%2d/%2d]", NetMaxPos + 1 - NetCurPos, ' ', NetCurPos, NetMaxPos); - fflush (stderr); - } -} - void FTTYStartupScreen::NetClose() { // TODO: Implement this @@ -249,6 +260,16 @@ bool FTTYStartupScreen::ShouldStartNet() return false; } +int FTTYStartupScreen::GetNetKickClient() +{ + return -1; +} + +int FTTYStartupScreen::GetNetBanClient() +{ + return -1; +} + //=========================================================================== // // FTTYStartupScreen :: NetLoop @@ -263,7 +284,7 @@ bool FTTYStartupScreen::ShouldStartNet() // //=========================================================================== -bool FTTYStartupScreen::NetLoop(bool (*timer_callback)(void *), void *userdata) +bool FTTYStartupScreen::NetLoop(bool (*loopCallback)(void *), void *data) { fd_set rfds; struct timeval tv; @@ -291,7 +312,7 @@ bool FTTYStartupScreen::NetLoop(bool (*timer_callback)(void *), void *userdata) } else if (retval == 0) { - if (timer_callback (userdata)) + if (loopCallback (data)) { fputc ('\n', stderr); return true; diff --git a/src/common/platform/win32/i_mainwindow.cpp b/src/common/platform/win32/i_mainwindow.cpp index be7d0f796..c760b96ba 100644 --- a/src/common/platform/win32/i_mainwindow.cpp +++ b/src/common/platform/win32/i_mainwindow.cpp @@ -118,34 +118,64 @@ void MainWindow::ShowErrorPane(const char* text) restartrequest = ErrorWindow::ExecModal(text, alltext); } -void MainWindow::ShowNetStartPane(const char* message, int maxpos) +void MainWindow::NetInit(const char* message, bool host) { - NetStartWindow::ShowNetStartPane(message, maxpos); + NetStartWindow::NetInit(message, host); } -void MainWindow::HideNetStartPane() +void MainWindow::NetMessage(const char* message) { - NetStartWindow::HideNetStartPane(); + NetStartWindow::NetMessage(message); } -void MainWindow::CloseNetStartPane() +void MainWindow::NetConnect(int client, const char* name, unsigned flags, int status) +{ + NetStartWindow::NetConnect(client, name, flags, status); +} + +void MainWindow::NetUpdate(int client, int status) +{ + NetStartWindow::NetUpdate(client, status); +} + +void MainWindow::NetDisconnect(int client) +{ + NetStartWindow::NetDisconnect(client); +} + +void MainWindow::NetProgress(int cur, int limit) +{ + NetStartWindow::NetProgress(cur, limit); +} + +void MainWindow::NetDone() +{ + NetStartWindow::NetDone(); +} + +void MainWindow::NetClose() { NetStartWindow::NetClose(); } -bool MainWindow::ShouldStartNetGame() +bool MainWindow::ShouldStartNet() { - return NetStartWindow::ShouldStartNetGame(); + return NetStartWindow::ShouldStartNet(); } -void MainWindow::SetNetStartProgress(int pos) +int MainWindow::GetNetKickClient() { - NetStartWindow::SetNetStartProgress(pos); + return NetStartWindow::GetNetKickClient(); } -bool MainWindow::RunMessageLoop(bool (*timer_callback)(void*), void* userdata) +int MainWindow::GetNetBanClient() { - return NetStartWindow::RunMessageLoop(timer_callback, userdata); + return NetStartWindow::GetNetBanClient(); +} + +bool MainWindow::NetLoop(bool (*loopCallback)(void*), void* data) +{ + return NetStartWindow::NetLoop(loopCallback, data); } bool MainWindow::CheckForRestart() diff --git a/src/common/platform/win32/i_mainwindow.h b/src/common/platform/win32/i_mainwindow.h index e2056c674..720e9155e 100644 --- a/src/common/platform/win32/i_mainwindow.h +++ b/src/common/platform/win32/i_mainwindow.h @@ -25,12 +25,18 @@ public: void PrintStr(const char* cp); void GetLog(std::function writeFile); - void ShowNetStartPane(const char* message, int maxpos); - void SetNetStartProgress(int pos); - bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); - void HideNetStartPane(); - void CloseNetStartPane(); - bool ShouldStartNetGame(); + void NetInit(const char* message, bool host); + void NetMessage(const char* message); + void NetConnect(int client, const char* name, unsigned flags, int status); + void NetUpdate(int client, int status); + void NetDisconnect(int client); + void NetProgress(int cur, int limit); + void NetDone(); + void NetClose(); + bool ShouldStartNet(); + int GetNetKickClient(); + int GetNetBanClient(); + bool NetLoop(bool (*loopCallback)(void*), void* data); void SetWindowTitle(const char* caption); diff --git a/src/common/platform/win32/st_start.cpp b/src/common/platform/win32/st_start.cpp index 6fb5a5f95..70868b583 100644 --- a/src/common/platform/win32/st_start.cpp +++ b/src/common/platform/win32/st_start.cpp @@ -137,14 +137,46 @@ void FBasicStartupScreen::Progress() // //========================================================================== -void FBasicStartupScreen::NetInit(const char *message, int numplayers) +void FBasicStartupScreen::NetInit(const char *message, bool host) { - NetMaxPos = numplayers; - mainwindow.ShowNetStartPane(message, numplayers); - - NetMaxPos = numplayers; + mainwindow.NetInit(message, host); NetCurPos = 0; - NetProgress(1); // You always know about yourself +} + +void FBasicStartupScreen::NetMessage(const char* message) +{ + mainwindow.NetMessage(message); +} + +void FBasicStartupScreen::NetConnect(int client, const char* name, unsigned flags, int status) +{ + mainwindow.NetConnect(client, name, flags, status); +} + +void FBasicStartupScreen::NetUpdate(int client, int status) +{ + mainwindow.NetUpdate(client, status); +} + +void FBasicStartupScreen::NetDisconnect(int client) +{ + mainwindow.NetDisconnect(client); +} + +//========================================================================== +// +// FBasicStartupScreen :: NetProgress +// +// Sets the network progress meter. If count is 0, it gets bumped by 1. +// Otherwise, it is set to count. +// +//========================================================================== + +void FBasicStartupScreen::NetProgress(int cur, int limit) +{ + NetMaxPos = limit; + NetCurPos = cur; + mainwindow.NetProgress(cur, limit); } //========================================================================== @@ -157,30 +189,27 @@ void FBasicStartupScreen::NetInit(const char *message, int numplayers) void FBasicStartupScreen::NetDone() { - mainwindow.HideNetStartPane(); + mainwindow.NetDone(); } -//========================================================================== -// -// FBasicStartupScreen :: NetProgress -// -// Sets the network progress meter. If count is 0, it gets bumped by 1. -// Otherwise, it is set to count. -// -//========================================================================== - -void FBasicStartupScreen::NetProgress(int count) +void FBasicStartupScreen::NetClose() { - if (count == 0) - { - NetCurPos++; - } - else - { - NetCurPos = count; - } + mainwindow.NetClose(); +} - mainwindow.SetNetStartProgress(count); +bool FBasicStartupScreen::ShouldStartNet() +{ + return mainwindow.ShouldStartNet(); +} + +int FBasicStartupScreen::GetNetKickClient() +{ + return mainwindow.GetNetKickClient(); +} + +int FBasicStartupScreen::GetNetBanClient() +{ + return mainwindow.GetNetBanClient(); } //========================================================================== @@ -197,17 +226,7 @@ void FBasicStartupScreen::NetProgress(int count) // //========================================================================== -bool FBasicStartupScreen::NetLoop(bool (*timer_callback)(void *), void *userdata) +bool FBasicStartupScreen::NetLoop(bool (*loopCallback)(void*), void* data) { - return mainwindow.RunMessageLoop(timer_callback, userdata); -} - -void FBasicStartupScreen::NetClose() -{ - mainwindow.CloseNetStartPane(); -} - -bool FBasicStartupScreen::ShouldStartNet() -{ - return mainwindow.ShouldStartNetGame(); + return mainwindow.NetLoop(loopCallback, data); } diff --git a/src/common/widgets/netstartwindow.cpp b/src/common/widgets/netstartwindow.cpp index 1a8845961..674a119ee 100644 --- a/src/common/widgets/netstartwindow.cpp +++ b/src/common/widgets/netstartwindow.cpp @@ -5,45 +5,141 @@ #include "gstrings.h" #include #include +#include #include NetStartWindow* NetStartWindow::Instance = nullptr; -void NetStartWindow::ShowNetStartPane(const char* message, int maxpos) +void NetStartWindow::NetInit(const char* message, bool host) { Size screenSize = GetScreenSize(); - double windowWidth = 300.0; - double windowHeight = 150.0; + double windowWidth = 450.0; + double windowHeight = 600.0; if (!Instance) { - Instance = new NetStartWindow(); + Instance = new NetStartWindow(host); Instance->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight); Instance->Show(); } - Instance->SetMessage(message, maxpos); + Instance->SetMessage(message); } -void NetStartWindow::HideNetStartPane() +void NetStartWindow::NetMessage(const char* message) +{ + if (Instance) + Instance->SetMessage(message); +} + +void NetStartWindow::NetConnect(int client, const char* name, unsigned flags, int status) +{ + if (!Instance) + return; + + std::string value = ""; + if (flags & 1) + value.append("*"); + if (flags & 2) + value.append("H"); + + Instance->LobbyWindow->UpdateItem(value, client, 1); + Instance->LobbyWindow->UpdateItem(name, client, 2); + + value = ""; + if (status == 1) + value = "CONNECTING"; + else if (status == 2) + value = "WAITING"; + else if (status == 3) + value = "READY"; + + Instance->LobbyWindow->UpdateItem(value, client, 3); +} + +void NetStartWindow::NetUpdate(int client, int status) +{ + if (!Instance) + return; + + std::string value = ""; + if (status == 1) + value = "CONNECTING"; + else if (status == 2) + value = "WAITING"; + else if (status == 3) + value = "READY"; + + Instance->LobbyWindow->UpdateItem(value, client, 3); +} + +void NetStartWindow::NetDisconnect(int client) +{ + if (Instance) + { + for (size_t i = 1u; i < Instance->LobbyWindow->GetColumnAmount(); ++i) + Instance->LobbyWindow->UpdateItem("", client, i); + } +} + +void NetStartWindow::NetProgress(int cur, int limit) +{ + if (!Instance) + return; + + Instance->maxpos = limit; + Instance->SetProgress(cur); + for (size_t start = Instance->LobbyWindow->GetItemAmount(); start < Instance->maxpos; ++start) + Instance->LobbyWindow->AddItem(std::to_string(start)); +} + +void NetStartWindow::NetDone() { delete Instance; Instance = nullptr; } -void NetStartWindow::SetNetStartProgress(int pos) +void NetStartWindow::NetClose() { - if (Instance) - Instance->SetProgress(pos); + if (Instance != nullptr) + Instance->OnClose(); } -bool NetStartWindow::RunMessageLoop(bool (*newtimer_callback)(void*), void* newuserdata) +bool NetStartWindow::ShouldStartNet() +{ + if (Instance != nullptr) + return Instance->shouldstart; + + return false; +} + +int NetStartWindow::GetNetKickClient() +{ + if (!Instance || !Instance->kickclients.size()) + return -1; + + int next = Instance->kickclients.back(); + Instance->kickclients.pop_back(); + return next; +} + +int NetStartWindow::GetNetBanClient() +{ + if (!Instance || !Instance->banclients.size()) + return -1; + + int next = Instance->banclients.back(); + Instance->banclients.pop_back(); + return next; +} + +bool NetStartWindow::NetLoop(bool (*loopCallback)(void*), void* data) { if (!Instance) return false; - Instance->timer_callback = newtimer_callback; - Instance->userdata = newuserdata; + Instance->timer_callback = loopCallback; + Instance->userdata = data; Instance->CallbackException = {}; DisplayWindow::RunLoop(); @@ -57,21 +153,7 @@ bool NetStartWindow::RunMessageLoop(bool (*newtimer_callback)(void*), void* newu return Instance->exitreason; } -void NetStartWindow::NetClose() -{ - if (Instance != nullptr) - Instance->OnClose(); -} - -bool NetStartWindow::ShouldStartNetGame() -{ - if (Instance != nullptr) - return Instance->shouldstart; - - return false; -} - -NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) +NetStartWindow::NetStartWindow(bool host) : Widget(nullptr, WidgetType::Window) { SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51)); @@ -81,27 +163,43 @@ NetStartWindow::NetStartWindow() : Widget(nullptr, WidgetType::Window) MessageLabel = new TextLabel(this); ProgressLabel = new TextLabel(this); + LobbyWindow = new ListView(this); AbortButton = new PushButton(this); - ForceStartButton = new PushButton(this); MessageLabel->SetTextAlignment(TextLabelAlignment::Center); ProgressLabel->SetTextAlignment(TextLabelAlignment::Center); AbortButton->OnClick = [=]() { OnClose(); }; - ForceStartButton->OnClick = [=]() { ForceStart(); }; - AbortButton->SetText("Abort"); - ForceStartButton->SetText("Start Game"); + + if (host) + { + hosting = true; + + ForceStartButton = new PushButton(this); + ForceStartButton->OnClick = [=]() { ForceStart(); }; + ForceStartButton->SetText("Start Game"); + + KickButton = new PushButton(this); + KickButton->OnClick = [=]() { OnKick(); }; + KickButton->SetText("Kick"); + + BanButton = new PushButton(this); + BanButton->OnClick = [=]() { OnBan(); }; + BanButton->SetText("Ban"); + } + + // Client number, flags, name, status. + LobbyWindow->SetColumnWidths({ 30.0, 30.0, 200.0, 50.0 }); CallbackTimer = new Timer(this); CallbackTimer->FuncExpired = [=]() { OnCallbackTimerExpired(); }; CallbackTimer->Start(500); } -void NetStartWindow::SetMessage(const std::string& message, int newmaxpos) +void NetStartWindow::SetMessage(const std::string& message) { MessageLabel->SetText(message); - maxpos = newmaxpos; } void NetStartWindow::SetProgress(int newpos) @@ -126,6 +224,36 @@ void NetStartWindow::ForceStart() shouldstart = true; } +void NetStartWindow::OnKick() +{ + int item = LobbyWindow->GetSelectedItem(); + + size_t i = 0u; + for (; i < kickclients.size(); ++i) + { + if (kickclients[i] == item) + break; + } + + if (i >= kickclients.size()) + kickclients.push_back(item); +} + +void NetStartWindow::OnBan() +{ + int item = LobbyWindow->GetSelectedItem(); + + size_t i = 0u; + for (; i < banclients.size(); ++i) + { + if (banclients[i] == item) + break; + } + + if (i >= banclients.size()) + banclients.push_back(item); +} + void NetStartWindow::OnGeometryChanged() { double w = GetWidth(); @@ -138,11 +266,23 @@ void NetStartWindow::OnGeometryChanged() labelheight = ProgressLabel->GetPreferredHeight(); ProgressLabel->SetFrameGeometry(Rect::xywh(5.0, y, w - 10.0, labelheight)); - y += labelheight; + y += labelheight + 5.0; + + labelheight = (GetHeight() - 30.0 - AbortButton->GetPreferredHeight()) - y; + LobbyWindow->SetFrameGeometry(Rect::xywh(5.0, y, w - 10.0, labelheight)); y = GetHeight() - 15.0 - AbortButton->GetPreferredHeight(); - AbortButton->SetFrameGeometry((w + 10.0) * 0.5, y, 100.0, AbortButton->GetPreferredHeight()); - ForceStartButton->SetFrameGeometry((w - 210.0) * 0.5, y, 100.0, ForceStartButton->GetPreferredHeight()); + if (hosting) + { + AbortButton->SetFrameGeometry((w + 215.0) * 0.5, y, 100.0, AbortButton->GetPreferredHeight()); + BanButton->SetFrameGeometry((w + 5.0) * 0.5, y, 100.0, BanButton->GetPreferredHeight()); + KickButton->SetFrameGeometry((w - 205.0) * 0.5, y, 100.0, KickButton->GetPreferredHeight()); + ForceStartButton->SetFrameGeometry((w - 415.0) * 0.5, y, 100.0, ForceStartButton->GetPreferredHeight()); + } + else + { + AbortButton->SetFrameGeometry((w - 100.0) * 0.5, y, 100.0, AbortButton->GetPreferredHeight()); + } } void NetStartWindow::OnCallbackTimerExpired() diff --git a/src/common/widgets/netstartwindow.h b/src/common/widgets/netstartwindow.h index b4189af8a..92018a4d0 100644 --- a/src/common/widgets/netstartwindow.h +++ b/src/common/widgets/netstartwindow.h @@ -4,37 +4,49 @@ #include class TextLabel; +class ListView; class PushButton; class Timer; class NetStartWindow : public Widget { public: - static void ShowNetStartPane(const char* message, int maxpos); - static void HideNetStartPane(); - static void SetNetStartProgress(int pos); - static bool RunMessageLoop(bool (*timer_callback)(void*), void* userdata); + static void NetInit(const char* message, bool host); + static void NetMessage(const char* message); + static void NetConnect(int client, const char* name, unsigned flags, int status); + static void NetUpdate(int client, int status); + static void NetDisconnect(int client); + static void NetProgress(int cur, int limit); + static void NetDone(); static void NetClose(); - static bool ShouldStartNetGame(); + static bool ShouldStartNet(); + static int GetNetKickClient(); + static int GetNetBanClient(); + static bool NetLoop(bool (*timer_callback)(void*), void* userdata); private: - NetStartWindow(); + NetStartWindow(bool host); - void SetMessage(const std::string& message, int maxpos); + void SetMessage(const std::string& message); void SetProgress(int pos); protected: void OnClose() override; void OnGeometryChanged() override; virtual void ForceStart(); + virtual void OnKick(); + virtual void OnBan(); private: void OnCallbackTimerExpired(); TextLabel* MessageLabel = nullptr; TextLabel* ProgressLabel = nullptr; + ListView* LobbyWindow = nullptr; PushButton* AbortButton = nullptr; PushButton* ForceStartButton = nullptr; + PushButton* KickButton = nullptr; + PushButton* BanButton = nullptr; Timer* CallbackTimer = nullptr; @@ -45,6 +57,9 @@ private: bool exitreason = false; bool shouldstart = false; + bool hosting = false; + std::vector kickclients; + std::vector banclients; std::exception_ptr CallbackException; diff --git a/src/d_net.cpp b/src/d_net.cpp index 1c9ca9202..856552682 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -87,12 +87,6 @@ extern bool AppActive; void P_ClearLevelInterpolation(); -struct FNetGameInfo -{ - uint32_t DetectedPlayers[MAXPLAYERS]; - uint8_t GotSetup[MAXPLAYERS]; -}; - enum ELevelStartStatus { LST_READY, @@ -107,8 +101,6 @@ enum ELevelStartStatus // // A world tick cannot be ran until CurrentSequence >= gametic for all clients. -#define NetBuffer (doomcom.data) -ENetMode NetMode = NET_PeerToPeer; int ClientTic = 0; usercmd_t LocalCmds[LOCALCMDTICS] = {}; int LastSentConsistency = 0; // Last consistency we sent out. If < CurrentConsistency, send them out. @@ -124,12 +116,12 @@ static uint8_t LocalNetBuffer[MAX_MSGLEN] = {}; static uint8_t CurrentLobbyID = 0u; // Ignore commands not from this lobby (useful when transitioning levels). static int LastGameUpdate = 0; // Track the last time the game actually ran the world. -static int MutedClients = 0; // Ignore messages from these clients. +static uint64_t MutedClients = 0u; // Ignore messages from these clients. static int LevelStartDebug = 0; static int LevelStartDelay = 0; // While this is > 0, don't start generating packets yet. static ELevelStartStatus LevelStartStatus = LST_READY; // Listen for when to actually start making tics. -static int LevelStartAck = 0; // Used by the host to determine if everyone has loaded in. +static uint64_t LevelStartAck = 0u; // Used by the host to determine if everyone has loaded in. static int FullLatencyCycle = MAXSENDTICS * 3; // Give ~3 seconds to gather latency info about clients on boot up. static int LastLatencyUpdate = 0; // Update average latency every ~1 second. @@ -146,7 +138,6 @@ void D_ProcessEvents(void); void G_BuildTiccmd(usercmd_t *cmd); void D_DoAdvanceDemo(void); -static void SendSetup(const FNetGameInfo& info, int len); static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always); extern bool advancedemo; @@ -231,14 +222,14 @@ public: void ResetStream() { - CurrentClientTic = ClientTic / doomcom.ticdup; + CurrentClientTic = ClientTic / TicDup; CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream; CurrentSize = 0; } void NewClientTic() { - const int tic = ClientTic / doomcom.ticdup; + const int tic = ClientTic / TicDup; if (CurrentClientTic == tic) return; @@ -342,14 +333,14 @@ void Net_ClearBuffers() state.Tics[j].Data.SetData(nullptr, 0); } - doomcom.command = doomcom.datalength = 0; - doomcom.remoteplayer = -1; - doomcom.numplayers = doomcom.ticdup = 1; - consoleplayer = doomcom.consoleplayer = 0; - LocalNetBufferSize = 0; + NetBufferLength = 0u; + RemoteClient = -1; + MaxClients = TicDup = 1u; + consoleplayer = 0; + LocalNetBufferSize = 0u; Net_Arbitrator = 0; - MutedClients = 0; + MutedClients = 0u; CurrentLobbyID = 0u; NetworkClients.Clear(); NetMode = NET_PeerToPeer; @@ -360,7 +351,7 @@ void Net_ClearBuffers() SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; NetEvents.ResetStream(); - LevelStartAck = 0; + LevelStartAck = 0u; LevelStartDelay = LevelStartDebug = 0; LevelStartStatus = LST_READY; @@ -377,17 +368,17 @@ void Net_ResetCommands(bool midTic) ++CurrentLobbyID; SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; - int tic = gametic / doomcom.ticdup; + int tic = gametic / TicDup; if (midTic) { // If we're mid ticdup cycle, make sure we immediately enter the next one after // the current tic we're in finishes. - ClientTic = (tic + 1) * doomcom.ticdup; - gametic = (tic * doomcom.ticdup) + (doomcom.ticdup - 1); + ClientTic = (tic + 1) * TicDup; + gametic = (tic * TicDup) + (TicDup - 1); } else { - ClientTic = gametic = tic * doomcom.ticdup; + ClientTic = gametic = tic * TicDup; --tic; } @@ -420,17 +411,17 @@ void Net_SetWaiting() static int GetNetBufferSize() { if (NetBuffer[0] & NCMD_EXIT) - return 1 + (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator); + return 1 + (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator); // TODO: Need a skipper for this. if (NetBuffer[0] & NCMD_SETUP) - return doomcom.datalength; + return NetBufferLength; if (NetBuffer[0] & (NCMD_LATENCY | NCMD_LATENCYACK)) return 2; if (NetBuffer[0] & NCMD_LEVELREADY) { int bytes = 2; - if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) bytes += 2; return bytes; @@ -448,23 +439,23 @@ static int GetNetBufferSize() const int ranTics = NetBuffer[totalBytes++]; if (ranTics > 0) totalBytes += 4; - if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) ++totalBytes; // Minimum additional packet size per player: // 1 byte for player number // If in packet server mode and from the host, 2 bytes for the latency to the host int padding = 1; - if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) padding += 2; - if (doomcom.datalength < totalBytes + playerCount * padding) + if (NetBufferLength < totalBytes + playerCount * padding) return totalBytes + playerCount * padding; uint8_t* skipper = &NetBuffer[totalBytes]; for (int p = 0; p < playerCount; ++p) { ++skipper; - if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator) + if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) skipper += 2; for (int i = 0; i < ranTics; ++i) @@ -489,8 +480,8 @@ static void HSendPacket(int client, size_t size) if (demoplayback) return; - doomcom.remoteplayer = client; - doomcom.datalength = size; + RemoteClient = client; + NetBufferLength = size; if (client == consoleplayer) { memcpy(LocalNetBuffer, NetBuffer, size); @@ -501,8 +492,7 @@ static void HSendPacket(int client, size_t size) if (!netgame) I_Error("Tried to send a packet to a client while offline"); - doomcom.command = CMD_SEND; - I_NetCmd(); + I_NetCmd(CMD_SEND); } // HGetPacket @@ -515,34 +505,23 @@ static bool HGetPacket() if (LocalNetBufferSize) { memcpy(NetBuffer, LocalNetBuffer, LocalNetBufferSize); - doomcom.datalength = LocalNetBufferSize; - doomcom.remoteplayer = consoleplayer; - LocalNetBufferSize = 0; + NetBufferLength = LocalNetBufferSize; + RemoteClient = consoleplayer; + LocalNetBufferSize = 0u; return true; } if (!netgame) return false; - doomcom.command = CMD_GET; - I_NetCmd(); - - if (doomcom.remoteplayer == -1) + I_NetCmd(CMD_GET); + if (RemoteClient == -1) return false; - // When the host randomly drops this can cause issues in packet server mode, so at - // least attempt to recover gracefully. - if (NetMode == NET_PacketServer && doomcom.remoteplayer == Net_Arbitrator - && (NetBuffer[0] & NCMD_EXIT) && doomcom.datalength == 1) - { - NetBuffer[1] = NetworkClients[1]; - doomcom.datalength = 2; - } - int sizeCheck = GetNetBufferSize(); - if (doomcom.datalength != sizeCheck) + if (NetBufferLength != sizeCheck) { - Printf("Incorrect packet size %d (expected %d)\n", doomcom.datalength, sizeCheck); + Printf("Incorrect packet size %d (expected %d)\n", NetBufferLength, sizeCheck); return false; } @@ -560,8 +539,8 @@ static void ClientConnecting(int client) static void DisconnectClient(int clientNum) { NetworkClients -= clientNum; - MutedClients &= ~(1 << clientNum); - I_ClearNode(clientNum); + MutedClients &= ~((uint64_t)1u << clientNum); + I_ClearClient(clientNum); // Capture the pawn leaving in the next world tick. players[clientNum].playerstate = PST_GONE; } @@ -634,21 +613,21 @@ static void CheckLevelStart(int client, int delayTics) if (client == Net_Arbitrator) { - LevelStartAck = 0; + LevelStartAck = 0u; LevelStartStatus = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? LST_HOST : LST_READY; LevelStartDelay = LevelStartDebug = delayTics; LastGameUpdate = EnterTic; return; } - int mask = 0; + uint64_t mask = 0u; for (auto pNum : NetworkClients) { if (pNum != Net_Arbitrator) - mask |= 1 << pNum; + mask |= (uint64_t)1u << pNum; } - LevelStartAck |= 1 << client; + LevelStartAck |= (uint64_t)1u << client; if ((LevelStartAck & mask) == mask && IsMapLoaded()) { // Beyond this point a player is likely lagging out anyway. @@ -707,10 +686,9 @@ struct FLatencyAck static void GetPackets() { TArray latencyAcks = {}; - TArray stuckInLobby = {}; while (HGetPacket()) { - const int clientNum = doomcom.remoteplayer; + const int clientNum = RemoteClient; auto& clientState = ClientStates[clientNum]; if (NetBuffer[0] & NCMD_EXIT) @@ -721,16 +699,7 @@ static void GetPackets() if (NetBuffer[0] & NCMD_SETUP) { - if (NetworkClients.InGame(clientNum)) - { - if (consoleplayer == Net_Arbitrator && stuckInLobby.Find(clientNum) >= stuckInLobby.Size()) - stuckInLobby.Push(clientNum); - } - else - { - ClientConnecting(clientNum); - } - + HandleIncomingConnection(); continue; } @@ -912,12 +881,6 @@ static void GetPackets() NetBuffer[1] = ack.Seq; HSendPacket(ack.Client, 2); } - - for (auto client : stuckInLobby) - { - NetBuffer[0] = NCMD_GAMEREADY; - HSendPacket(client, 1); - } } static void SendHeartbeat() @@ -1033,7 +996,7 @@ static int16_t CalculateConsistency(int client, uint32_t seed) // check, not just the player's x position like BOOM. static void MakeConsistencies() { - if (!netgame || demoplayback || (gametic % doomcom.ticdup) || !IsMapLoaded()) + if (!netgame || demoplayback || (gametic % TicDup) || !IsMapLoaded()) return; const uint32_t rngSum = StaticSumSeeds(); @@ -1057,7 +1020,7 @@ static bool Net_UpdateStatus() // Check against the previous tick in case we're recovering from a huge // system hiccup. If the game has taken too long to update, it's likely // another client is hanging up the game. - if (LastEnterTic - LastGameUpdate >= MAXSENDTICS * doomcom.ticdup) + if (LastEnterTic - LastGameUpdate >= MAXSENDTICS * TicDup) { // Try again in the next MaxDelay tics. LastGameUpdate = EnterTic; @@ -1066,7 +1029,7 @@ static bool Net_UpdateStatus() { // Use a missing packet here to tell the other players to retransmit instead of simply retransmitting our // own data over instantly. This avoids flooding the network at a time where it's not opportune to do so. - const int curTic = gametic / doomcom.ticdup; + const int curTic = gametic / TicDup; for (auto client : NetworkClients) { if (client == consoleplayer) @@ -1125,7 +1088,7 @@ static bool Net_UpdateStatus() else if (consoleplayer == Net_Arbitrator) { // If we're consistenty ahead of the highest sequence player, slow down. - const int curTic = ClientTic / doomcom.ticdup; + const int curTic = ClientTic / TicDup; for (auto client : NetworkClients) { if (client != Net_Arbitrator && (ClientStates[client].Flags & CF_UPDATED)) @@ -1156,7 +1119,7 @@ static bool Net_UpdateStatus() { SkipCommandTimer = 0; if (SkipCommandAmount <= 0) - SkipCommandAmount = lowestDiff * doomcom.ticdup; + SkipCommandAmount = lowestDiff * TicDup; } } else @@ -1219,7 +1182,7 @@ void NetUpdate(int tics) { // If we're the host, idly wait until all packets have arrived. There's no point in predicting since we // know for a fact the game won't be started until everyone is accounted for. (Packet server only) - const int curTic = gametic / doomcom.ticdup; + const int curTic = gametic / TicDup; int lowestSeq = curTic; for (auto client : NetworkClients) { @@ -1241,7 +1204,7 @@ void NetUpdate(int tics) const bool netGood = Net_UpdateStatus(); const int startTic = ClientTic; - tics = min(tics, MAXSENDTICS * doomcom.ticdup); + tics = min(tics, MAXSENDTICS * TicDup); for (int i = 0; i < tics; ++i) { I_StartTic(); @@ -1256,18 +1219,18 @@ void NetUpdate(int tics) } G_BuildTiccmd(&LocalCmds[ClientTic++ % LOCALCMDTICS]); - if (doomcom.ticdup == 1) + if (TicDup == 1) { Net_NewClientTic(); } else { - const int ticDiff = ClientTic % doomcom.ticdup; + const int ticDiff = ClientTic % TicDup; if (ticDiff) { const int startTic = ClientTic - ticDiff; - // Even if we're not sending out inputs, update the local commands so that the doomcom.ticdup + // Even if we're not sending out inputs, update the local commands so that the TicDup // is correctly played back while predicting as best as possible. This will help prevent // minor hitches when playing online. for (int j = ClientTic - 1; j > startTic; --j) @@ -1275,10 +1238,10 @@ void NetUpdate(int tics) } else { - // Gather up the Command across the last doomcom.ticdup number of tics + // Gather up the Command across the last TicDup number of tics // and average them out. These are propagated back to the local // command so that they'll be predicted correctly. - const int lastTic = ClientTic - doomcom.ticdup; + const int lastTic = ClientTic - TicDup; for (int j = ClientTic - 1; j > lastTic; --j) LocalCmds[(j - 1) % LOCALCMDTICS].buttons |= LocalCmds[j % LOCALCMDTICS].buttons; @@ -1289,7 +1252,7 @@ void NetUpdate(int tics) int sidemove = 0; int upmove = 0; - for (int j = 0; j < doomcom.ticdup; ++j) + for (int j = 0; j < TicDup; ++j) { const int mod = (lastTic + j) % LOCALCMDTICS; pitch += LocalCmds[mod].pitch; @@ -1300,14 +1263,14 @@ void NetUpdate(int tics) upmove += LocalCmds[mod].upmove; } - pitch /= doomcom.ticdup; - yaw /= doomcom.ticdup; - roll /= doomcom.ticdup; - forwardmove /= doomcom.ticdup; - sidemove /= doomcom.ticdup; - upmove /= doomcom.ticdup; + pitch /= TicDup; + yaw /= TicDup; + roll /= TicDup; + forwardmove /= TicDup; + sidemove /= TicDup; + upmove /= TicDup; - for (int j = 0; j < doomcom.ticdup; ++j) + for (int j = 0; j < TicDup; ++j) { const int mod = (lastTic + j) % LOCALCMDTICS; LocalCmds[mod].pitch = pitch; @@ -1323,7 +1286,7 @@ void NetUpdate(int tics) } } - const int newestTic = ClientTic / doomcom.ticdup; + const int newestTic = ClientTic / TicDup; if (demoplayback) { // Don't touch net command data while playing a demo, as it'll already exist. @@ -1333,7 +1296,7 @@ void NetUpdate(int tics) return; } - int startSequence = startTic / doomcom.ticdup; + int startSequence = startTic / TicDup; int endSequence = newestTic; int quitters = 0; int quitNums[MAXPLAYERS]; @@ -1342,7 +1305,7 @@ void NetUpdate(int tics) // In packet server mode special handling is used to ensure the host only // sends out available tics when ready instead of constantly shotgunning // them out as they're made locally. - startSequence = gametic / doomcom.ticdup; + startSequence = gametic / TicDup; int lowestSeq = endSequence - 1; for (auto client : NetworkClients) { @@ -1359,7 +1322,7 @@ void NetUpdate(int tics) endSequence = lowestSeq + 1; } - const bool resendOnly = startSequence == endSequence && (ClientTic % doomcom.ticdup); + const bool resendOnly = startSequence == endSequence && (ClientTic % TicDup); for (auto client : NetworkClients) { // If in packet server mode, we don't want to send information to anyone but the host. On the other @@ -1509,8 +1472,8 @@ void NetUpdate(int tics) int curTic = sequenceNum + t, lastTic = curTic - 1; if (playerNums[i] == consoleplayer) { - int realTic = (curTic * doomcom.ticdup) % LOCALCMDTICS; - int realLastTic = (lastTic * doomcom.ticdup) % LOCALCMDTICS; + int realTic = (curTic * TicDup) % LOCALCMDTICS; + int realLastTic = (lastTic * TicDup) % LOCALCMDTICS; // Write out the net events before the user commands so inputs can // be used as a marker for when the given command ends. auto& stream = NetEvents.Streams[curTic % BACKUPTICS]; @@ -1556,291 +1519,78 @@ void NetUpdate(int tics) GetPackets(); } -// User info packets look like this: -// -// One byte set to NCMD_SETUP or NCMD_USERINFO -// One byte for the relevant player number. -// Three bytes for the sender's game version (major, minor, revision). -// Four bytes for the bit mask indicating which players the sender knows about. -// If NCMD_SETUP, one byte indicating the guest got the game setup info. -// Player's user information. -// -// The guests always send NCMD_SETUP packets, and the host always -// sends NCMD_USERINFO packets. -// -// Game info packets look like this: -// -// One byte set to NCMD_GAMEINFO. -// One byte for doomcom.ticdup setting. -// One byte for NetMode setting. -// String with starting map's name. -// The game's RNG seed. -// Remaining game information. -// -// Ready packets look like this: -// -// One byte set to NCMD_GAMEREADY. -// -// Each machine sends user info packets to the host. The host sends user -// info packets back to the other machines as well as game info packets. -// Negotiation is done when all the guests have reported to the host that -// they know about the other nodes. +// These have to be here since they have game-specific data. Only the data +// from the frontend should be put in these, all backend handling should be +// done in the core files. -bool ExchangeNetGameInfo(void *userdata) +void Net_SetupUserInfo() { - FNetGameInfo *data = reinterpret_cast(userdata); - uint8_t *stream = nullptr; - while (HGetPacket()) - { - // For now throw an error until something more complex can be done to handle this case. - if (NetBuffer[0] == NCMD_EXIT) - I_Error("Game unexpectedly ended."); - - if (NetBuffer[0] == NCMD_SETUP || NetBuffer[0] == NCMD_USERINFO) - { - int clientNum = NetBuffer[1]; - data->DetectedPlayers[clientNum] = - (NetBuffer[5] << 24) | (NetBuffer[6] << 16) | (NetBuffer[7] << 8) | NetBuffer[8]; - - if (NetBuffer[0] == NCMD_SETUP) - { - // Sent from guest. - data->GotSetup[clientNum] = NetBuffer[9]; - stream = &NetBuffer[10]; - } - else - { - // Sent from host. - stream = &NetBuffer[9]; - } - - D_ReadUserInfoStrings(clientNum, &stream, false); - if (!NetworkClients.InGame(clientNum)) - { - if (NetBuffer[2] != VER_MAJOR % 255 || NetBuffer[3] != VER_MINOR % 255 || NetBuffer[4] != VER_REVISION % 255) - I_Error("Different " GAMENAME " versions cannot play a net game"); - - NetworkClients += clientNum; - data->DetectedPlayers[consoleplayer] |= 1 << clientNum; - - I_NetMessage("Found %s (%d)", players[clientNum].userinfo.GetName(), clientNum); - } - } - else if (NetBuffer[0] == NCMD_GAMEINFO) - { - data->GotSetup[consoleplayer] = true; - - doomcom.ticdup = clamp(NetBuffer[1], 1, MAXTICDUP); - NetMode = static_cast(NetBuffer[2]); - - stream = &NetBuffer[3]; - startmap = ReadStringConst(&stream); - rngseed = ReadInt32(&stream); - C_ReadCVars(&stream); - } - else if (NetBuffer[0] == NCMD_GAMEREADY) - { - return true; - } - } - - // If everybody already knows everything, it's time to start. - if (consoleplayer == Net_Arbitrator) - { - bool stillWaiting = false; - - // Update this dynamically. - uint32_t allPlayers = 0u; - for (int i = 0; i < doomcom.numplayers; ++i) - allPlayers |= 1 << i; - - for (int i = 0; i < doomcom.numplayers; ++i) - { - if (!data->GotSetup[i] || (data->DetectedPlayers[i] & allPlayers) != allPlayers) - { - stillWaiting = true; - break; - } - } - - if (!stillWaiting) - return true; - } - - NetBuffer[2] = VER_MAJOR % 255; - NetBuffer[3] = VER_MINOR % 255; - NetBuffer[4] = VER_REVISION % 255; - NetBuffer[5] = data->DetectedPlayers[consoleplayer] >> 24; - NetBuffer[6] = data->DetectedPlayers[consoleplayer] >> 16; - NetBuffer[7] = data->DetectedPlayers[consoleplayer] >> 8; - NetBuffer[8] = data->DetectedPlayers[consoleplayer]; - - if (consoleplayer != Net_Arbitrator) - { - // If we're a guest, send our info over to the host. - NetBuffer[0] = NCMD_SETUP; - NetBuffer[1] = consoleplayer; - NetBuffer[9] = data->GotSetup[consoleplayer]; - stream = &NetBuffer[10]; - // If the host already knows we're here, just send over a heartbeat. - if (!(data->DetectedPlayers[Net_Arbitrator] & (1 << consoleplayer))) - { - auto str = D_GetUserInfoStrings(consoleplayer, true); - const size_t userSize = str.Len() + 1; - memcpy(stream, str.GetChars(), userSize); - stream += userSize; - } - else - { - *stream = 0; - ++stream; - } - } - else - { - // If we're the host, send over known player data to guests. This is done instantly - // since the game info will also get sent out after this. - NetBuffer[0] = NCMD_USERINFO; - for (auto client : NetworkClients) - { - if (client == Net_Arbitrator) - continue; - - for (auto cl : NetworkClients) - { - if (cl == client) - continue; - - // If the host knows about a client that the guest doesn't, send that client's info over to them. - const int clBit = 1 << cl; - if ((data->DetectedPlayers[Net_Arbitrator] & clBit) && !(data->DetectedPlayers[client] & clBit)) - { - NetBuffer[1] = cl; - stream = &NetBuffer[9]; - auto str = D_GetUserInfoStrings(cl, true); - const size_t userSize = str.Len() + 1; - memcpy(stream, str.GetChars(), userSize); - stream += userSize; - HSendPacket(client, int(stream - NetBuffer)); - } - } - } - - // If we're the host, send the game info too. - NetBuffer[0] = NCMD_GAMEINFO; - NetBuffer[1] = (uint8_t)doomcom.ticdup; - NetBuffer[2] = NetMode; - stream = &NetBuffer[3]; - WriteString(startmap.GetChars(), &stream); - WriteInt32(rngseed, &stream); - C_WriteCVars(&stream, CVAR_SERVERINFO, true); - } - - SendSetup(*data, int(stream - NetBuffer)); - return false; + D_SetupUserInfo(); } -static bool D_ExchangeNetInfo() +const char* Net_GetClientName(int client, unsigned int charLimit = 0u) { - // Return right away if it's just a solo net game. - if (doomcom.numplayers == 1) - return true; - - autostart = true; - - FNetGameInfo info = {}; - info.DetectedPlayers[consoleplayer] = 1 << consoleplayer; - info.GotSetup[consoleplayer] = consoleplayer == Net_Arbitrator; - NetworkClients += consoleplayer; - - if (consoleplayer == Net_Arbitrator) - I_NetInit("Sending game information", 1); - else - I_NetInit("Waiting for host information", 1); - - if (!I_NetLoop(ExchangeNetGameInfo, &info)) - return false; - - // Let everyone else know the game is ready to start. - if (consoleplayer == Net_Arbitrator) - { - NetBuffer[0] = NCMD_GAMEREADY; - for (auto client : NetworkClients) - { - if (client != Net_Arbitrator) - HSendPacket(client, 1); - } - } - - I_NetDone(); - return true; + return players[client].userinfo.GetName(charLimit); } -static void SendSetup(const FNetGameInfo& info, int len) +int Net_SetUserInfo(int client, uint8_t*& stream) { - if (consoleplayer != Net_Arbitrator) - { - HSendPacket(Net_Arbitrator, len); - } - else - { - for (auto client : NetworkClients) - { - // Only send game info over to clients still needing it. - if (client != Net_Arbitrator && !info.GotSetup[client]) - HSendPacket(client, len); - } - } + auto str = D_GetUserInfoStrings(client, true); + const size_t userSize = str.Len() + 1; + memcpy(stream, str.GetChars(), userSize); + return userSize; +} + +int Net_ReadUserInfo(int client, uint8_t*& stream) +{ + const uint8_t* start = stream; + D_ReadUserInfoStrings(client, &stream, false); + return int(stream - start); +} + +int Net_SetGameInfo(uint8_t*& stream) +{ + const uint8_t* start = stream; + WriteString(startmap.GetChars(), &stream); + WriteInt32(rngseed, &stream); + C_WriteCVars(&stream, CVAR_SERVERINFO, true); + return int(stream - start); +} + +int Net_ReadGameInfo(uint8_t*& stream) +{ + const uint8_t* start = stream; + startmap = ReadStringConst(&stream); + rngseed = ReadInt32(&stream); + C_ReadCVars(&stream); + return int(stream - start); } // Connects players to each other if needed. bool D_CheckNetGame() { - const char* v = Args->CheckValue("-netmode"); - int result = I_InitNetwork(); // I_InitNetwork sets doomcom and netgame - if (result == -1) + if (!I_InitNetwork()) return false; - else if (result > 0 && v == nullptr) // Don't override manual netmode setting. - NetMode = NET_PacketServer; - if (doomcom.id != DOOMCOM_ID) - I_FatalError("Invalid doomcom id set for network buffer"); + if (GameID != DEFAULT_GAME_ID) + I_FatalError("Invalid id set for network buffer"); + + if (Args->CheckParm("-extratic")) + net_extratic = true; players[Net_Arbitrator].settings_controller = true; - consoleplayer = doomcom.consoleplayer; - - if (consoleplayer == Net_Arbitrator) - { - if (v != nullptr) - NetMode = atoi(v) ? NET_PacketServer : NET_PeerToPeer; - - if (doomcom.numplayers > 1) - { - Printf("Selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode. (%s)\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server", - v != nullptr ? "forced" : "auto"); - } - - if (Args->CheckParm("-extratic")) - net_extratic = true; - } - - // [RH] Setup user info - D_SetupUserInfo(); - - if (netgame) - { - GameConfig->ReadNetVars(); // [RH] Read network ServerInfo cvars - if (!D_ExchangeNetInfo()) - return false; - } - for (auto client : NetworkClients) playeringame[client] = true; - if (consoleplayer != Net_Arbitrator && doomcom.numplayers > 1) - Printf("Host selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode.\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); + if (MaxClients > 1u) + { + if (consoleplayer == Net_Arbitrator) + Printf("Selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); + else + Printf("Host selected " TEXTCOLOR_BLUE "%s" TEXTCOLOR_NORMAL " networking mode\n", NetMode == NET_PeerToPeer ? "peer to peer" : "packet server"); - Printf("player %d of %d\n", consoleplayer + 1, doomcom.numplayers); + Printf("Player %d of %d\n", consoleplayer + 1, MaxClients); + } return true; } @@ -1918,19 +1668,19 @@ ADD_STAT(network) } out.AppendFormat("Max players: %d\tNet mode: %s\tTic dup: %d", - doomcom.numplayers, + MaxClients, NetMode == NET_PacketServer ? "Packet server" : "Peer to peer", - doomcom.ticdup); + TicDup); if (net_extratic) out.AppendFormat("\tExtra tic enabled"); - out.AppendFormat("\nWorld tic: %06d (sequence %06d)", gametic, gametic / doomcom.ticdup); + out.AppendFormat("\nWorld tic: %06d (sequence %06d)", gametic, gametic / TicDup); if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) out.AppendFormat("\tStart tics delay: %d", LevelStartDebug); - const int delay = max((ClientTic - gametic) / doomcom.ticdup, 0); - const int msDelay = min(delay * doomcom.ticdup * 1000.0 / TICRATE, 999); + const int delay = max((ClientTic - gametic) / TicDup, 0); + const int msDelay = min(delay * TicDup * 1000.0 / TICRATE, 999); out.AppendFormat("\nLocal\n\tIs arbitrator: %d\tDelay: %02d (%03dms)", consoleplayer == Net_Arbitrator, delay, msDelay); @@ -1948,7 +1698,7 @@ ADD_STAT(network) out.AppendFormat("\tWaiting for arbitrator"); } - int lowestSeq = ClientTic / doomcom.ticdup; + int lowestSeq = ClientTic / TicDup; for (auto client : NetworkClients) { if (client == consoleplayer) @@ -1980,8 +1730,8 @@ ADD_STAT(network) if (NetMode != NET_PacketServer) { - const int cDelay = max(state.CurrentSequence - (gametic / doomcom.ticdup), 0); - const int mscDelay = min(cDelay * doomcom.ticdup * 1000.0 / TICRATE, 999); + const int cDelay = max(state.CurrentSequence - (gametic / TicDup), 0); + const int mscDelay = min(cDelay * TicDup * 1000.0 / TICRATE, 999); out.AppendFormat("\tDelay: %02d (%03dms)", cDelay, mscDelay); } @@ -1991,7 +1741,7 @@ ADD_STAT(network) } if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) - out.AppendFormat("\nAvailable tics: %03d", max(lowestSeq - (gametic / doomcom.ticdup), 0)); + out.AppendFormat("\nAvailable tics: %03d", max(lowestSeq - (gametic / TicDup), 0)); return out; } @@ -2091,7 +1841,7 @@ void TryRunTics() // If the lowest confirmed tic matches the server gametic or greater, allow the client // to run some of them. - const int availableTics = (lowestSequence - gametic / doomcom.ticdup) + 1; + const int availableTics = (lowestSequence - gametic / TicDup) + 1; // If the amount of tics to run is falling behind the amount of available tics, // speed the playsim up a bit to help catch up. @@ -2314,7 +2064,7 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player) s = ReadStringConst(stream); // If chat is disabled, there's nothing else to do here since the stream has been advanced. - if (cl_showchat == CHAT_DISABLED || (MutedClients & (1 << player))) + if (cl_showchat == CHAT_DISABLED || (MutedClients & ((uint64_t)1u << player))) break; constexpr int MSG_TEAM = 1; @@ -3195,7 +2945,7 @@ CCMD(mute) return; } - MutedClients |= 1 << pNum; + MutedClients |= (uint64_t)1u << pNum; } CCMD(muteall) @@ -3209,7 +2959,7 @@ CCMD(muteall) for (auto client : NetworkClients) { if (client != consoleplayer) - MutedClients |= 1 << client; + MutedClients |= (uint64_t)1u << client; } } @@ -3224,7 +2974,7 @@ CCMD(listmuted) bool found = false; for (auto client : NetworkClients) { - if (MutedClients & (1 << client)) + if (MutedClients & ((uint64_t)1u << client)) { found = true; Printf("%s - %d\n", players[client].userinfo.GetName(), client); @@ -3262,7 +3012,7 @@ CCMD(unmute) return; } - MutedClients &= ~(1 << pNum); + MutedClients &= ~((uint64_t)1u << pNum); } CCMD(unmuteall) @@ -3273,7 +3023,7 @@ CCMD(unmuteall) return; } - MutedClients = 0; + MutedClients = 0u; } //========================================================================== diff --git a/src/d_net.h b/src/d_net.h index c04ec3c34..8b2b6649d 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -36,34 +36,62 @@ uint64_t I_msTime(); +enum EChatType +{ + CHAT_DISABLED, + CHAT_TEAM_ONLY, + CHAT_GLOBAL, +}; + +enum EClientFlags +{ + CF_NONE = 0, + CF_QUIT = 1, // If in packet server mode, this client sent an exit command and needs to be disconnected. + CF_MISSING_SEQ = 1 << 1, // If a sequence was missed/out of order, ask this client to send back over their info. + CF_RETRANSMIT_SEQ = 1 << 2, // If set, this client needs command data resent to them. + CF_MISSING_CON = 1 << 3, // If a consistency was missed/out of order, ask this client to send back over their info. + CF_RETRANSMIT_CON = 1 << 4, // If set, this client needs consistency data resent to them. + CF_UPDATED = 1 << 5, // Got an updated packet from this client. + + CF_RETRANSMIT = CF_RETRANSMIT_CON | CF_RETRANSMIT_SEQ, + CF_MISSING = CF_MISSING_CON | CF_MISSING_SEQ, +}; + class FDynamicBuffer { public: FDynamicBuffer(); ~FDynamicBuffer(); - void SetData(const uint8_t *data, int len); - uint8_t* GetData(int *len = nullptr); + void SetData(const uint8_t* data, int len); + uint8_t* GetData(int* len = nullptr); private: uint8_t* m_Data; int m_Len, m_BufferLen; }; -enum EClientFlags -{ - CF_NONE = 0, - CF_QUIT = 1, // If in packet server mode, this client sent an exit command and needs to be disconnected. - CF_MISSING_SEQ = 1 << 1, // If a sequence was missed/out of order, ask this client to send back over their info. - CF_RETRANSMIT_SEQ = 1 << 2, // If set, this client needs command data resent to them. - CF_MISSING_CON = 1 << 3, // If a consistency was missed/out of order, ask this client to send back over their info. - CF_RETRANSMIT_CON = 1 << 4, // If set, this client needs consistency data resent to them. - CF_UPDATED = 1 << 5, // Got an updated packet from this client. - - CF_RETRANSMIT = CF_RETRANSMIT_CON | CF_RETRANSMIT_SEQ, - CF_MISSING = CF_MISSING_CON | CF_MISSING_SEQ, -}; - +// New packet structure: +// +// One byte for the net command flags. +// Four bytes for the last sequence we got from that client. +// Four bytes for the last consistency we got from that client. +// If NCMD_QUITTERS set, one byte for the number of players followed by one byte for each player's consolenum. Packet server mode only. +// One byte for the number of players. +// One byte for the number of tics. +// If > 0, four bytes for the base sequence being worked from. +// One byte for the number of world tics ran. +// If > 0, four bytes for the base consistency being worked from. +// If in packet server mode and from the host, one byte for how far ahead of the host we are. +// For each player: +// One byte for the player number. +// If in packet server mode and from the host, two bytes for the latency to the host. +// For each consistency: +// One byte for the delta from the base consistency. +// Two bytes for each consistency. +// For each tic: +// One byte for the delta from the base sequence. +// The remaining command and event data for that player. struct FClientNetState { // Networked client data. @@ -98,41 +126,6 @@ struct FClientNetState int16_t LocalConsistency[BACKUPTICS] = {}; // Local consistency of the client to check against. }; -enum ENetMode : uint8_t -{ - NET_PeerToPeer, - NET_PacketServer -}; - -enum EChatType -{ - CHAT_DISABLED, - CHAT_TEAM_ONLY, - CHAT_GLOBAL, -}; - -// New packet structure: -// -// One byte for the net command flags. -// Four bytes for the last sequence we got from that client. -// Four bytes for the last consistency we got from that client. -// If NCMD_QUITTERS set, one byte for the number of players followed by one byte for each player's consolenum. Packet server mode only. -// One byte for the number of players. -// One byte for the number of tics. -// If > 0, four bytes for the base sequence being worked from. -// One byte for the number of world tics ran. -// If > 0, four bytes for the base consistency being worked from. -// If in packet server mode and from the host, one byte for how far ahead of the host we are. -// For each player: -// One byte for the player number. -// If in packet server mode and from the host, two bytes for the latency to the host. -// For each consistency: -// One byte for the delta from the base consistency. -// Two bytes for each consistency. -// For each tic: -// One byte for the delta from the base sequence. -// The remaining command and event data for that player. - // Create any new ticcmds and broadcast to other players. void NetUpdate(int tics); @@ -164,13 +157,9 @@ void Net_ClearBuffers(); // Netgame stuff (buffers and pointers, i.e. indices). -// This is the interface to the packet driver, a separate program -// in DOS, but just an abstraction here. -extern doomcom_t doomcom; extern usercmd_t LocalCmds[LOCALCMDTICS]; extern int ClientTic; extern FClientNetState ClientStates[MAXPLAYERS]; -extern ENetMode NetMode; class player_t; class DObject; diff --git a/src/d_protocol.cpp b/src/d_protocol.cpp index 044593c96..2ecc713bc 100644 --- a/src/d_protocol.cpp +++ b/src/d_protocol.cpp @@ -446,7 +446,7 @@ int ReadUserCmdMessage(uint8_t*& stream, int player, int tic) void RunPlayerCommands(int player, int tic) { // We don't have the full command yet, so don't run it. - if (gametic % doomcom.ticdup) + if (gametic % TicDup) return; int len; diff --git a/src/doomdef.h b/src/doomdef.h index ce9b82316..6727923c3 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -30,6 +30,7 @@ #include #include +#include "i_net.h" // // Global parameters/defines. @@ -63,9 +64,6 @@ constexpr int TICRATE = 35; // Global constants that were defines. enum { - // The maximum number of players, multiplayer/networking. - MAXPLAYERS = 16, - // Amount of damage done by a telefrag. TELEFRAG_DAMAGE = 1000000 }; diff --git a/src/doomstat.cpp b/src/doomstat.cpp index ef681fb85..2e4e83353 100644 --- a/src/doomstat.cpp +++ b/src/doomstat.cpp @@ -39,9 +39,6 @@ int SaveVersion; // Game speed EGameSpeed GameSpeed = SPEED_Normal; -// [RH] Network arbitrator -int Net_Arbitrator = 0; - int NextSkill = -1; int SinglePlayerClass[MAXPLAYERS]; diff --git a/src/doomstat.h b/src/doomstat.h index cc991a4fe..b41a6dcc3 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -198,8 +198,6 @@ EXTERN_CVAR (Int, developer) extern bool ToggleFullscreen; -extern int Net_Arbitrator; - EXTERN_CVAR (Bool, var_friction) diff --git a/src/g_game.cpp b/src/g_game.cpp index c8b990150..88fc30144 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -622,7 +622,7 @@ void G_BuildTiccmd (usercmd_t *cmd) // and not the joystick, since we treat the joystick as // the analog device it is. if (buttonMap.ButtonDown(Button_Left) || buttonMap.ButtonDown(Button_Right)) - turnheld += doomcom.ticdup; + turnheld += TicDup; else turnheld = 0; @@ -1232,7 +1232,7 @@ void G_Ticker () } // get commands, check consistancy, and build new consistancy check - const int curTic = gametic / doomcom.ticdup; + const int curTic = gametic / TicDup; //Added by MC: For some of that bot stuff. The main bot function. primaryLevel->BotInfo.Main (primaryLevel); @@ -2545,7 +2545,7 @@ void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf) } // [RH] Write any special "ticcmds" for this player to the demo - if ((specdata = ClientStates[player].Tics[buf % BACKUPTICS].Data.GetData (&speclen)) && !(gametic % doomcom.ticdup)) + if ((specdata = ClientStates[player].Tics[buf % BACKUPTICS].Data.GetData (&speclen)) && !(gametic % TicDup)) { memcpy (demo_p, specdata, speclen); demo_p += speclen; diff --git a/src/playsim/p_acs.cpp b/src/playsim/p_acs.cpp index 6eb9cb4e6..34b78219a 100644 --- a/src/playsim/p_acs.cpp +++ b/src/playsim/p_acs.cpp @@ -540,6 +540,8 @@ +extern int Net_Arbitrator; + FRandom pr_acs ("ACS"); // I imagine this much stack space is probably overkill, but it could diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index c73ed76ab..d0356ee0d 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1,6 +1,6 @@ // for flag changer functions. const FLAG_NO_CHANGE = -1; -const MAXPLAYERS = 16; +const MAXPLAYERS = 64; enum EStateUseFlags { From def3082ed8f5c9f1c8e5fe4282595db37cc86f8c Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Fri, 7 Mar 2025 21:57:10 -0700 Subject: [PATCH 058/384] Forgot to account for when both floor and ceiling of a sector are portals. --- src/rendering/hwrenderer/scene/hw_portal.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index 6877f7982..965582a2e 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -815,9 +815,7 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c if (origin->plane != -1) screen->instack[origin->plane]++; if (lines.Size() > 0) { - flat.plane.GetFromSector(lines[0].sub->sector, - lines[0].sub->sector->GetPortal(sector_t::ceiling)->mType & (PORTS_STACKEDSECTORTHING | PORTS_PORTAL | PORTS_LINKEDPORTAL) ? - sector_t::ceiling : sector_t::floor); + flat.plane.GetFromSector(lines[0].sub->sector, origin->plane); di->SetClipHeight(flat.plane.plane.ZatPoint(vp.Pos), flat.plane.plane.Normal().Z > 0 ? -1.f : 1.f); } From 60ebd71feaaae70a93f08fc8231691588f68bc48 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Fri, 7 Mar 2025 23:47:12 -0700 Subject: [PATCH 059/384] Handle sectors within sectors for stacked portals and plane mirrors (affects OoB only). --- src/rendering/hwrenderer/scene/hw_portal.cpp | 10 ++++++---- src/rendering/hwrenderer/scene/hw_sky.cpp | 10 ++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index 965582a2e..cc7289480 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -815,6 +815,7 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c if (origin->plane != -1) screen->instack[origin->plane]++; if (lines.Size() > 0) { + flat.plane.GetFromSector(lines[0].sub->sector, origin->plane); di->SetClipHeight(flat.plane.plane.ZatPoint(vp.Pos), flat.plane.plane.Normal().Z > 0 ? -1.f : 1.f); @@ -841,11 +842,12 @@ void HWSectorStackPortal::DrawPortalStencil(FRenderState &state, int pass) if (mState->vpIsAllowedOoB) { bool isceiling = planesused & (1 << sector_t::ceiling); - for (unsigned int i = 0; i < lines.Size(); i++) + for (unsigned i = 0; isection; - flat.iboindex = lines[i].sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; - flat.plane.GetFromSector(lines[i].sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); + subsector_t *sub = subsectors[i]; + flat.section = sub->section; + flat.iboindex = sub->sector->iboindex[isceiling ? sector_t::ceiling : sector_t::floor]; + flat.plane.GetFromSector(sub->sector, isceiling ? sector_t::ceiling : sector_t::floor); // if (isceiling) flat.plane.plane.FlipVert(); // Doesn't do anything. Stencil is a screen-space projection state.SetNormal(flat.plane.plane.Normal().X, flat.plane.plane.Normal().Z, flat.plane.plane.Normal().Y); diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index 2aef16c8f..ce89b545a 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -319,7 +319,10 @@ void HWWall::SkyTop(HWWallDispatcher *di, seg_t * seg,sector_t * fs,sector_t * b if (backreflect > 0 && bs->ceilingplane.fD() == fs->ceilingplane.fD() && !bs->isClosed()) { // Don't add intra-portal line to the portal. - return; + if (!(di->di && di->di->Viewpoint.IsAllowedOoB())) + { + return; + } } } else @@ -398,7 +401,10 @@ void HWWall::SkyBottom(HWWallDispatcher *di, seg_t * seg,sector_t * fs,sector_t if (backreflect > 0 && bs->floorplane.fD() == fs->floorplane.fD() && !bs->isClosed()) { // Don't add intra-portal line to the portal. - return; + if (!(di->di && di->di->Viewpoint.IsAllowedOoB())) + { + return; + } } } else From 3c470019de199decb7f4d3dafeb187657a3bf5ef Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 8 Mar 2025 02:10:05 -0500 Subject: [PATCH 060/384] Fixed default value of consoleplayer Needs to be 0 since certain cvars will try and use it on initial callback when the engine boots up. --- src/common/engine/i_net.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 82668a0b5..3bddcbeff 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -158,7 +158,7 @@ struct FConnection bool netgame = false; bool multiplayer = false; ENetMode NetMode = NET_PeerToPeer; -int consoleplayer = -1; +int consoleplayer = 0; int Net_Arbitrator = 0; FClientStack NetworkClients = {}; @@ -930,7 +930,6 @@ static bool HostGame(int arg, bool forcedNetMode) if (MaxClients > MAXPLAYERS) I_FatalError("Cannot host a game with %u players. The limit is currently %u", MaxClients, MAXPLAYERS); - consoleplayer = 0; NetworkClients += 0; Connected[consoleplayer].Status = CSTAT_READY; Net_SetupUserInfo(); @@ -1183,6 +1182,7 @@ static bool JoinGame(int arg) I_FatalError("You need to specify the host machine's address"); } + consoleplayer = -1; StartNetwork(true); // Host is always client 0. @@ -1254,7 +1254,6 @@ bool I_InitNetwork() { // single player game TicDup = 1; - consoleplayer = 0; NetworkClients += 0; Connected[0].Status = CSTAT_READY; Net_SetupUserInfo(); From e24c6fa4db170191fdedc633b0a9b34374798083 Mon Sep 17 00:00:00 2001 From: dileepvr Date: Sat, 8 Mar 2025 07:10:59 -0700 Subject: [PATCH 061/384] Update hw_portal OoB height clip Hopefully the last bug squash. --- src/rendering/hwrenderer/scene/hw_portal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index cc7289480..4d1ce3f8b 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -813,7 +813,7 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c // avoid recursions! if (origin->plane != -1) screen->instack[origin->plane]++; - if (lines.Size() > 0) + if (vp.IsAllowedOoB() && lines.Size() > 0) { flat.plane.GetFromSector(lines[0].sub->sector, origin->plane); From 74594e4c345975d712dffa013a21904ce0d5837b Mon Sep 17 00:00:00 2001 From: dileepvr Date: Sat, 8 Mar 2025 07:33:58 -0700 Subject: [PATCH 062/384] Remove bitwise opeartion on bool Visual Studio compiler was giving the warning: `warning C4805: '|=': unsafe mix of type 'bool' and type 'int' in operation` --- src/rendering/hwrenderer/scene/hw_walls.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index d5271d05f..527c02c0d 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -85,7 +85,7 @@ void HWWall::RenderWall(FRenderState &state, int textured) { bool ditherT = (type == RENDERWALL_BOTTOM) && (seg->sidedef->Flags & WALLF_DITHERTRANS_BOTTOM); ditherT |= (type == RENDERWALL_TOP) && (seg->sidedef->Flags & WALLF_DITHERTRANS_TOP); - ditherT |= seg->sidedef->Flags & WALLF_DITHERTRANS_MID; + ditherT = ditherT || (seg->sidedef->Flags & WALLF_DITHERTRANS_MID); if (ditherT) { state.SetEffect(EFF_DITHERTRANS); From 210ee1780ef042f63e1a6118577e38ddb4380b0d Mon Sep 17 00:00:00 2001 From: MajorCooke Date: Sat, 8 Mar 2025 14:14:53 -0600 Subject: [PATCH 063/384] Added particle rendering to VisualThinkers. To activate, use `SetParticleType(int type)`. To deactivate, use `DisableParticle()`. Types are: - PT_DEFAULT (default value; uses `gl_particles_style`) - PT_SQUARE - PT_ROUND - PT_SMOOTH While in this mode: - `Texture` & `Translation` are ignored - `Scale.X` sets the size - `SColor` sets the color Misc changes: - Removed warning on textureless destruction --- src/playsim/p_effect.cpp | 40 ++++++++++++++++--- src/playsim/p_effect.h | 8 ++++ src/playsim/p_visualthinker.h | 8 ++++ src/rendering/hwrenderer/scene/hw_sprites.cpp | 28 +++++++++---- wadsrc/static/zscript/constants.zs | 14 +++++++ wadsrc/static/zscript/visualthinker.zs | 26 ++++++++++++ 6 files changed, 110 insertions(+), 14 deletions(-) diff --git a/src/playsim/p_effect.cpp b/src/playsim/p_effect.cpp index c16d4a982..dabf0ed16 100644 --- a/src/playsim/p_effect.cpp +++ b/src/playsim/p_effect.cpp @@ -1108,7 +1108,8 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, SpawnVisualThinker, SpawnVisualThink void DVisualThinker::UpdateSpriteInfo() { PT.style = ERenderStyle(GetRenderStyle()); - if((PT.flags & SPF_LOCAL_ANIM) && PT.texture != AnimatedTexture) + + if ((PT.flags & SPF_LOCAL_ANIM) && PT.texture != AnimatedTexture) { AnimatedTexture = PT.texture; TexAnim.InitStandaloneAnimation(PT.animData, PT.texture, Level->maptime); @@ -1127,6 +1128,10 @@ DEFINE_ACTION_FUNCTION_NATIVE(DVisualThinker, UpdateSpriteInfo, UpdateSpriteInfo return 0; } +bool DVisualThinker::ValidTexture() +{ + return ((flags & VTF_IsParticle) || PT.texture.isValid()); +} // This runs just like Actor's, make sure to call Super.Tick() in ZScript. void DVisualThinker::Tick() @@ -1134,10 +1139,8 @@ void DVisualThinker::Tick() if (ObjectFlags & OF_EuthanizeMe) return; - // There won't be a standard particle for this, it's only for graphics. - if (!PT.texture.isValid()) + if (!ValidTexture()) { - Printf("No valid texture, destroyed"); Destroy(); return; } @@ -1156,7 +1159,6 @@ void DVisualThinker::Tick() PT.Pos.X = newxy.X; PT.Pos.Y = newxy.Y; PT.Pos.Z += PT.Vel.Z; - subsector_t * ss = Level->PointInRenderSubsector(PT.Pos); // Handle crossing a sector portal. @@ -1246,7 +1248,33 @@ DEFINE_ACTION_FUNCTION_NATIVE(DVisualThinker, SetTranslation, SetTranslation) return 0; } -static int IsFrozen(DVisualThinker * self) +int DVisualThinker::GetParticleType() const +{ + int flag = (flags & VTF_IsParticle); + switch (flag) + { + case VTF_ParticleSquare: + return PT_SQUARE; + case VTF_ParticleRound: + return PT_ROUND; + case VTF_ParticleSmooth: + return PT_SMOOTH; + } + return PT_DEFAULT; +} + +static int GetParticleType(DVisualThinker* self) +{ + return self->GetParticleType(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DVisualThinker, GetParticleType, GetParticleType) +{ + PARAM_SELF_PROLOGUE(DVisualThinker); + ACTION_RETURN_INT(self->GetParticleType()); +} + +static int IsFrozen(DVisualThinker* self) { return !!(self->Level->isFrozen() && !(self->PT.flags & SPF_NOTIMEFREEZE)); } diff --git a/src/playsim/p_effect.h b/src/playsim/p_effect.h index 29042b293..b23efb01b 100644 --- a/src/playsim/p_effect.h +++ b/src/playsim/p_effect.h @@ -53,6 +53,14 @@ struct FLevelLocals; // [RH] Particle details +enum EParticleStyle +{ + PT_DEFAULT = -1, // Use gl_particles_style + PT_SQUARE = 0, + PT_ROUND = 1, + PT_SMOOTH = 2, +}; + enum EParticleFlags { SPF_FULLBRIGHT = 1 << 0, diff --git a/src/playsim/p_visualthinker.h b/src/playsim/p_visualthinker.h index 16043effd..69faada9f 100644 --- a/src/playsim/p_visualthinker.h +++ b/src/playsim/p_visualthinker.h @@ -20,6 +20,12 @@ enum EVisualThinkerFlags VTF_FlipY = 1 << 3, // flip the sprite on the x/y axis. VTF_DontInterpolate = 1 << 4, // disable all interpolation VTF_AddLightLevel = 1 << 5, // adds sector light level to 'LightLevel' + + VTF_ParticleDefault = 0x40, + VTF_ParticleSquare = 0x80, + VTF_ParticleRound = 0xC0, + VTF_ParticleSmooth = 0x100, + VTF_IsParticle = 0x1C0, // Renders as a particle instead }; class DVisualThinker : public DThinker @@ -53,6 +59,8 @@ public: void SetTranslation(FName trname); int GetRenderStyle() const; bool isFrozen(); + bool ValidTexture(); + int GetParticleType() const; int GetLightLevel(sector_t *rendersector) const; FVector3 InterpolatedPosition(double ticFrac) const; float InterpolatedRoll(double ticFrac) const; diff --git a/src/rendering/hwrenderer/scene/hw_sprites.cpp b/src/rendering/hwrenderer/scene/hw_sprites.cpp index f3ab6d4c3..26643e745 100644 --- a/src/rendering/hwrenderer/scene/hw_sprites.cpp +++ b/src/rendering/hwrenderer/scene/hw_sprites.cpp @@ -1409,7 +1409,7 @@ void HWSprite::ProcessParticle(HWDrawInfo *di, particle_t *particle, sector_t *s if (!particle || particle->alpha <= 0) return; - if (spr && spr->PT.texture.isNull()) + if (spr && !spr->ValidTexture()) return; lightlevel = hw_ClampLight(spr ? spr->GetLightLevel(sector) : sector->GetSpriteLight()); @@ -1477,17 +1477,29 @@ void HWSprite::ProcessParticle(HWDrawInfo *di, particle_t *particle, sector_t *s if (paused || (di->Level->isFrozen() && !(particle->flags & SPF_NOTIMEFREEZE))) timefrac = 0.; - if (spr) + + if (spr && !(spr->flags & VTF_IsParticle)) { AdjustVisualThinker(di, spr, sector); } else { - bool has_texture = particle->texture.isValid(); - bool custom_animated_texture = (particle->flags & SPF_LOCAL_ANIM) && particle->animData.ok; - - int particle_style = has_texture ? 2 : gl_particles_style; // Treat custom texture the same as smooth particles - + bool has_texture = false; + bool custom_animated_texture = false; + int particle_style = 0; + float size = particle->size; + if (!spr) + { + has_texture = particle->texture.isValid(); + custom_animated_texture = (particle->flags & SPF_LOCAL_ANIM) && particle->animData.ok; + particle_style = has_texture ? 2 : gl_particles_style; // Treat custom texture the same as smooth particles + } + else + { + size = float(spr->Scale.X); + const int ptype = spr->GetParticleType(); + particle_style = (ptype != PT_DEFAULT) ? ptype : gl_particles_style; + } // [BB] Load the texture for round or smooth particles if (particle_style) { @@ -1549,7 +1561,7 @@ void HWSprite::ProcessParticle(HWDrawInfo *di, particle_t *particle, sector_t *s if (particle_style == 1) factor = 1.3f / 7.f; else if (particle_style == 2) factor = 2.5f / 7.f; else factor = 1 / 7.f; - float scalefac=particle->size * factor; + float scalefac= size * factor; float ps = di->Level->pixelstretch; diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index d0356ee0d..249d031bb 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1532,4 +1532,18 @@ enum EVisualThinkerFlags VTF_FlipY = 1 << 3, // flip the sprite on the x/y axis. VTF_DontInterpolate = 1 << 4, // disable all interpolation VTF_AddLightLevel = 1 << 5, // adds sector light level to 'LightLevel' + + VTF_ParticleDefault = 0x40, + VTF_ParticleSquare = 0x80, + VTF_ParticleRound = 0xC0, + VTF_ParticleSmooth = 0x100, + VTF_IsParticle = 0x1C0 +}; + +enum EParticleStyle +{ + PT_DEFAULT = -1, // Use gl_particles_style + PT_SQUARE = 0, + PT_ROUND = 1, + PT_SMOOTH = 2, }; diff --git a/wadsrc/static/zscript/visualthinker.zs b/wadsrc/static/zscript/visualthinker.zs index d5dc2253a..34c4643f5 100644 --- a/wadsrc/static/zscript/visualthinker.zs +++ b/wadsrc/static/zscript/visualthinker.zs @@ -60,4 +60,30 @@ Class VisualThinker : Thinker native } return p; } + + native int GetParticleType() const; + + void SetParticleType(EParticleStyle type = PT_DEFAULT) + { + switch(type) + { + Default: + VisualThinkerFlags = (VisualThinkerFlags & ~VTF_IsParticle) | VTF_ParticleDefault; + break; + case PT_SQUARE: + VisualThinkerFlags = (VisualThinkerFlags & ~VTF_IsParticle) | VTF_ParticleSquare; + break; + case PT_ROUND: + VisualThinkerFlags = (VisualThinkerFlags & ~VTF_IsParticle) | VTF_ParticleRound; + break; + case PT_SMOOTH: + VisualThinkerFlags = (VisualThinkerFlags & ~VTF_IsParticle) | VTF_ParticleSmooth; + break; + } + } + + void DisableParticle() + { + VisualThinkerFlags &= ~VTF_IsParticle; + } } From ce18a556b6b1829a9e9b1effed12b822648907c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 9 Mar 2025 16:54:41 -0300 Subject: [PATCH 064/384] remove K&R C function declaration bullshit from lemon.c should be enough to fix GCC15 compilation without fucking up size_t/etc --- tools/lemon/lemon.c | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tools/lemon/lemon.c b/tools/lemon/lemon.c index bdc004a17..e5fd16905 100644 --- a/tools/lemon/lemon.c +++ b/tools/lemon/lemon.c @@ -53,7 +53,7 @@ extern int access(char *path, int mode); #endif static int showPrecedenceConflict = 0; -static void *msort(void *list, void *next, int (*cmp)()); +static void *msort(void *list, void *next, int (*cmp)(void*, void*)); /* ** Compilers are getting increasingly pedantic about type conversions @@ -359,7 +359,7 @@ struct symbol **Symbol_arrayof(void); /* Routines to manage the state table */ -int Configcmp(const char *, const char *); +int Configcmp(void *, void *); struct state *State_new(void); void State_init(void); int State_insert(struct state *, struct config *); @@ -403,10 +403,10 @@ static struct action *Action_new(void){ ** positive if the first action is less than, equal to, or greater than ** the first */ -static int actioncmp(ap1,ap2) -struct action *ap1; -struct action *ap2; +static int actioncmp(void *_ap1,void *_ap2) { + struct action * ap1 = (struct action *)_ap1; + struct action * ap2 = (struct action *)_ap2; int rc; rc = ap1->sp->index - ap2->sp->index; if( rc==0 ){ @@ -1757,9 +1757,9 @@ int main(int argc, char **argv) ** The "next" pointers for elements in the lists a and b are ** changed. */ -static void *merge(void *a,void *b,int (*cmp)(),size_t offset) +static void *merge(void *a,void *b,int (*cmp)(void *a, void *b),size_t offset) { - char *ptr, *head; + void *ptr, *head; if( a==0 ){ head = b; @@ -1805,11 +1805,11 @@ static void *merge(void *a,void *b,int (*cmp)(),size_t offset) ** The "next" pointers for elements in list are changed. */ #define LISTSIZE 30 -static void *msort(void *list,void *next,int (*cmp)()) +static void *msort(void *list,void *next,int (*cmp)(void*, void*)) { size_t offset; - char *ep; - char *set[LISTSIZE]; + void *ep; + void *set[LISTSIZE]; int i; offset = (size_t)next - (size_t)list; for(i=0; irp->index - b->rp->index; if( x==0 ) x = a->dot - b->dot; @@ -5147,8 +5145,10 @@ int Configcmp(const char *_a,const char *_b) } /* Compare two states */ -PRIVATE int statecmp(struct config *a, struct config *b) +PRIVATE int statecmp(void *_a, void *_b) { + const struct config *a = (const struct config *) _a; + const struct config *b = (const struct config *) _b; int rc; for(rc=0; rc==0 && a && b; a=a->bp, b=b->bp){ rc = a->rp->index - b->rp->index; @@ -5377,7 +5377,7 @@ int Configtable_insert(struct config *data) h = ph & (x4a->size-1); np = x4a->ht[h]; while( np ){ - if( Configcmp((const char *) np->data,(const char *) data)==0 ){ + if( Configcmp(np->data, data)==0 ){ /* An existing entry with the same key is found. */ /* Fail because overwrite is not allows. */ return 0; @@ -5430,7 +5430,7 @@ struct config *Configtable_find(struct config *key) h = confighash(key) & (x4a->size-1); np = x4a->ht[h]; while( np ){ - if( Configcmp((const char *) np->data,(const char *) key)==0 ) break; + if( Configcmp(np->data,key)==0 ) break; np = np->next; } return np ? np->data : 0; From 49e47fe81c5aaa5b57e5cb1fcba3c759eb113e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 9 Mar 2025 16:57:28 -0300 Subject: [PATCH 065/384] fix non-void forward declarations as well --- tools/lemon/lemon.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/lemon/lemon.c b/tools/lemon/lemon.c index e5fd16905..45f458d72 100644 --- a/tools/lemon/lemon.c +++ b/tools/lemon/lemon.c @@ -72,12 +72,12 @@ static struct action *Action_new(void); static struct action *Action_sort(struct action *); /********** From the file "build.h" ************************************/ -void FindRulePrecedences(); -void FindFirstSets(); -void FindStates(); -void FindLinks(); -void FindFollowSets(); -void FindActions(); +void FindRulePrecedences(struct lemon *xp); +void FindFirstSets(struct lemon *lemp); +void FindStates(struct lemon *lemp); +void FindLinks(struct lemon *lemp); +void FindFollowSets(struct lemon *lemp); +void FindActions(struct lemon *lemp); /********* From the file "configlist.h" *********************************/ void Configlist_init(void); From e2103d2508d6d784dc20ffa598a4a11ac993ae69 Mon Sep 17 00:00:00 2001 From: James Le Cuirot Date: Sun, 9 Mar 2025 12:34:44 +0000 Subject: [PATCH 066/384] Fix building with GCC 15 --- libraries/ZWidget/include/zwidget/window/window.h | 1 + src/common/utility/r_memory.h | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/ZWidget/include/zwidget/window/window.h b/libraries/ZWidget/include/zwidget/window/window.h index 0539f773f..4cdb748d8 100644 --- a/libraries/ZWidget/include/zwidget/window/window.h +++ b/libraries/ZWidget/include/zwidget/window/window.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/src/common/utility/r_memory.h b/src/common/utility/r_memory.h index d9db538ca..41abe0be5 100644 --- a/src/common/utility/r_memory.h +++ b/src/common/utility/r_memory.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include From e4081df0db74dd012e1f1724e8c63a1a479d34ac Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 10 Mar 2025 17:50:48 -0400 Subject: [PATCH 067/384] Run net events on load barriers --- src/d_net.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 856552682..b14869a33 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -394,7 +394,6 @@ void Net_ResetCommands(bool midTic) // Make sure not to run its current command either. auto& curTic = state.Tics[tic % BACKUPTICS]; memset(&curTic.Command, 0, sizeof(curTic.Command)); - curTic.Data.SetData(nullptr, 0); } NetEvents.ResetStream(); From f7e62a8cd63d03f2d437e00d7558c7d9cba3b821 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 9 Nov 2024 22:18:04 -0500 Subject: [PATCH 068/384] 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. --- src/am_map.cpp | 2 +- src/common/objects/dobject.cpp | 15 ++- src/common/objects/dobject.h | 1 + src/common/objects/dobjgc.h | 1 + src/common/objects/dobjtype.cpp | 2 +- src/d_net.cpp | 28 ++++-- src/g_game.cpp | 27 +----- src/g_level.cpp | 7 +- src/g_levellocals.h | 54 ++++++++++- src/gamedata/info.cpp | 3 +- src/gamedata/info.h | 9 ++ src/p_saveg.cpp | 1 + src/p_setup.cpp | 3 + src/p_tick.cpp | 53 ++++++++++- src/playsim/dthinker.cpp | 82 +++++++++++++++-- src/playsim/dthinker.h | 16 ++-- src/playsim/p_acs.cpp | 127 +++++++++++++++++--------- src/playsim/p_acs.h | 3 +- src/playsim/p_maputl.cpp | 97 ++++++++++---------- src/playsim/p_mobj.cpp | 59 +++++++++--- src/playsim/p_user.cpp | 56 ++++-------- src/scripting/thingdef_data.cpp | 3 +- src/scripting/vmiterators.cpp | 30 +++++- src/scripting/vmthunks.cpp | 20 ++++ src/scripting/vmthunks_actors.cpp | 7 +- wadsrc/static/zscript/actors/actor.zs | 1 + wadsrc/static/zscript/doombase.zs | 8 +- wadsrc/static/zscript/engine/base.zs | 5 + 28 files changed, 506 insertions(+), 214 deletions(-) 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. From 69b0932f23b6f2a71fd5401e030747739795dcfc Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 10 Mar 2025 09:35:02 -0400 Subject: [PATCH 069/384] Use unique flag for clientside Actor handling in GZDoom This offers different behaviors from Zandronum so should be made exclusive. --- src/scripting/thingdef_data.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripting/thingdef_data.cpp b/src/scripting/thingdef_data.cpp index 7c301c835..930d2b1f6 100644 --- a/src/scripting/thingdef_data.cpp +++ b/src/scripting/thingdef_data.cpp @@ -465,6 +465,7 @@ 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), }; From a7a9cbe30ac2b81be7126312d51e70526e8ef869 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 12 Mar 2025 11:35:45 -0400 Subject: [PATCH 070/384] Move particle thinking back to server These need to be ran before anything has spawned them but after the game's pause state has been confirmed --- src/p_tick.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/p_tick.cpp b/src/p_tick.cpp index 1837fb3da..b952aa23a 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -77,8 +77,6 @@ void P_RunClientsideLogic() ac->ClearFOVInterpolation(); } - P_ThinkParticles(level); // [RH] make the particles think - level->ClientsideThinkers.RunClientsideThinkers(level); } @@ -246,6 +244,8 @@ 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]); From b213b81c92debd98c8b91c040d3297cffcc04bb8 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 11 Mar 2025 08:59:47 -0400 Subject: [PATCH 071/384] Auto split packets in packet-server mode Avoid fragmentation by trying to keep data in each packet to <1500b. Helps avoid possible issues with fragmentation with large player counts and bad network conditions. --- src/d_net.cpp | 305 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 181 insertions(+), 124 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index d3251fce1..438bb3d0a 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1297,10 +1297,14 @@ void NetUpdate(int tics) return; } + constexpr size_t MaxPlayersPerPacket = 16u; + int startSequence = startTic / TicDup; int endSequence = newestTic; int quitters = 0; int quitNums[MAXPLAYERS]; + size_t players = 1u; + int maxCommands = MAXSENDTICS; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { // In packet server mode special handling is used to ensure the host only @@ -1315,15 +1319,40 @@ void NetUpdate(int tics) // The host has special handling when disconnecting in a packet server game. if (ClientStates[client].Flags & CF_QUIT) + { quitNums[quitters++] = client; - else if (ClientStates[client].CurrentSequence < lowestSeq) - lowestSeq = ClientStates[client].CurrentSequence; + } + else + { + ++players; + if (ClientStates[client].CurrentSequence < lowestSeq) + lowestSeq = ClientStates[client].CurrentSequence; + } } endSequence = lowestSeq + 1; + + // To avoid fragmenting, split up commands into groups of 16p with only 2 commands per packet. + // If the average packet size with 16p is ~500b, this gives up to ~1000b per packet of data + // with some leeway for network events and UDP header data. Most routers have an MTU of 1500b. + // If player count is < 16, scale the number of commands by 1 per every 4 less players. + // If player count is < 8, scale the number of commands by 1 per every 1 less player. + // If player count is < 4, scale the number of commands by 4 per every 1 less player. + constexpr size_t MaxTicsPerPacket = 2u; + if (players > 1u) + { + maxCommands = MaxTicsPerPacket; + if (players >= MaxPlayersPerPacket / 2 && players < MaxPlayersPerPacket) + maxCommands = MaxTicsPerPacket + (MaxPlayersPerPacket - players) / 4; + else if (players >= MaxPlayersPerPacket / 4 && players < MaxPlayersPerPacket / 2) + maxCommands = MaxPlayersPerPacket / 4 + MaxPlayersPerPacket / 2 - players; + else if (players < MaxPlayersPerPacket / 4) + maxCommands = MaxPlayersPerPacket / 2 + (MaxPlayersPerPacket / 4 - players) * 4; + } } const bool resendOnly = startSequence == endSequence && (ClientTic % TicDup); + const int playerLoops = static_cast(ceil((double)players / MaxPlayersPerPacket)); for (auto client : NetworkClients) { // If in packet server mode, we don't want to send information to anyone but the host. On the other @@ -1333,11 +1362,11 @@ void NetUpdate(int tics) auto& curState = ClientStates[client]; // If we can only resend, don't send clients any information that they already have. If - // we couldn't generate any commands because we're at the cap, instead send out a heartbeat - // containing the latest command. - if (resendOnly && !(curState.Flags & (CF_RETRANSMIT | CF_MISSING))) + // we couldn't generate any commands because we're at the cap, instead send out a heartbeat. + if ((curState.Flags & CF_QUIT) || (resendOnly && !(curState.Flags & (CF_RETRANSMIT | CF_MISSING)))) continue; + const bool isSelf = client == consoleplayer; NetBuffer[0] = (curState.Flags & CF_MISSING) ? NCMD_RETRANSMIT : 0; curState.Flags &= ~CF_MISSING; @@ -1353,30 +1382,6 @@ void NetUpdate(int tics) NetBuffer[8] = (curState.CurrentNetConsistency >> 8); NetBuffer[9] = curState.CurrentNetConsistency; - size_t size = 10; - if (quitters > 0) - { - NetBuffer[0] |= NCMD_QUITTERS; - NetBuffer[size++] = quitters; - for (int i = 0; i < quitters; ++i) - NetBuffer[size++] = quitNums[i]; - } - - int playerNums[MAXPLAYERS]; - int playerCount = NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator ? NetworkClients.Size() : 1; - NetBuffer[size++] = playerCount; - if (playerCount > 1) - { - int i = 0; - for (auto cl : NetworkClients) - playerNums[i++] = cl; - } - else - { - playerNums[0] = consoleplayer; - } - - // Only send over our newest commands. If a client missed one, they'll let us know. if (curState.Flags & CF_RETRANSMIT_SEQ) { curState.Flags &= ~CF_RETRANSMIT_SEQ; @@ -1386,21 +1391,6 @@ void NetUpdate(int tics) const int sequenceNum = curState.ResendSequenceFrom >= 0 ? curState.ResendSequenceFrom : startSequence; const int numTics = clamp(endSequence - sequenceNum, 0, MAXSENDTICS); - if (curState.ResendSequenceFrom >= 0) - { - curState.ResendSequenceFrom += numTics; - if (curState.ResendSequenceFrom >= endSequence) - curState.ResendSequenceFrom = -1; - } - - NetBuffer[size++] = numTics; - if (numTics > 0) - { - NetBuffer[size++] = (sequenceNum >> 24); - NetBuffer[size++] = (sequenceNum >> 16); - NetBuffer[size++] = (sequenceNum >> 8); - NetBuffer[size++] = sequenceNum; - } if (curState.Flags & CF_RETRANSMIT_CON) { @@ -1408,106 +1398,173 @@ void NetUpdate(int tics) if (curState.ResendConsistencyFrom < 0) curState.ResendConsistencyFrom = curState.ConsistencyAck + 1; } - + const int baseConsistency = curState.ResendConsistencyFrom >= 0 ? curState.ResendConsistencyFrom : LastSentConsistency; // Don't bother sending over consistencies in packet server unless you're the host. int ran = 0; if (NetMode != NET_PacketServer || consoleplayer == Net_Arbitrator) - { ran = clamp(CurrentConsistency - baseConsistency, 0, MAXSENDTICS); - if (curState.ResendConsistencyFrom >= 0) - { - curState.ResendConsistencyFrom += ran; - if (curState.ResendConsistencyFrom >= CurrentConsistency) - curState.ResendConsistencyFrom = -1; - } - } - NetBuffer[size++] = ran; - if (ran > 0) + int ticLoops = static_cast(ceil(max(numTics, ran) / maxCommands)); + if (isSelf || !ticLoops) + ticLoops = 1; + + const int maxPlayerLoops = isSelf ? 1 : playerLoops; + for (int tLoops = 0, curTicOfs = 0; tLoops < ticLoops; ++tLoops, curTicOfs += maxCommands) { - NetBuffer[size++] = (baseConsistency >> 24); - NetBuffer[size++] = (baseConsistency >> 16); - NetBuffer[size++] = (baseConsistency >> 8); - NetBuffer[size++] = baseConsistency; - } - - if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) - NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence - newestTic, 0); - - // Client commands. - - uint8_t* cmd = &NetBuffer[size]; - for (int i = 0; i < playerCount; ++i) - { - cmd[0] = playerNums[i]; - ++cmd; - - auto& clientState = ClientStates[playerNums[i]]; - // Time used to track latency since in packet server mode we want each - // client's latency to the server itself. - if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + for (int pLoops = 0, curPlayerOfs = 0; pLoops < maxPlayerLoops; ++pLoops, curPlayerOfs += MaxPlayersPerPacket) { - cmd[0] = (clientState.AverageLatency >> 8); - ++cmd; - cmd[0] = clientState.AverageLatency; - ++cmd; - } - - for (int r = 0; r < ran; ++r) - { - cmd[0] = r; - ++cmd; - const int tic = (baseConsistency + r) % BACKUPTICS; - cmd[0] = (clientState.LocalConsistency[tic] >> 8); - ++cmd; - cmd[0] = clientState.LocalConsistency[tic]; - ++cmd; - } - - for (int t = 0; t < numTics; ++t) - { - cmd[0] = t; - ++cmd; - - int curTic = sequenceNum + t, lastTic = curTic - 1; - if (playerNums[i] == consoleplayer) + size_t size = 10; + if (quitters > 0) { - int realTic = (curTic * TicDup) % LOCALCMDTICS; - int realLastTic = (lastTic * TicDup) % LOCALCMDTICS; - // Write out the net events before the user commands so inputs can - // be used as a marker for when the given command ends. - auto& stream = NetEvents.Streams[curTic % BACKUPTICS]; - if (stream.Used) - { - memcpy(cmd, stream.Stream, stream.Used); - cmd += stream.Used; - } + NetBuffer[0] |= NCMD_QUITTERS; + NetBuffer[size++] = quitters; + for (int i = 0; i < quitters; ++i) + NetBuffer[size++] = quitNums[i]; - WriteUserCmdMessage(LocalCmds[realTic], - realLastTic >= 0 ? &LocalCmds[realLastTic] : nullptr, cmd); + quitters = 0; } else { - auto& netTic = clientState.Tics[curTic % BACKUPTICS]; + NetBuffer[0] &= ~NCMD_QUITTERS; + } - int len; - uint8_t* data = netTic.Data.GetData(&len); - if (data != nullptr) + int playerNums[MAXPLAYERS]; + int playerCount = isSelf ? players : min(players - curPlayerOfs, MaxPlayersPerPacket); + NetBuffer[size++] = playerCount; + if (players > 1) + { + int i = 0; + for (auto cl : NetworkClients) { - memcpy(cmd, data, len); - cmd += len; + if (ClientStates[cl].Flags & CF_QUIT) + continue; + + if (i >= curPlayerOfs) + playerNums[i - curPlayerOfs] = cl; + + ++i; + if (!isSelf && i >= curPlayerOfs + MaxPlayersPerPacket) + break; + } + } + else + { + playerNums[0] = consoleplayer; + } + + int sendTics = isSelf ? numTics : clamp(numTics - curTicOfs, 0, maxCommands); + if (curState.ResendSequenceFrom >= 0) + { + curState.ResendSequenceFrom += sendTics; + if (curState.ResendSequenceFrom >= endSequence) + curState.ResendSequenceFrom = -1; + } + + NetBuffer[size++] = sendTics; + if (sendTics > 0) + { + NetBuffer[size++] = (sequenceNum + curTicOfs) >> 24; + NetBuffer[size++] = (sequenceNum + curTicOfs) >> 16; + NetBuffer[size++] = (sequenceNum + curTicOfs) >> 8; + NetBuffer[size++] = sequenceNum + curTicOfs; + } + + int sendCon = isSelf ? ran : clamp(ran - curTicOfs, 0, maxCommands); + if (curState.ResendConsistencyFrom >= 0) + { + curState.ResendConsistencyFrom += sendCon; + if (curState.ResendConsistencyFrom >= CurrentConsistency) + curState.ResendConsistencyFrom = -1; + } + + NetBuffer[size++] = sendCon; + if (sendCon > 0) + { + NetBuffer[size++] = (baseConsistency + curTicOfs) >> 24; + NetBuffer[size++] = (baseConsistency + curTicOfs) >> 16; + NetBuffer[size++] = (baseConsistency + curTicOfs) >> 8; + NetBuffer[size++] = baseConsistency + curTicOfs; + } + + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence - newestTic, 0); + + // Client commands. + + uint8_t* cmd = &NetBuffer[size]; + for (int i = 0; i < playerCount; ++i) + { + cmd[0] = playerNums[i]; + ++cmd; + + auto& clientState = ClientStates[playerNums[i]]; + // Time used to track latency since in packet server mode we want each + // client's latency to the server itself. + if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) + { + cmd[0] = (clientState.AverageLatency >> 8); + ++cmd; + cmd[0] = clientState.AverageLatency; + ++cmd; } - WriteUserCmdMessage(netTic.Command, - lastTic >= 0 ? &clientState.Tics[lastTic % BACKUPTICS].Command : nullptr, cmd); + for (int r = 0; r < sendCon; ++r) + { + cmd[0] = r; + ++cmd; + const int tic = (baseConsistency + curTicOfs + r) % BACKUPTICS; + cmd[0] = (clientState.LocalConsistency[tic] >> 8); + ++cmd; + cmd[0] = clientState.LocalConsistency[tic]; + ++cmd; + } + + for (int t = 0; t < sendTics; ++t) + { + cmd[0] = t; + ++cmd; + + int curTic = sequenceNum + curTicOfs + t, lastTic = curTic - 1; + if (playerNums[i] == consoleplayer) + { + int realTic = (curTic * TicDup) % LOCALCMDTICS; + int realLastTic = (lastTic * TicDup) % LOCALCMDTICS; + // Write out the net events before the user commands so inputs can + // be used as a marker for when the given command ends. + auto& stream = NetEvents.Streams[curTic % BACKUPTICS]; + if (stream.Used) + { + memcpy(cmd, stream.Stream, stream.Used); + cmd += stream.Used; + } + + WriteUserCmdMessage(LocalCmds[realTic], + realLastTic >= 0 ? &LocalCmds[realLastTic] : nullptr, cmd); + } + else + { + auto& netTic = clientState.Tics[curTic % BACKUPTICS]; + + int len; + uint8_t* data = netTic.Data.GetData(&len); + if (data != nullptr) + { + memcpy(cmd, data, len); + cmd += len; + } + + WriteUserCmdMessage(netTic.Command, + lastTic >= 0 ? &clientState.Tics[lastTic % BACKUPTICS].Command : nullptr, cmd); + } + } } + + HSendPacket(client, int(cmd - NetBuffer)); + if (net_extratic && !isSelf) + HSendPacket(client, int(cmd - NetBuffer)); } } - - HSendPacket(client, int(cmd - NetBuffer)); - if (client != consoleplayer && net_extratic) - HSendPacket(client, int(cmd - NetBuffer)); } // Update this now that all the packets have been sent out. From 918b4a88347d5b5e886aab3d40f907ce5456168e Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 11 Mar 2025 09:19:57 -0400 Subject: [PATCH 072/384] Fixed clients after host not getting quitters --- src/d_net.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 438bb3d0a..689f90848 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1410,19 +1410,20 @@ void NetUpdate(int tics) ticLoops = 1; const int maxPlayerLoops = isSelf ? 1 : playerLoops; + int totalQuits = quitters; for (int tLoops = 0, curTicOfs = 0; tLoops < ticLoops; ++tLoops, curTicOfs += maxCommands) { for (int pLoops = 0, curPlayerOfs = 0; pLoops < maxPlayerLoops; ++pLoops, curPlayerOfs += MaxPlayersPerPacket) { size_t size = 10; - if (quitters > 0) + if (totalQuits > 0) { NetBuffer[0] |= NCMD_QUITTERS; - NetBuffer[size++] = quitters; - for (int i = 0; i < quitters; ++i) + NetBuffer[size++] = totalQuits; + for (int i = 0; i < totalQuits; ++i) NetBuffer[size++] = quitNums[i]; - quitters = 0; + totalQuits = 0; } else { From a7fd3852e261fb69c39740185b1777568aca11fc Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 11 Mar 2025 10:44:49 -0400 Subject: [PATCH 073/384] Slow down host if they're too far ahead of the world Makes packet splitting recover more elegantly during rough net conditions. --- src/d_net.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 689f90848..dfcc8551b 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1073,35 +1073,73 @@ static bool Net_UpdateStatus() if (NetMode != NET_PacketServer) { // Check if everyone has a buffer for us. If they do, we're too far ahead. + bool allUpdated = true; + int highestLatency = 0; for (auto client : NetworkClients) { - if (client != consoleplayer && (ClientStates[client].Flags & CF_UPDATED)) + if (client != consoleplayer) { - updated = true; - int diff = ClientStates[client].SequenceAck - ClientStates[client].CurrentSequence; - if (diff < lowestDiff) - lowestDiff = diff; + if (ClientStates[client].Flags & CF_UPDATED) + { + updated = true; + int diff = ClientStates[client].SequenceAck - ClientStates[client].CurrentSequence; + if (diff < lowestDiff) + lowestDiff = diff; + if (ClientStates[client].AverageLatency > highestLatency) + highestLatency = ClientStates[client].AverageLatency; + } + else + { + allUpdated = false; + } } ClientStates[client].Flags &= ~CF_UPDATED; } + + if (allUpdated) + { + // If we're consistently ahead of the highest latency player we're connected to, slow down + // as well since we should generally be in that ballpark. + const int diff = (ClientTic - gametic) / TicDup; + const int goal = static_cast(ceil((double)highestLatency / TICRATE)) / TicDup + 1; + if (diff > goal) + lowestDiff = diff - goal; + } } else if (consoleplayer == Net_Arbitrator) { // If we're consistenty ahead of the highest sequence player, slow down. + bool allUpdated = true; const int curTic = ClientTic / TicDup; for (auto client : NetworkClients) { - if (client != Net_Arbitrator && (ClientStates[client].Flags & CF_UPDATED)) + if (client != Net_Arbitrator) { - updated = true; - int diff = curTic - ClientStates[client].CurrentSequence; - if (diff < lowestDiff) - lowestDiff = diff; + if (ClientStates[client].Flags & CF_UPDATED) + { + updated = true; + int diff = curTic - ClientStates[client].CurrentSequence; + if (diff < lowestDiff) + lowestDiff = diff; + } + else + { + allUpdated = false; + } } ClientStates[client].Flags &= ~CF_UPDATED; } + + if (allUpdated) + { + // If we're consistently ahead of the world, force a stop here as well. Likely some client + // has fallen super far behind and needs to be reset. + const int diff = curTic - gametic / TicDup; + if (diff > 1) + lowestDiff = diff; + } } else if (ClientStates[Net_Arbitrator].Flags & CF_UPDATED) { From a07a6e7922011f42ce27d58e382957aef9d33f05 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 12 Mar 2025 07:18:38 -0400 Subject: [PATCH 074/384] Fixed first consistency check --- src/d_net.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index dfcc8551b..26502d9a9 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -942,6 +942,7 @@ static void CheckConsistencies() const int limit = min(CurrentConsistency - 1, clientState.CurrentNetConsistency); while (clientState.LastVerifiedConsistency < limit) { + ++clientState.LastVerifiedConsistency; const int tic = clientState.LastVerifiedConsistency % BACKUPTICS; if (clientState.LocalConsistency[tic] != clientState.NetConsistency[tic]) { @@ -949,8 +950,6 @@ static void CheckConsistencies() clientState.LastVerifiedConsistency = clientState.CurrentNetConsistency; break; } - - ++clientState.LastVerifiedConsistency; } } } From 7e410fd5d881a17286ea61d799f6f29a38eb834f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 12 Mar 2025 08:15:19 -0400 Subject: [PATCH 075/384] Added cvar for cool downs on repeatable actions when playing online Currently only applies to doors. --- src/d_net.cpp | 1 + src/playsim/mapthinkers/a_doors.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/d_net.cpp b/src/d_net.cpp index 26502d9a9..2195d6d1c 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -150,6 +150,7 @@ CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later. CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_disablepause, false, CVAR_SERVERINFO | CVAR_NOSAVE) +CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, cl_noboldchat, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Bool, cl_nochatsound, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) diff --git a/src/playsim/mapthinkers/a_doors.cpp b/src/playsim/mapthinkers/a_doors.cpp index e32068565..c13c73aa9 100644 --- a/src/playsim/mapthinkers/a_doors.cpp +++ b/src/playsim/mapthinkers/a_doors.cpp @@ -42,6 +42,8 @@ #include "texturemanager.h" #include "vm.h" +EXTERN_CVAR(Bool, net_repeatableactioncooldown) + //============================================================================ // // VERTICAL DOORS @@ -486,6 +488,10 @@ bool FLevelLocals::EV_DoDoor (DDoor::EVlDoor type, line_t *line, AActor *thing, return false; // JDC: bad guys never close doors //Added by MC: Neither do bots. + // Don't let users spam open/close doors when playing online. + if (net_repeatableactioncooldown && NetworkClients.Size() > 1) + return false; + door->m_Direction = -1; // start going down immediately // Start the door close sequence. From d0e056565b44e94f5cb3f689b458a793809a14f0 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 13 Mar 2025 12:46:49 -0400 Subject: [PATCH 076/384] Exported episode and skill infos --- src/gamedata/g_mapinfo.cpp | 7 ++++ src/gamedata/g_mapinfo.h | 6 ++-- src/gamedata/g_skill.cpp | 52 +++++++++++++++++++++++++++++ src/scripting/thingdef_data.cpp | 8 +++++ wadsrc/static/zscript/doombase.zs | 54 +++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/gamedata/g_mapinfo.cpp b/src/gamedata/g_mapinfo.cpp index ea6192080..350788f33 100644 --- a/src/gamedata/g_mapinfo.cpp +++ b/src/gamedata/g_mapinfo.cpp @@ -2776,3 +2776,10 @@ void G_ParseMapInfo (FString basemapinfo) } } +DEFINE_GLOBAL(AllEpisodes) + +DEFINE_FIELD_X(EpisodeInfo, FEpisode, mEpisodeName) +DEFINE_FIELD_X(EpisodeInfo, FEpisode, mEpisodeMap) +DEFINE_FIELD_X(EpisodeInfo, FEpisode, mPicName) +DEFINE_FIELD_X(EpisodeInfo, FEpisode, mShortcut) +DEFINE_FIELD_X(EpisodeInfo, FEpisode, mNoSkill) diff --git a/src/gamedata/g_mapinfo.h b/src/gamedata/g_mapinfo.h index 25a5ed1ca..d6bd695df 100644 --- a/src/gamedata/g_mapinfo.h +++ b/src/gamedata/g_mapinfo.h @@ -41,6 +41,8 @@ #include "screenjob.h" #include "hwrenderer/postprocessing/hw_postprocess.h" #include "hw_viewpointuniforms.h" +#include "vm.h" +#include "maps.h" struct level_info_t; struct cluster_info_t; @@ -535,9 +537,9 @@ int G_SkillProperty(ESkillProperty prop); double G_SkillProperty(EFSkillProperty prop); const char * G_SkillName(); -typedef TMap SkillMenuNames; +typedef ZSMap SkillMenuNames; -typedef TMap SkillActorReplacement; +typedef ZSMap SkillActorReplacement; struct FSkillInfo { diff --git a/src/gamedata/g_skill.cpp b/src/gamedata/g_skill.cpp index e8d7849ba..837578188 100644 --- a/src/gamedata/g_skill.cpp +++ b/src/gamedata/g_skill.cpp @@ -647,3 +647,55 @@ FName FSkillInfo::GetReplacedBy(FName b) if (Replaced.CheckKey(b)) return Replaced[b]; else return NAME_None; } + +DEFINE_GLOBAL(AllSkills) + +// Avoid Name type clashing +DEFINE_FIELD_NAMED(FSkillInfo, Name, SkillName) +DEFINE_FIELD(FSkillInfo, AmmoFactor) +DEFINE_FIELD(FSkillInfo, DoubleAmmoFactor) +DEFINE_FIELD(FSkillInfo, DropAmmoFactor) +DEFINE_FIELD(FSkillInfo, DamageFactor) +DEFINE_FIELD(FSkillInfo, ArmorFactor) +DEFINE_FIELD(FSkillInfo, HealthFactor) +DEFINE_FIELD(FSkillInfo, KickbackFactor) +DEFINE_FIELD(FSkillInfo, FastMonsters) +DEFINE_FIELD(FSkillInfo, SlowMonsters) +DEFINE_FIELD(FSkillInfo, DisableCheats) +DEFINE_FIELD(FSkillInfo, AutoUseHealth) +DEFINE_FIELD(FSkillInfo, EasyBossBrain) +DEFINE_FIELD(FSkillInfo, EasyKey) +DEFINE_FIELD(FSkillInfo, NoMenu) +DEFINE_FIELD(FSkillInfo, RespawnCounter) +DEFINE_FIELD(FSkillInfo, RespawnLimit) +DEFINE_FIELD(FSkillInfo, Aggressiveness) +DEFINE_FIELD(FSkillInfo, SpawnFilter) +DEFINE_FIELD(FSkillInfo, SpawnMulti) +DEFINE_FIELD(FSkillInfo, InstantReaction) +DEFINE_FIELD(FSkillInfo, SpawnMultiCoopOnly) +DEFINE_FIELD(FSkillInfo, ACSReturn) +DEFINE_FIELD(FSkillInfo, MenuName) +DEFINE_FIELD(FSkillInfo, PicName) +DEFINE_FIELD(FSkillInfo, MenuNamesForPlayerClass) +DEFINE_FIELD(FSkillInfo, MustConfirm) +DEFINE_FIELD(FSkillInfo, MustConfirmText) +DEFINE_FIELD(FSkillInfo, Shortcut) +DEFINE_FIELD(FSkillInfo, TextColor) +DEFINE_FIELD(FSkillInfo, Replace) +DEFINE_FIELD(FSkillInfo, Replaced) +DEFINE_FIELD(FSkillInfo, MonsterHealth) +DEFINE_FIELD(FSkillInfo, FriendlyHealth) +DEFINE_FIELD(FSkillInfo, NoPain) +DEFINE_FIELD(FSkillInfo, Infighting) +DEFINE_FIELD(FSkillInfo, PlayerRespawn) + +static int GetTextColor(FSkillInfo* self) +{ + return self->GetTextColor(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FSkillInfo, GetTextColor, GetTextColor) +{ + PARAM_SELF_STRUCT_PROLOGUE(FSkillInfo); + ACTION_RETURN_INT(self->GetTextColor()); +} diff --git a/src/scripting/thingdef_data.cpp b/src/scripting/thingdef_data.cpp index 930d2b1f6..c1e8dd045 100644 --- a/src/scripting/thingdef_data.cpp +++ b/src/scripting/thingdef_data.cpp @@ -767,6 +767,14 @@ void InitThingdef() terraindefstruct->Size = sizeof(FTerrainDef); terraindefstruct->Align = alignof(FTerrainDef); + auto episodestruct = NewStruct("EpisodeInfo", nullptr, true); + episodestruct->Size = sizeof(FEpisode); + episodestruct->Align = alignof(FEpisode); + + auto skillstruct = NewStruct("SkillInfo", nullptr, true); + skillstruct->Size = sizeof(FSkillInfo); + skillstruct->Align = alignof(FSkillInfo); + PStruct *pstruct = NewStruct("PlayerInfo", nullptr, true); pstruct->Size = sizeof(player_t); pstruct->Align = alignof(player_t); diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 9a3e9fe04..7cd4d3be4 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -23,6 +23,60 @@ extend struct _ native readonly bool playeringame[MAXPLAYERS]; native play LevelLocals Level; + native readonly Array<@EpisodeInfo> AllEpisodes; + native readonly Array<@SkillInfo> AllSkills; +} + +struct EpisodeInfo native +{ + native readonly string mEpisodeName; + native readonly string mEpisodeMap; + native readonly string mPicName; + native readonly int8 mShortcut; + native readonly bool mNoSkill; +} + +struct SkillInfo native +{ + native readonly Name SkillName; + native readonly double AmmoFactor, DoubleAmmoFactor, DropAmmoFactor; + native readonly double DamageFactor; + native readonly double ArmorFactor; + native readonly double HealthFactor; + native readonly double KickbackFactor; + + native readonly bool FastMonsters; + native readonly bool SlowMonsters; + native readonly bool DisableCheats; + native readonly bool AutoUseHealth; + + native readonly bool EasyBossBrain; + native readonly bool EasyKey; + native readonly bool NoMenu; + native readonly int RespawnCounter; + native readonly int RespawnLimit; + native readonly double Aggressiveness; + native readonly int SpawnFilter; + native readonly bool SpawnMulti; + native readonly bool InstantReaction; + native readonly bool SpawnMultiCoopOnly; + native readonly int ACSReturn; + native readonly string MenuName; + native readonly string PicName; + native readonly Map MenuNamesForPlayerClass; + native readonly bool MustConfirm; + native readonly string MustConfirmText; + native readonly int8 Shortcut; + native readonly string TextColor; + native readonly Map Replace; + native readonly Map Replaced; + native readonly double MonsterHealth; + native readonly double FriendlyHealth; + native readonly bool NoPain; + native readonly int Infighting; + native readonly bool PlayerRespawn; + + native int GetTextColor() const; } extend struct TexMan From 345926f05751c770e92ccf7f77e2117e58ca4ebf Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 14 Mar 2025 19:39:29 -0400 Subject: [PATCH 077/384] Fixed setinv cheat --- src/m_cheat.cpp | 2 +- wadsrc/static/zscript/actors/player/player_cheat.zs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m_cheat.cpp b/src/m_cheat.cpp index e918c2408..9cf2314ee 100644 --- a/src/m_cheat.cpp +++ b/src/m_cheat.cpp @@ -570,7 +570,7 @@ FString cht_Morph(player_t *player, PClassActor *morphclass, bool quickundo) void cht_SetInv(player_t *player, const char *string, int amount, bool beyond) { if (!player->mo) return; - IFVIRTUALPTRNAME(player->mo, NAME_PlayerPawn, CheatTakeInv) + IFVIRTUALPTRNAME(player->mo, NAME_PlayerPawn, CheatSetInv) { FString message = string; VMValue params[] = { player->mo, &message, amount, beyond }; diff --git a/wadsrc/static/zscript/actors/player/player_cheat.zs b/wadsrc/static/zscript/actors/player/player_cheat.zs index 3fbb5f30b..25624a3b7 100644 --- a/wadsrc/static/zscript/actors/player/player_cheat.zs +++ b/wadsrc/static/zscript/actors/player/player_cheat.zs @@ -403,7 +403,7 @@ extend class PlayerPawn virtual void CheatSetInv(String strng, int amount, bool beyond) { - if (!(strng ~== "health")) + if (strng ~== "health") { if (amount <= 0) { From d33df9dba8f8bd97a254e461c076eb5e42ffad37 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 17 Mar 2025 12:53:10 -0400 Subject: [PATCH 078/384] Fixed crash with teamplay and the scoreboard Now checks to make sure the player is on a valid team before fetching its info. --- src/hu_scores.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hu_scores.cpp b/src/hu_scores.cpp index bb7999dda..f88621917 100644 --- a/src/hu_scores.cpp +++ b/src/hu_scores.cpp @@ -425,7 +425,8 @@ static void HU_DrawPlayer (player_t *player, bool highlight, int col1, int col2, HU_DrawFontScaled(col5, y + ypadding, color, str); - if (teamplay && Teams[player->userinfo.GetTeam()].GetLogo().IsNotEmpty ()) + const int team = player->userinfo.GetTeam(); + if (team != TEAM_NONE && teamplay && Teams[team].GetLogo().IsNotEmpty ()) { auto pic = TexMan.GetGameTextureByName(Teams[player->userinfo.GetTeam()].GetLogo().GetChars ()); DrawTexture(twod, pic, col1 - (pic->GetDisplayWidth() + 2) * CleanXfac, y, From 1542ca8e8c4512a5e082509a1109ccc6915f385c Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 19 Mar 2025 08:11:29 -0400 Subject: [PATCH 079/384] Added missing serializing for level script controller --- src/playsim/p_acs.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/playsim/p_acs.cpp b/src/playsim/p_acs.cpp index 6fac4c79c..7075dfc8c 100644 --- a/src/playsim/p_acs.cpp +++ b/src/playsim/p_acs.cpp @@ -3519,7 +3519,8 @@ void DLevelScript::Serialize(FSerializer &arc) ("cliprectheight", ClipRectHeight) ("wrapwidth", WrapWidth) ("inmodulescriptnum", InModuleScriptNumber) - ("level", Level); + ("level", Level) + ("controller", controller); if (arc.isReading()) { From 02b5f9a2c5096e40e6f1f1462353595baf29b8cc Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 19 Mar 2025 10:24:21 -0400 Subject: [PATCH 080/384] Fixed player respawning Pass appropriate information to the VM --- src/playsim/p_mobj.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 8dc686f71..4d18899f5 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -6115,8 +6115,10 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag IFVIRTUALPTRNAME(p->mo, NAME_PlayerPawn, ResetAirSupply) { + int drowning = 0; VMValue params[] = { p->mo, false }; - VMCall(func, params, 2, nullptr, 0); + VMReturn rets[] = { &drowning }; + VMCall(func, params, 2, rets, 1); } for (int ii = 0; ii < MAXPLAYERS; ++ii) @@ -6151,7 +6153,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag IFVM(PlayerPawn, FilterCoopRespawnInventory) { VMValue params[] = { p->mo, oldactor, ((heldWeap == nullptr || (heldWeap->ObjectFlags & OF_EuthanizeMe)) ? nullptr : heldWeap) }; - VMCall(func, params, 2, nullptr, 0); + VMCall(func, params, 3, nullptr, 0); } } if (oldactor != NULL) From 43031375f4b7b8a383547f85aaa9456bf15d6f2e Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 19 Mar 2025 13:57:26 -0400 Subject: [PATCH 081/384] Added missing return values in VM calls These are not supported by the JIT and must always be passed. --- src/common/cutscenes/screenjob.cpp | 4 +++- src/g_statusbar/sbarinfo_commands.cpp | 8 ++++---- src/intermission/intermission.cpp | 4 +++- src/p_conversation.cpp | 4 +++- src/playsim/fragglescript/t_func.cpp | 4 +++- src/playsim/p_interaction.cpp | 5 ++++- src/playsim/p_map.cpp | 2 +- src/playsim/p_teleport.cpp | 2 +- 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/common/cutscenes/screenjob.cpp b/src/common/cutscenes/screenjob.cpp index fa3752ba7..6220291ea 100644 --- a/src/common/cutscenes/screenjob.cpp +++ b/src/common/cutscenes/screenjob.cpp @@ -270,8 +270,10 @@ void ScreenJobDraw() ScaleOverrider ovr(twod); IFVIRTUALPTRNAME(cutscene.runner, NAME_ScreenJobRunner, RunFrame) { + int ret = 0; VMValue parm[] = { cutscene.runner, smoothratio }; - VMCall(func, parm, 2, nullptr, 0); + VMReturn rets[] = { &ret }; + VMCall(func, parm, 2, rets, 1); } } } diff --git a/src/g_statusbar/sbarinfo_commands.cpp b/src/g_statusbar/sbarinfo_commands.cpp index a28d43e19..e927b1616 100644 --- a/src/g_statusbar/sbarinfo_commands.cpp +++ b/src/g_statusbar/sbarinfo_commands.cpp @@ -1441,10 +1441,10 @@ class CommandDrawNumber : public CommandDrawString static VMFunction *func = nullptr; if (func == nullptr) PClass::FindFunction(&func, NAME_PlayerPawn, "GetEffectTicsForItem"); VMValue params[] = { statusBar->CPlayer->mo, inventoryItem }; - int retv; - VMReturn ret(&retv); - VMCall(func, params, 2, &ret, 1); - num = retv < 0? 0 : retv / TICRATE + 1; + int ret1 = 0, ret2 = 0; + VMReturn rets[] = { &ret1, &ret2 }; + VMCall(func, params, 2, rets, 2); + num = ret1 < 0? 0 : ret1 / TICRATE + 1; break; } case INVENTORY: diff --git a/src/intermission/intermission.cpp b/src/intermission/intermission.cpp index 24915f9f7..0ae8e41da 100644 --- a/src/intermission/intermission.cpp +++ b/src/intermission/intermission.cpp @@ -325,8 +325,10 @@ void DIntermissionScreenCutscene::Drawer () ScaleOverrider ovr(twod); IFVIRTUALPTRNAME(mScreenJobRunner, NAME_ScreenJobRunner, RunFrame) { + int res = 0; VMValue parm[] = { mScreenJobRunner, I_GetTimeFrac() }; - VMCall(func, parm, 2, nullptr, 0); + VMReturn ret[] = { &res }; + VMCall(func, parm, 2, ret, 1); } } diff --git a/src/p_conversation.cpp b/src/p_conversation.cpp index f51d498f2..417b0e49f 100644 --- a/src/p_conversation.cpp +++ b/src/p_conversation.cpp @@ -218,8 +218,10 @@ static void TakeStrifeItem (player_t *player, PClassActor *itemtype, int amount) IFVM(Actor, TakeInventory) { + int taken = false; VMValue params[] = { player->mo, itemtype, amount, false, false }; - VMCall(func, params, 5, nullptr, 0); + VMReturn rets[] = { &taken }; + VMCall(func, params, 5, rets, 1); } } diff --git a/src/playsim/fragglescript/t_func.cpp b/src/playsim/fragglescript/t_func.cpp index f9fff1b9e..fe7699c35 100644 --- a/src/playsim/fragglescript/t_func.cpp +++ b/src/playsim/fragglescript/t_func.cpp @@ -2532,8 +2532,10 @@ void FParser::SF_PlayerWeapon() IFVM(PlayerPawn, PickNewWeapon) { + AActor* weap = nullptr; VMValue param[] = { Level->Players[playernum]->mo, (void*)nullptr }; - VMCall(func, param, 2, nullptr, 0); + VMReturn rets[] = { (void**)&weap }; + VMCall(func, param, 2, rets, 1); } } } diff --git a/src/playsim/p_interaction.cpp b/src/playsim/p_interaction.cpp index 8d3e5022b..5202041f3 100644 --- a/src/playsim/p_interaction.cpp +++ b/src/playsim/p_interaction.cpp @@ -345,7 +345,10 @@ void AActor::Die (AActor *source, AActor *inflictor, int dmgflags, FName MeansOf } } - VMCall(func, params, 1, nullptr, 0); + AActor* unused = nullptr; + int unused2 = 0, unused3 = 0; + VMReturn ret[] = { (void**)&unused, &unused2, &unused3 }; + VMCall(func, params, 1, ret, 3); // Kill the dummy Actor if it didn't unmorph, otherwise checking the morph flags. Player pawns need // to stay, otherwise they won't respawn correctly. diff --git a/src/playsim/p_map.cpp b/src/playsim/p_map.cpp index 949514f2a..5b427e0f7 100644 --- a/src/playsim/p_map.cpp +++ b/src/playsim/p_map.cpp @@ -215,7 +215,7 @@ bool P_CanCrossLine(AActor *mo, line_t *line, DVector3 next) assert(VIndex != ~0u); } - VMValue params[] = { mo, line, next.X, next.Y, next.Z, false }; + VMValue params[] = { mo, line, next.X, next.Y, next.Z }; VMReturn ret; int retval; ret.IntAt(&retval); diff --git a/src/playsim/p_teleport.cpp b/src/playsim/p_teleport.cpp index a198f2b27..f26bb6fcb 100644 --- a/src/playsim/p_teleport.cpp +++ b/src/playsim/p_teleport.cpp @@ -249,7 +249,7 @@ bool P_Teleport (AActor *thing, DVector3 pos, DAngle angle, int flags) IFVIRTUALPTR(thing, AActor, PostTeleport) { VMValue params[] = { thing, pos.X, pos.Y, pos.Z, angle.Degrees(), flags }; - VMCall(func, params, countof(params), nullptr, 1); + VMCall(func, params, countof(params), nullptr, 0); } } return true; From b4c3d2331e8ace7cf2dacd7bafa9cd2746e545c1 Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Sun, 16 Mar 2025 18:54:35 +0100 Subject: [PATCH 082/384] Fix memory leak in mixins --- src/common/scripting/frontend/zcc_compile.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/common/scripting/frontend/zcc_compile.cpp b/src/common/scripting/frontend/zcc_compile.cpp index 006219d0e..8bd42c0a1 100644 --- a/src/common/scripting/frontend/zcc_compile.cpp +++ b/src/common/scripting/frontend/zcc_compile.cpp @@ -597,8 +597,13 @@ ZCCCompiler::~ZCCCompiler() { delete c; } + for (auto m : Mixins) + { + delete m; + } Structs.Clear(); Classes.Clear(); + Mixins.Clear(); } //========================================================================== From 92cc96a6721f017f4a416a194086a9cad6e990de Mon Sep 17 00:00:00 2001 From: Magnus Norddahl Date: Wed, 19 Mar 2025 22:15:29 +0100 Subject: [PATCH 083/384] Add VMCallScript template for calling ZScript functions with type checking --- src/common/scripting/vm/vm.h | 230 ++++++++++++++++++++++++++++ src/common/scripting/vm/vmframe.cpp | 70 +++++++++ 2 files changed, 300 insertions(+) diff --git a/src/common/scripting/vm/vm.h b/src/common/scripting/vm/vm.h index 2b7344d5d..dba1a36de 100644 --- a/src/common/scripting/vm/vm.h +++ b/src/common/scripting/vm/vm.h @@ -534,6 +534,236 @@ inline int VMCallAction(VMFunction *func, VMValue *params, int numparams, VMRetu return VMCall(func, params, numparams, results, numresults); } +template struct VMReturnTypeTrait { typedef T type; static const int ReturnCount = 1; }; +template<> struct VMReturnTypeTrait { typedef void type; static const int ReturnCount = 0; }; + +void VMCheckParamCount(VMFunction* func, int retcount, int argcount); + +template +void VMCheckParamCount(VMFunction* func, int argcount) { return VMCheckParamCount(func, VMReturnTypeTrait::ReturnCount, argcount); } + +// The type can't be mapped to ZScript automatically: + +template void VMCheckParam(VMFunction* func, int index) = delete; +template void VMCheckReturn(VMFunction* func) = delete; + +// Native types we support converting to/from: + +template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); + +template<> void VMCheckReturn(VMFunction* func); +template<> void VMCheckReturn(VMFunction* func); +template<> void VMCheckReturn(VMFunction* func); +template<> void VMCheckReturn(VMFunction* func); +template<> void VMCheckReturn(VMFunction* func); + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 0); + VMCheckReturn(func); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 1); + VMCheckReturn(func); + VMCheckParam(func, 0); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 2); + VMCheckReturn(func); + VMCheckParam(func, 0); + VMCheckParam(func, 1); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 3); + VMCheckReturn(func); + VMCheckParam(func, 0); + VMCheckParam(func, 1); + VMCheckParam(func, 2); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 4); + VMCheckReturn(func); + VMCheckParam(func, 0); + VMCheckParam(func, 1); + VMCheckParam(func, 2); + VMCheckParam(func, 3); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 5); + VMCheckReturn(func); + VMCheckParam(func, 0); + VMCheckParam(func, 1); + VMCheckParam(func, 2); + VMCheckParam(func, 3); + VMCheckParam(func, 4); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 6); + VMCheckReturn(func); + VMCheckParam(func, 0); + VMCheckParam(func, 1); + VMCheckParam(func, 2); + VMCheckParam(func, 3); + VMCheckParam(func, 4); + VMCheckParam(func, 5); +} + +template void VMValidateSignature(VMFunction* func) +{ + VMCheckParamCount(func, 7); + VMCheckReturn(func); + VMCheckParam(func, 0); + VMCheckParam(func, 1); + VMCheckParam(func, 2); + VMCheckParam(func, 3); + VMCheckParam(func, 4); + VMCheckParam(func, 5); + VMCheckParam(func, 6); +} + +void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMReturn* results, int numresults); + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1) +{ + VMValidateSignature(func); + VMValue params[] = { p1 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 1, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 2, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 3, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 4, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4, p5 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 5, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4, p5, p6 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 6, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4, p5, p6, p7 }; + RetVal resultval; VMReturn results(&resultval); + VMCallCheckResult(func, params, 7, &results, 1); + return resultval; +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1) +{ + VMValidateSignature(func); + VMValue params[1] = { p1 }; + VMCallCheckResult(func, params, 1, nullptr, 0); +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2 }; + VMCallCheckResult(func, params, 2, nullptr, 0); +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3 }; + VMCallCheckResult(func, params, 3, nullptr, 0); +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4 }; + VMCallCheckResult(func, params, 4, nullptr, 0); +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4, p5 }; + VMCallCheckResult(func, params, 5, nullptr, 0); +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4, p5, p6 }; + VMCallCheckResult(func, params, 6, nullptr, 0); +} + +template +typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7) +{ + VMValidateSignature(func); + VMValue params[] = { p1, p2, p3, p4, p5, p6, p7 }; + VMCallCheckResult(func, params, 7, nullptr, 0); +} + // Use these to collect the parameters in a native function. // variable name at position

[[noreturn]] diff --git a/src/common/scripting/vm/vmframe.cpp b/src/common/scripting/vm/vmframe.cpp index f69b8ea61..e04c6f0d7 100644 --- a/src/common/scripting/vm/vmframe.cpp +++ b/src/common/scripting/vm/vmframe.cpp @@ -552,6 +552,76 @@ VMFrame *VMFrameStack::PopFrame() return parent; } +//=========================================================================== +// Templates for calling ZScript from C++ with type checks + +void VMCheckParamCount(VMFunction* func, int retcount, int argcount) +{ + if (func->Proto->ReturnTypes.Size() != retcount) + I_FatalError("Incorrect return value passed to %s", func->PrintableName); + if (func->Proto->ArgumentTypes.Size() != argcount) + I_FatalError("Incorrect parameter count passed to %s", func->PrintableName); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (!func->Proto->ArgumentTypes[index]->isIntCompatible()) + I_FatalError("%s argument %d is not an integer", func->PrintableName); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index] != TypeFloat64) + I_FatalError("%s argument %d is not a double", func->PrintableName); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index] != TypeString) + I_FatalError("%s argument %d is not a string", func->PrintableName); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index]->isObjectPointer()) + I_FatalError("%s argument %d is not an object", func->PrintableName); +} + +template<> void VMCheckReturn(VMFunction* func) +{ +} + +template<> void VMCheckReturn(VMFunction* func) +{ + if (!func->Proto->ReturnTypes[0]->isIntCompatible()) + I_FatalError("%s return value %d is not an integer", func->PrintableName); +} + +template<> void VMCheckReturn(VMFunction* func) +{ + if (func->Proto->ReturnTypes[0] != TypeFloat64) + I_FatalError("%s return value %d is not a double", func->PrintableName); +} + +template<> void VMCheckReturn(VMFunction* func) +{ + if (func->Proto->ReturnTypes[0] != TypeString) + I_FatalError("%s return value %d is not a string", func->PrintableName); +} + +template<> void VMCheckReturn(VMFunction* func) +{ + if (func->Proto->ReturnTypes[0]->isObjectPointer()) + I_FatalError("%s return value %d is not an object", func->PrintableName); +} + +void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMReturn* results, int numresults) +{ + int retval = VMCall(func, params, numparams, results, numresults); + if (retval != numresults) + I_FatalError("%s did not return the expected number of results", func->PrintableName); +} + //=========================================================================== // // VMFrameStack :: Call From f5a96237e5382270ff57878ab956508160e6bb18 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 20 Mar 2025 07:04:51 -0400 Subject: [PATCH 084/384] You client-side flag instead of storing ACS controller Prevents old save files from breaking if they were running ACS. --- src/playsim/p_acs.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/playsim/p_acs.cpp b/src/playsim/p_acs.cpp index 7075dfc8c..aa26c0163 100644 --- a/src/playsim/p_acs.cpp +++ b/src/playsim/p_acs.cpp @@ -705,7 +705,6 @@ public: protected: DLevelScript *next, *prev; - TObjPtr controller; int script; TArray Localvars; int *pc; @@ -714,6 +713,7 @@ protected: TObjPtr activator; line_t *activationline; bool backSide; + bool bClientSide; FFont *activefont = nullptr; int hudwidth, hudheight; int ClipRectLeft, ClipRectTop, ClipRectWidth, ClipRectHeight; @@ -741,6 +741,7 @@ protected: } } + DACSThinker* GetController() const; void Link(); void Unlink(); void PutLast(); @@ -3520,7 +3521,7 @@ void DLevelScript::Serialize(FSerializer &arc) ("wrapwidth", WrapWidth) ("inmodulescriptnum", InModuleScriptNumber) ("level", Level) - ("controller", controller); + ("bclientside", bClientSide); if (arc.isReading()) { @@ -3535,8 +3536,14 @@ void DLevelScript::Serialize(FSerializer &arc) } } +DACSThinker* DLevelScript::GetController() const +{ + return bClientSide ? Level->ClientSideACSThinker : Level->ACSThinker; +} + void DLevelScript::Unlink () { + auto controller = GetController(); if (controller->LastScript == this) { controller->LastScript = prev; @@ -3561,6 +3568,7 @@ void DLevelScript::Unlink () void DLevelScript::Link () { + auto controller = GetController(); next = controller->Scripts; GC::WriteBarrier(this, next); if (controller->Scripts) @@ -3579,6 +3587,7 @@ void DLevelScript::Link () void DLevelScript::PutLast () { + auto controller = GetController(); if (controller->LastScript == this) return; @@ -3599,6 +3608,7 @@ void DLevelScript::PutLast () void DLevelScript::PutFirst () { + auto controller = GetController(); if (controller->Scripts == this) return; @@ -6941,6 +6951,7 @@ int DLevelScript::RunScript() // Hexen truncates all special arguments to bytes (only when using an old MAPINFO and old ACS format const int specialargmask = ((Level->flags2 & LEVEL2_HEXENHACK) && activeBehavior->GetFormat() == ACS_Old) ? 255 : ~0; + auto controller = GetController(); switch (state) { @@ -10435,14 +10446,14 @@ DLevelScript::DLevelScript (FLevelLocals *l, AActor *who, line_t *where, int num if (Level->ClientSideACSThinker == nullptr) Level->ClientSideACSThinker = Level->CreateClientsideThinker(); - controller = Level->ClientSideACSThinker; + bClientSide = true; } else { if (Level->ACSThinker == nullptr) Level->ACSThinker = Level->CreateThinker(); - controller = Level->ACSThinker; + bClientSide = false; } script = num; @@ -10470,7 +10481,7 @@ DLevelScript::DLevelScript (FLevelLocals *l, AActor *who, line_t *where, int num // goes by while they're in their default state. if (!(flags & ACS_ALWAYS)) - controller->RunningScripts[num] = this; + GetController()->RunningScripts[num] = this; Link(); From 2d42438ebdfe19f3f7e1c2f9225b32d64115ed47 Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Fri, 14 Mar 2025 00:26:28 +0800 Subject: [PATCH 085/384] Properly assign tags to various Raven game items --- wadsrc/static/language.def | 21 +++++++++++++++++++ .../zscript/actors/heretic/hereticammo.zs | 1 + .../zscript/actors/heretic/hereticarmor.zs | 2 ++ .../actors/heretic/hereticartifacts.zs | 1 + .../zscript/actors/heretic/heretickeys.zs | 3 +++ .../static/zscript/actors/hexen/hexenkeys.zs | 11 ++++++++++ .../zscript/actors/raven/ravenhealth.zs | 1 + 7 files changed, 40 insertions(+) diff --git a/wadsrc/static/language.def b/wadsrc/static/language.def index 103d4d3a9..2700fbb20 100644 --- a/wadsrc/static/language.def +++ b/wadsrc/static/language.def @@ -391,5 +391,26 @@ TAG_ARTIINVULNERABILITY = "$$TXT_ARTIINVULNERABILITY"; TAG_ARTITELEPORT = "$$TXT_ARTITELEPORT"; TAG_ARTITOMEOFPOWER = "$$TXT_ARTITOMEOFPOWER"; TAG_ARTITORCH = "$$TXT_ARTITORCH"; +TAG_ITEMBAGOFHOLDING = "$$TXT_ITEMBAGOFHOLDING"; +TAG_ITEMSHIELD1 = "$$TXT_ITEMSHIELD2"; +TAG_ITEMSHIELD2 = "$$TXT_ITEMSHIELD2"; +TAG_GOTGREENKEY = "$$TXT_GOTGREENKEY"; +TAG_GOTBLUEKEY = "$$TXT_GOTBLUEKEY"; +TAG_GOTYELLOWKEY = "$$TXT_GOTYELLOWKEY"; +TAG_ITEMSUPERMAP = "$$TXT_ITEMSUPERMAP"; + +TAG_KEY_STEEL = "$$TXT_KEY_STEEL"; +TAG_KEY_CAVE = "$$TXT_KEY_CAVE"; +TAG_KEY_AXE = "$$TXT_KEY_AXE"; +TAG_KEY_FIRE = "$$TXT_KEY_FIRE"; +TAG_KEY_EMERALD = "$$TXT_KEY_EMERALD"; +TAG_KEY_DUNGEON = "$$TXT_KEY_DUNGEON"; +TAG_KEY_SILVER = "$$TXT_KEY_SILVER"; +TAG_KEY_RUSTED = "$$TXT_KEY_RUSTED"; +TAG_KEY_HORN = "$$TXT_KEY_HORN"; +TAG_KEY_SWAMP = "$$TXT_KEY_SWAMP"; +TAG_KEY_CASTLE = "$$TXT_KEY_CASTLE"; + +TAG_ITEMHEALTH = "$$TXT_ITEMHEALTH"; OPTMNU_SWRENDERER = "$$DSPLYMNU_SWOPT"; diff --git a/wadsrc/static/zscript/actors/heretic/hereticammo.zs b/wadsrc/static/zscript/actors/heretic/hereticammo.zs index 6464cf9dc..daf6d3ea7 100644 --- a/wadsrc/static/zscript/actors/heretic/hereticammo.zs +++ b/wadsrc/static/zscript/actors/heretic/hereticammo.zs @@ -237,6 +237,7 @@ Class BagOfHolding : BackpackItem Default { Inventory.PickupMessage "$TXT_ITEMBAGOFHOLDING"; + Tag "$TAG_ITEMBAGOFHOLDING"; +COUNTITEM +FLOATBOB } diff --git a/wadsrc/static/zscript/actors/heretic/hereticarmor.zs b/wadsrc/static/zscript/actors/heretic/hereticarmor.zs index 8cbcb9fbc..b95a71ad9 100644 --- a/wadsrc/static/zscript/actors/heretic/hereticarmor.zs +++ b/wadsrc/static/zscript/actors/heretic/hereticarmor.zs @@ -7,6 +7,7 @@ Class SilverShield : BasicArmorPickup { +FLOATBOB Inventory.Pickupmessage "$TXT_ITEMSHIELD1"; + Tag "$TAG_ITEMSHIELD1"; Inventory.Icon "SHLDA0"; Armor.Savepercent 50; Armor.Saveamount 100; @@ -27,6 +28,7 @@ Class EnchantedShield : BasicArmorPickup { +FLOATBOB Inventory.Pickupmessage "$TXT_ITEMSHIELD2"; + Tag "$TAG_ITEMSHIELD2"; Inventory.Icon "SHD2A0"; Armor.Savepercent 75; Armor.Saveamount 200; diff --git a/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs b/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs index b620b04b4..117d37586 100644 --- a/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs +++ b/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs @@ -9,6 +9,7 @@ Class SuperMap : MapRevealer +FLOATBOB Inventory.MaxAmount 0; Inventory.PickupMessage "$TXT_ITEMSUPERMAP"; + Tag "$TAG_ITEMSUPERMAP"; } States { diff --git a/wadsrc/static/zscript/actors/heretic/heretickeys.zs b/wadsrc/static/zscript/actors/heretic/heretickeys.zs index d5532ce01..b2bf3f363 100644 --- a/wadsrc/static/zscript/actors/heretic/heretickeys.zs +++ b/wadsrc/static/zscript/actors/heretic/heretickeys.zs @@ -16,6 +16,7 @@ Class KeyGreen : HereticKey Default { Inventory.PickupMessage "$TXT_GOTGREENKEY"; + Tag "$TAG_GOTGREENKEY"; Inventory.Icon "GKEYICON"; } States @@ -33,6 +34,7 @@ Class KeyBlue : HereticKey Default { Inventory.PickupMessage "$TXT_GOTBLUEKEY"; + Tag "$TAG_GOTBLUEKEY"; Inventory.Icon "BKEYICON"; } States @@ -50,6 +52,7 @@ Class KeyYellow : HereticKey Default { Inventory.PickupMessage "$TXT_GOTYELLOWKEY"; + Tag "$TAG_GOTYELLOWKEY"; Inventory.Icon "YKEYICON"; } States diff --git a/wadsrc/static/zscript/actors/hexen/hexenkeys.zs b/wadsrc/static/zscript/actors/hexen/hexenkeys.zs index 9bf9f426c..c0ec85ce7 100644 --- a/wadsrc/static/zscript/actors/hexen/hexenkeys.zs +++ b/wadsrc/static/zscript/actors/hexen/hexenkeys.zs @@ -14,6 +14,7 @@ class KeySteel : HexenKey { Inventory.Icon "KEYSLOT1"; Inventory.PickupMessage "$TXT_KEY_STEEL"; + Tag "$TAG_KEY_STEEL"; } States { @@ -29,6 +30,7 @@ class KeyCave : HexenKey { Inventory.Icon "KEYSLOT2"; Inventory.PickupMessage "$TXT_KEY_CAVE"; + Tag "$TAG_KEY_CAVE"; } States { @@ -44,6 +46,7 @@ class KeyAxe : HexenKey { Inventory.Icon "KEYSLOT3"; Inventory.PickupMessage "$TXT_KEY_AXE"; + Tag "$TAG_KEY_AXE"; } States { @@ -59,6 +62,7 @@ class KeyFire : HexenKey { Inventory.Icon "KEYSLOT4"; Inventory.PickupMessage "$TXT_KEY_FIRE"; + Tag "$TAG_KEY_FIRE"; } States { @@ -74,6 +78,7 @@ class KeyEmerald : HexenKey { Inventory.Icon "KEYSLOT5"; Inventory.PickupMessage "$TXT_KEY_EMERALD"; + Tag "$TAG_KEY_EMERALD"; } States { @@ -89,6 +94,7 @@ class KeyDungeon : HexenKey { Inventory.Icon "KEYSLOT6"; Inventory.PickupMessage "$TXT_KEY_DUNGEON"; + Tag "$TAG_KEY_DUNGEON"; } States { @@ -104,6 +110,7 @@ class KeySilver : HexenKey { Inventory.Icon "KEYSLOT7"; Inventory.PickupMessage "$TXT_KEY_SILVER"; + Tag "$TAG_KEY_SILVER"; } States { @@ -119,6 +126,7 @@ class KeyRusted : HexenKey { Inventory.Icon "KEYSLOT8"; Inventory.PickupMessage "$TXT_KEY_RUSTED"; + Tag "$TAG_KEY_RUSTED"; } States { @@ -134,6 +142,7 @@ class KeyHorn : HexenKey { Inventory.Icon "KEYSLOT9"; Inventory.PickupMessage "$TXT_KEY_HORN"; + Tag "$TAG_KEY_HORN"; } States { @@ -149,6 +158,7 @@ class KeySwamp : HexenKey { Inventory.Icon "KEYSLOTA"; Inventory.PickupMessage "$TXT_KEY_SWAMP"; + Tag "$TAG_KEY_SWAMP"; } States { @@ -164,6 +174,7 @@ class KeyCastle : HexenKey { Inventory.Icon "KEYSLOTB"; Inventory.PickupMessage "$TXT_KEY_CASTLE"; + Tag "$TAG_KEY_CASTLE"; } States { diff --git a/wadsrc/static/zscript/actors/raven/ravenhealth.zs b/wadsrc/static/zscript/actors/raven/ravenhealth.zs index cf7c79d0a..7bf14ae15 100644 --- a/wadsrc/static/zscript/actors/raven/ravenhealth.zs +++ b/wadsrc/static/zscript/actors/raven/ravenhealth.zs @@ -5,6 +5,7 @@ class CrystalVial : Health +FLOATBOB Inventory.Amount 10; Inventory.PickupMessage "$TXT_ITEMHEALTH"; + Tag "$TAG_ITEMHEALTH"; } States { From c91912567eb1ffc411d1f90ea00e7f36c03c88f5 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Mon, 24 Mar 2025 09:38:51 +0100 Subject: [PATCH 086/384] disable Build light mode due to being broken. --- src/g_level.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/g_level.cpp b/src/g_level.cpp index 97a17a554..38b6e5feb 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -150,7 +150,7 @@ CUSTOM_CVAR(Bool, gl_notexturefill, false, CVAR_NOINITCALL) CUSTOM_CVAR(Int, gl_maplightmode, -1, CVAR_NOINITCALL | CVAR_CHEAT) // this is just for testing. -1 means 'inactive' { - if (self > 5 || self < -1) self = -1; + if (self > 4 || self < -1) self = -1; } CUSTOM_CVARD(Int, gl_lightmode, 1, CVAR_ARCHIVE, "Select lighting mode. 2 is vanilla accurate, 1 is accurate to the ZDoom software renderer and 0 is a less demanding non-shader implementation") From 302d27978541e10300e6f3cebcacc7a6085404c5 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Mon, 24 Mar 2025 09:44:14 +0100 Subject: [PATCH 087/384] forgot to save the MAPINFO part. --- src/gamedata/g_mapinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gamedata/g_mapinfo.cpp b/src/gamedata/g_mapinfo.cpp index 350788f33..fee2347cb 100644 --- a/src/gamedata/g_mapinfo.cpp +++ b/src/gamedata/g_mapinfo.cpp @@ -1580,7 +1580,7 @@ DEFINE_MAP_OPTION(lightmode, false) parse.sc.MustGetNumber(); if (parse.sc.Number == 8 || parse.sc.Number == 16) info->lightmode = ELightMode::NotSet; - else if (parse.sc.Number >= 0 && parse.sc.Number <= 5) + else if (parse.sc.Number >= 0 && parse.sc.Number < 5) { info->lightmode = ELightMode(parse.sc.Number); } From 5b1023c44781a38551c9d378f7781b74ae248bf0 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 24 Mar 2025 21:30:01 -0400 Subject: [PATCH 088/384] Fixed inventory ticking when playing online No longer tied to the player's latency value as it now runs in the client-side logic. --- src/p_tick.cpp | 6 ++++++ wadsrc/static/zscript/actors/player/player.zs | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/p_tick.cpp b/src/p_tick.cpp index b952aa23a..18589a01b 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -67,6 +67,12 @@ void P_RunClientsideLogic() if (gamestate == GS_LEVEL || gamestate == GS_TITLELEVEL) { + for (int i = 0; i < MAXPLAYERS; ++i) + { + if (playeringame[i] && players[i].inventorytics > 0) + --players[i].inventorytics; + } + for (auto level : AllLevels()) { auto it = level->GetClientsideThinkerIterator(); diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index 1c87c515c..eda846144 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -1671,10 +1671,6 @@ class PlayerPawn : Actor ++BobTimer; CheckFOV(); - if (player.inventorytics) - { - player.inventorytics--; - } CheckCheats(); if (bJustAttacked) From 134e8f2d607b01fa11e5ae2a5dad7975e2aa15c7 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 24 Mar 2025 09:55:40 -0400 Subject: [PATCH 089/384] Fixed potential softlock in packet-server mode Make sure to send over the lowest player's data we got as the ack and not whatever the host specifically had --- src/d_net.cpp | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 2195d6d1c..cb695bb95 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1409,16 +1409,31 @@ void NetUpdate(int tics) curState.Flags &= ~CF_MISSING; NetBuffer[1] = (curState.Flags & CF_RETRANSMIT_SEQ) ? curState.ResendID : CurrentLobbyID; + int lastSeq = curState.CurrentSequence; + int lastCon = curState.CurrentNetConsistency; + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + { + // If in packet-server mode, make sure to get the lowest sequence of all players + // since the host themselves might have gotten updated but someone else in the packet + // did not. That way the host knows to send over the correct tic. + for (auto cl : NetworkClients) + { + if (ClientStates[cl].CurrentSequence < lastSeq) + lastSeq = ClientStates[cl].CurrentSequence; + if (ClientStates[cl].CurrentNetConsistency < lastCon) + lastCon = ClientStates[cl].CurrentNetConsistency; + } + } // Last sequence we got from this client. - NetBuffer[2] = (curState.CurrentSequence >> 24); - NetBuffer[3] = (curState.CurrentSequence >> 16); - NetBuffer[4] = (curState.CurrentSequence >> 8); - NetBuffer[5] = curState.CurrentSequence; + NetBuffer[2] = (lastSeq >> 24); + NetBuffer[3] = (lastSeq >> 16); + NetBuffer[4] = (lastSeq >> 8); + NetBuffer[5] = lastSeq; // Last consistency we got from this client. - NetBuffer[6] = (curState.CurrentNetConsistency >> 24); - NetBuffer[7] = (curState.CurrentNetConsistency >> 16); - NetBuffer[8] = (curState.CurrentNetConsistency >> 8); - NetBuffer[9] = curState.CurrentNetConsistency; + NetBuffer[6] = (lastCon >> 24); + NetBuffer[7] = (lastCon >> 16); + NetBuffer[8] = (lastCon >> 8); + NetBuffer[9] = lastCon; if (curState.Flags & CF_RETRANSMIT_SEQ) { From 154eea56e6ab3e084dfacdbaf6f8296277693105 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 24 Mar 2025 10:23:36 -0400 Subject: [PATCH 090/384] Update all players' acks in packet-server mode Fixes potential issue when host switching in packet-server mode --- src/d_net.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/d_net.cpp b/src/d_net.cpp index cb695bb95..98bf60021 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -873,6 +873,9 @@ static void GetPackets() { pState.CurrentSequence = seq; } + // Update this so host switching doesn't have any hiccups in packet-server mode. + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator && pNum != Net_Arbitrator) + pState.SequenceAck = seq; } } } From ac42aa83376e5a3c03bffe09ff40e0fea91670d6 Mon Sep 17 00:00:00 2001 From: Sally Coolatta Date: Tue, 25 Mar 2025 06:41:46 -0400 Subject: [PATCH 091/384] cl_debugprediction console variable Makes `gametic` run behind `ClientTic` by n ticks, and forces player prediction code to run. Only works if you are in a singleplayer session. This is intended to be useful for mod development, as you can quickly test prediction issues without creating a multiplayer server. It may also come in handy if any improvements are made to the prediction code in the future. --- src/d_net.cpp | 22 +++++++++++++++++++++- src/playsim/p_user.cpp | 4 +++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 98bf60021..68959425d 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -162,6 +162,14 @@ CUSTOM_CVAR(Int, cl_showchat, CHAT_GLOBAL, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) self = CHAT_GLOBAL; } +CUSTOM_CVAR(Int, cl_debugprediction, 0, CVAR_CHEAT) +{ + if (self < 0) + self = 0; + else if (self > BACKUPTICS - 1) + self = BACKUPTICS - 1; +} + // Used to write out all network events that occured leading up to the next tick. static struct NetEventData { @@ -1969,7 +1977,19 @@ void TryRunTics() int runTics = min(totalTics, availableTics); if (totalTics > 0 && totalTics < availableTics - 1 && !singletics) ++runTics; - + + // Test player prediction code in singleplayer + // by running the gametic behind the ClientTic + if (!netgame && !demoplayback && cl_debugprediction > 0) + { + int debugTarget = ClientTic - cl_debugprediction; + int debugOffset = gametic - debugTarget; + if (debugOffset > 0) + { + runTics = max(runTics - debugOffset, 0); + } + } + // If there are no tics to run, check for possible stall conditions and new // commands to predict. if (runTics <= 0) diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 956337331..2580cb41e 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -133,6 +133,8 @@ CUSTOM_CVAR(Float, cl_rubberband_limit, 756.0f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG self = 0.0f; } +EXTERN_CVAR (Int, cl_debugprediction) + ColorSetList ColorSets; PainFlashList PainFlashes; @@ -1440,7 +1442,7 @@ void P_PredictPlayer (player_t *player) player->mo == NULL || player != player->mo->Level->GetConsolePlayer() || player->playerstate != PST_LIVE || - !netgame || + (!netgame && cl_debugprediction == 0) || /*player->morphTics ||*/ (player->cheats & CF_PREDICTING)) { From 3d0663d29905cff6c94d83980b9bc4698899efd4 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Tue, 25 Mar 2025 13:17:14 -0400 Subject: [PATCH 092/384] - add new dsquake by Enjay --- wadsrc/static/sounds/dsquake.ogg | Bin 15728 -> 16573 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/wadsrc/static/sounds/dsquake.ogg b/wadsrc/static/sounds/dsquake.ogg index 6df679254d271532099e13b91f39419b32e880f9..79317bae7671111363de73b99a885af2b7c90352 100644 GIT binary patch literal 16573 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 HA4&`WWr}D@ literal 15728 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 From da6730d0a7b8ab6ac330529d1d9a54c0a8ef25d4 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sat, 29 Mar 2025 09:36:36 -0600 Subject: [PATCH 093/384] Addressing some small regression by conditioning a few calculations on OoB viewpoints. Branching Frustum calculation to old method. --- src/rendering/hwrenderer/scene/hw_bsp.cpp | 26 ++++++++----- .../hwrenderer/scene/hw_drawinfo.cpp | 39 ++++++++++++++----- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_bsp.cpp b/src/rendering/hwrenderer/scene/hw_bsp.cpp index 3f834fcf6..a927872f6 100644 --- a/src/rendering/hwrenderer/scene/hw_bsp.cpp +++ b/src/rendering/hwrenderer/scene/hw_bsp.cpp @@ -285,19 +285,25 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) angle_t startAngle = clipper.GetClipAngle(seg->v2); angle_t endAngle = clipper.GetClipAngle(seg->v1); auto &clipperr = *rClipper; - angle_t startAngleR = clipperr.PointToPseudoAngle(seg->v2->fX(), seg->v2->fY()); - angle_t endAngleR = clipperr.PointToPseudoAngle(seg->v1->fX(), seg->v1->fY()); + angle_t startAngleR = 0; + angle_t endAngleR = 0; angle_t paddingR = 0x00200000; // Make radar clipping more aggressive (reveal less) - if(Viewpoint.IsAllowedOoB() && r_radarclipper && !(Level->flags3 & LEVEL3_NOFOGOFWAR) && (startAngleR - endAngleR >= ANGLE_180)) + if(Viewpoint.IsAllowedOoB() && r_radarclipper && !(Level->flags3 & LEVEL3_NOFOGOFWAR)) { - if (!seg->backsector) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); - else if((seg->sidedef != nullptr) && !uint8_t(seg->sidedef->Flags & WALLF_POLYOBJ) && (currentsector->sectornum != seg->backsector->sectornum)) + startAngleR = clipperr.PointToPseudoAngle(seg->v2->fX(), seg->v2->fY()); + endAngleR = clipperr.PointToPseudoAngle(seg->v1->fX(), seg->v1->fY()); + + if (startAngleR - endAngleR >= ANGLE_180) { - if (in_area == area_default) in_area = hw_CheckViewArea(seg->v1, seg->v2, seg->frontsector, seg->backsector); - backsector = hw_FakeFlat(seg->backsector, in_area, true); - if (hw_CheckClip(seg->sidedef, currentsector, backsector)) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); - backsector = nullptr; + if (!seg->backsector) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); + else if((seg->sidedef != nullptr) && !uint8_t(seg->sidedef->Flags & WALLF_POLYOBJ) && (currentsector->sectornum != seg->backsector->sectornum)) + { + if (in_area == area_default) in_area = hw_CheckViewArea(seg->v1, seg->v2, seg->frontsector, seg->backsector); + backsector = hw_FakeFlat(seg->backsector, in_area, true); + if (hw_CheckClip(seg->sidedef, currentsector, backsector)) clipperr.SafeAddClipRange(startAngleR - paddingR, endAngleR + paddingR); + backsector = nullptr; + } } } @@ -708,7 +714,7 @@ void HWDrawInfo::DoSubsector(subsector_t * sub) if(Viewpoint.IsAllowedOoB() && sector->isSecret() && sector->wasSecret() && !r_radarclipper) return; // cull everything if subsector outside all relevant clippers - if ((sub->polys == nullptr)) + if (Viewpoint.IsAllowedOoB() && (sub->polys == nullptr)) { auto &clipper = *mClipper; auto &clipperv = *vClipper; diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 404203933..0e647e307 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -268,8 +268,8 @@ void HWDrawInfo::ClearBuffers() void HWDrawInfo::UpdateCurrentMapSection() { int mapsection = Level->PointInRenderSubsector(Viewpoint.Pos)->mapsection; - if (Viewpoint.IsAllowedOoB()) - mapsection = Level->PointInRenderSubsector(Viewpoint.camera->Pos())->mapsection; + if (Viewpoint.IsAllowedOoB() || Viewpoint.IsOrtho()) + mapsection = Level->PointInRenderSubsector(Viewpoint.OffPos)->mapsection; CurrentMapSections.Set(mapsection); } @@ -363,20 +363,19 @@ int HWDrawInfo::SetFullbrightFlags(player_t *player) // //----------------------------------------------------------------------------- -angle_t HWDrawInfo::FrustumAngle() +angle_t OoBFrustumAngle(FRenderViewpoint* Viewpoint) { // If pitch is larger than this you can look all around at an FOV of 90 degrees - if (fabs(Viewpoint.HWAngles.Pitch.Degrees()) > 89.0) return 0xffffffff; - else if (fabs(Viewpoint.HWAngles.Pitch.Degrees()) > 46.0 && !Viewpoint.IsAllowedOoB()) return 0xffffffff; // Just like 4.12.2 and older did + if (fabs(Viewpoint->HWAngles.Pitch.Degrees()) > 89.0) return 0xffffffff; int aspMult = AspectMultiplier(r_viewwindow.WidescreenRatio); // 48 == square window - double absPitch = fabs(Viewpoint.HWAngles.Pitch.Degrees()); + double absPitch = fabs(Viewpoint->HWAngles.Pitch.Degrees()); // Smaller aspect ratios still clip too much. Need a better solution if (aspMult > 36 && absPitch > 30.0) return 0xffffffff; else if (aspMult > 40 && absPitch > 25.0) return 0xffffffff; else if (aspMult > 45 && absPitch > 20.0) return 0xffffffff; else if (aspMult > 47 && absPitch > 10.0) return 0xffffffff; - double xratio = r_viewwindow.FocalTangent / Viewpoint.PitchCos; + double xratio = r_viewwindow.FocalTangent / Viewpoint->PitchCos; double floatangle = 0.05 + atan ( xratio ) * 48.0 / aspMult; // this is radians angle_t a1 = DAngle::fromRad(floatangle).BAMs(); @@ -384,6 +383,28 @@ angle_t HWDrawInfo::FrustumAngle() return a1; } +angle_t HWDrawInfo::FrustumAngle() +{ + if (Viewpoint.IsAllowedOoB()) + { + return OoBFrustumAngle(&Viewpoint); + } + else + { + float tilt = fabs(Viewpoint.HWAngles.Pitch.Degrees()); + + // If the pitch is larger than this you can look all around at a FOV of 90° + if (tilt > 46.0f) return 0xffffffff; + + // ok, this is a gross hack that barely works... + // but at least it doesn't overestimate too much... + double floatangle = 2.0 + (45.0 + ((tilt / 1.9)))*Viewpoint.FieldOfView.Degrees() * 48.0 / AspectMultiplier(r_viewwindow.WidescreenRatio) / 90.0; + angle_t a1 = DAngle::fromDeg(floatangle).BAMs(); + if (a1 >= ANGLE_180) return 0xffffffff; + return a1; + } +} + //----------------------------------------------------------------------------- // // Setup the modelview matrix @@ -1043,8 +1064,8 @@ void HWDrawInfo::ProcessScene(bool toscreen) portalState.BeginScene(); int mapsection = Level->PointInRenderSubsector(Viewpoint.Pos)->mapsection; - if (Viewpoint.IsAllowedOoB()) - mapsection = Level->PointInRenderSubsector(Viewpoint.camera->Pos())->mapsection; + if (Viewpoint.IsAllowedOoB() || Viewpoint.IsOrtho()) + mapsection = Level->PointInRenderSubsector(Viewpoint.OffPos)->mapsection; CurrentMapSections.Set(mapsection); DrawScene(toscreen ? DM_MAINVIEW : DM_OFFSCREEN); From 6f3032dc547296c7623986514a337168a394178c Mon Sep 17 00:00:00 2001 From: MajorCooke Date: Sun, 30 Mar 2025 18:15:53 -0500 Subject: [PATCH 094/384] Exported: * GetLumpContainer * GetContainerName * GetLumpFullPath for WADS struct, useful for debugging custom-made parsers and identifying where problems may arise. All credit goes to Jay for the code. --- src/common/scripting/interface/vmnatives.cpp | 21 ++++++++++++++++++++ wadsrc/static/zscript/engine/base.zs | 3 +++ 2 files changed, 24 insertions(+) diff --git a/src/common/scripting/interface/vmnatives.cpp b/src/common/scripting/interface/vmnatives.cpp index c1bb2a2b5..1b4cdb746 100644 --- a/src/common/scripting/interface/vmnatives.cpp +++ b/src/common/scripting/interface/vmnatives.cpp @@ -853,6 +853,27 @@ DEFINE_ACTION_FUNCTION(_Wads, GetLumpFullName) ACTION_RETURN_STRING(fileSystem.GetFileFullName(lump)); } +DEFINE_ACTION_FUNCTION(_Wads, GetLumpContainer) +{ + PARAM_PROLOGUE; + PARAM_INT(lump); + ACTION_RETURN_INT(fileSystem.GetFileContainer(lump)); +} + +DEFINE_ACTION_FUNCTION(_Wads, GetContainerName) +{ + PARAM_PROLOGUE; + PARAM_INT(lump); + ACTION_RETURN_STRING(fileSystem.GetResourceFileName(lump)); +} + +DEFINE_ACTION_FUNCTION(_Wads, GetLumpFullPath) +{ + PARAM_PROLOGUE; + PARAM_INT(lump); + ACTION_RETURN_STRING(fileSystem.GetFileFullPath(lump)); +} + DEFINE_ACTION_FUNCTION(_Wads, GetLumpNamespace) { PARAM_PROLOGUE; diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index 04789cdd4..ee9161656 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -898,6 +898,9 @@ struct Wads // todo: make FileSystem an alias to 'Wads' native static string GetLumpName(int lump); native static string GetLumpFullName(int lump); native static int GetLumpNamespace(int lump); + native static int GetLumpContainer(int lump); + native static string GetContainerName(int lump); + native static string GetLumpFullPath(int lump); } enum EmptyTokenType From a0ab9ba25c8e6b93520e55ade059de4afafb850e Mon Sep 17 00:00:00 2001 From: DyNaM1Kk <42520902+DyNaM1Kk@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:01:20 +0400 Subject: [PATCH 095/384] Added autoSwitch parameter to A_ReFire --- wadsrc/static/zscript/actors/inventory/stateprovider.zs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wadsrc/static/zscript/actors/inventory/stateprovider.zs b/wadsrc/static/zscript/actors/inventory/stateprovider.zs index e02287258..bba4adb3b 100644 --- a/wadsrc/static/zscript/actors/inventory/stateprovider.zs +++ b/wadsrc/static/zscript/actors/inventory/stateprovider.zs @@ -413,7 +413,7 @@ class StateProvider : Inventory // //--------------------------------------------------------------------------- - action void A_ReFire(statelabel flash = null) + action void A_ReFire(statelabel flash = null, bool autoSwitch = true) { let player = player; bool pending; @@ -438,7 +438,7 @@ class StateProvider : Inventory else { player.refire = 0; - player.ReadyWeapon.CheckAmmo (player.ReadyWeapon.bAltFire? Weapon.AltFire : Weapon.PrimaryFire, true); + player.ReadyWeapon.CheckAmmo (player.ReadyWeapon.bAltFire? Weapon.AltFire : Weapon.PrimaryFire, autoSwitch); } } From 1f1c27188368a93fc1b7c0476bb76a02511973f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 1 Apr 2025 16:04:12 -0300 Subject: [PATCH 096/384] VMCallScript -> VMCallSingle --- src/common/scripting/vm/vm.h | 201 +++-------------------------------- 1 file changed, 13 insertions(+), 188 deletions(-) diff --git a/src/common/scripting/vm/vm.h b/src/common/scripting/vm/vm.h index dba1a36de..ff1fb32ff 100644 --- a/src/common/scripting/vm/vm.h +++ b/src/common/scripting/vm/vm.h @@ -560,209 +560,34 @@ template<> void VMCheckReturn(VMFunction* func); template<> void VMCheckReturn(VMFunction* func); template<> void VMCheckReturn(VMFunction* func); -template void VMValidateSignature(VMFunction* func) +template +void VMValidateSignatureSingle(VMFunction* func, std::index_sequence) { - VMCheckParamCount(func, 0); + VMCheckParamCount(func, sizeof...(Args)); VMCheckReturn(func); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 1); - VMCheckReturn(func); - VMCheckParam(func, 0); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 2); - VMCheckReturn(func); - VMCheckParam(func, 0); - VMCheckParam(func, 1); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 3); - VMCheckReturn(func); - VMCheckParam(func, 0); - VMCheckParam(func, 1); - VMCheckParam(func, 2); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 4); - VMCheckReturn(func); - VMCheckParam(func, 0); - VMCheckParam(func, 1); - VMCheckParam(func, 2); - VMCheckParam(func, 3); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 5); - VMCheckReturn(func); - VMCheckParam(func, 0); - VMCheckParam(func, 1); - VMCheckParam(func, 2); - VMCheckParam(func, 3); - VMCheckParam(func, 4); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 6); - VMCheckReturn(func); - VMCheckParam(func, 0); - VMCheckParam(func, 1); - VMCheckParam(func, 2); - VMCheckParam(func, 3); - VMCheckParam(func, 4); - VMCheckParam(func, 5); -} - -template void VMValidateSignature(VMFunction* func) -{ - VMCheckParamCount(func, 7); - VMCheckReturn(func); - VMCheckParam(func, 0); - VMCheckParam(func, 1); - VMCheckParam(func, 2); - VMCheckParam(func, 3); - VMCheckParam(func, 4); - VMCheckParam(func, 5); - VMCheckParam(func, 6); + (VMCheckParam(func, I)...); } void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMReturn* results, int numresults); -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1) +template +typename VMReturnTypeTrait::type VMCallSingle(VMFunction* func, Args... args) { - VMValidateSignature(func); - VMValue params[] = { p1 }; + VMValidateSignature(func); + VMValue params[] = { args... }; RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 1, &results, 1); + VMCallCheckResult(func, params, sizeof...(Args), &results, 1); return resultval; } -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2 }; - RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 2, &results, 1); - return resultval; -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3 }; - RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 3, &results, 1); - return resultval; -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4 }; - RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 4, &results, 1); - return resultval; -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4, p5 }; - RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 5, &results, 1); - return resultval; -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4, p5, p6 }; - RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 6, &results, 1); - return resultval; -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4, p5, p6, p7 }; - RetVal resultval; VMReturn results(&resultval); - VMCallCheckResult(func, params, 7, &results, 1); - return resultval; -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1) +template +typename VMReturnTypeTrait::type VMCallSingle(VMFunction* func, Args... args) { VMValidateSignature(func); - VMValue params[1] = { p1 }; - VMCallCheckResult(func, params, 1, nullptr, 0); + VMValue params[] = { args... }; + VMCallCheckResult(func, params, sizeof...(Args), nullptr, 0); } -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2 }; - VMCallCheckResult(func, params, 2, nullptr, 0); -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3 }; - VMCallCheckResult(func, params, 3, nullptr, 0); -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4 }; - VMCallCheckResult(func, params, 4, nullptr, 0); -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4, p5 }; - VMCallCheckResult(func, params, 5, nullptr, 0); -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4, p5, p6 }; - VMCallCheckResult(func, params, 6, nullptr, 0); -} - -template -typename VMReturnTypeTrait::type VMCallScript(VMFunction* func, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7) -{ - VMValidateSignature(func); - VMValue params[] = { p1, p2, p3, p4, p5, p6, p7 }; - VMCallCheckResult(func, params, 7, nullptr, 0); -} // Use these to collect the parameters in a native function. // variable name at position

From 278bd0fb7d04f25174e87593c69dbeddf268249a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 1 Apr 2025 16:49:28 -0300 Subject: [PATCH 097/384] finish implementing VMCallSingle --- src/common/scripting/vm/vm.h | 43 ++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/common/scripting/vm/vm.h b/src/common/scripting/vm/vm.h index ff1fb32ff..727c8c362 100644 --- a/src/common/scripting/vm/vm.h +++ b/src/common/scripting/vm/vm.h @@ -559,13 +559,40 @@ template<> void VMCheckReturn(VMFunction* func); template<> void VMCheckReturn(VMFunction* func); template<> void VMCheckReturn(VMFunction* func); template<> void VMCheckReturn(VMFunction* func); +template<> void VMCheckReturn(VMFunction* func); + +template +struct vm_decay_pointer_object +{ // convert any pointer to a type derived from DObject into a pointer to DObject, and any other to a pointer to void + using decayed = typename std::conditional::element_type>, + typename std::pointer_traits::template rebind, + typename std::pointer_traits::template rebind>::type; +}; + +template +struct vm_decay_pointer_void +{ // convert any pointer to a type derived from DObject into a pointer to DObject, and any other to a pointer to void + using decayed = typename std::pointer_traits::template rebind; +}; + +template +struct vm_decay_none +{ + using decayed = T; +}; + +template +using vm_pointer_decay = typename std::conditional, vm_decay_pointer_object, vm_decay_none>::type::decayed; + +template +using vm_pointer_decay_void = typename std::conditional, vm_decay_pointer_void, vm_decay_none>::type::decayed; template void VMValidateSignatureSingle(VMFunction* func, std::index_sequence) { VMCheckParamCount(func, sizeof...(Args)); - VMCheckReturn(func); - (VMCheckParam(func, I)...); + VMCheckReturn>(func); + (VMCheckParam>(func, I), ...); } void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMReturn* results, int numresults); @@ -573,17 +600,19 @@ void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMRetur template typename VMReturnTypeTrait::type VMCallSingle(VMFunction* func, Args... args) { - VMValidateSignature(func); + VMValidateSignatureSingle(func, std::make_index_sequence{}); VMValue params[] = { args... }; - RetVal resultval; VMReturn results(&resultval); + + vm_pointer_decay_void resultval; // convert any pointer to void + VMReturn results(&resultval); VMCallCheckResult(func, params, sizeof...(Args), &results, 1); - return resultval; + return (RetVal)resultval; } template -typename VMReturnTypeTrait::type VMCallSingle(VMFunction* func, Args... args) +typename VMReturnTypeTrait::type VMCallVoid(VMFunction* func, Args... args) { - VMValidateSignature(func); + VMValidateSignatureSingle(func, std::make_index_sequence{}); VMValue params[] = { args... }; VMCallCheckResult(func, params, sizeof...(Args), nullptr, 0); } From d8651420ccfddf84e3822f027c6f3a81ce02dbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 1 Apr 2025 17:07:53 -0300 Subject: [PATCH 098/384] fix parameter/return checking --- src/common/scripting/vm/vm.h | 16 ++++++------- src/common/scripting/vm/vmframe.cpp | 36 ++++++++++++++--------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/common/scripting/vm/vm.h b/src/common/scripting/vm/vm.h index 727c8c362..6f6176f28 100644 --- a/src/common/scripting/vm/vm.h +++ b/src/common/scripting/vm/vm.h @@ -545,7 +545,7 @@ void VMCheckParamCount(VMFunction* func, int argcount) { return VMCheckParamCoun // The type can't be mapped to ZScript automatically: template void VMCheckParam(VMFunction* func, int index) = delete; -template void VMCheckReturn(VMFunction* func) = delete; +template void VMCheckReturn(VMFunction* func, int index) = delete; // Native types we support converting to/from: @@ -554,12 +554,12 @@ template<> void VMCheckParam(VMFunction* func, int index); template<> void VMCheckParam(VMFunction* func, int index); template<> void VMCheckParam(VMFunction* func, int index); -template<> void VMCheckReturn(VMFunction* func); -template<> void VMCheckReturn(VMFunction* func); -template<> void VMCheckReturn(VMFunction* func); -template<> void VMCheckReturn(VMFunction* func); -template<> void VMCheckReturn(VMFunction* func); -template<> void VMCheckReturn(VMFunction* func); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); template struct vm_decay_pointer_object @@ -591,7 +591,7 @@ template void VMValidateSignatureSingle(VMFunction* func, std::index_sequence) { VMCheckParamCount(func, sizeof...(Args)); - VMCheckReturn>(func); + VMCheckReturn>(func, 0); (VMCheckParam>(func, I), ...); } diff --git a/src/common/scripting/vm/vmframe.cpp b/src/common/scripting/vm/vmframe.cpp index e04c6f0d7..40b3dbef8 100644 --- a/src/common/scripting/vm/vmframe.cpp +++ b/src/common/scripting/vm/vmframe.cpp @@ -566,53 +566,53 @@ void VMCheckParamCount(VMFunction* func, int retcount, int argcount) template<> void VMCheckParam(VMFunction* func, int index) { if (!func->Proto->ArgumentTypes[index]->isIntCompatible()) - I_FatalError("%s argument %d is not an integer", func->PrintableName); + I_FatalError("%s argument %d is not an integer", func->PrintableName, index); } template<> void VMCheckParam(VMFunction* func, int index) { if (func->Proto->ArgumentTypes[index] != TypeFloat64) - I_FatalError("%s argument %d is not a double", func->PrintableName); + I_FatalError("%s argument %d is not a double", func->PrintableName, index); } template<> void VMCheckParam(VMFunction* func, int index) { if (func->Proto->ArgumentTypes[index] != TypeString) - I_FatalError("%s argument %d is not a string", func->PrintableName); + I_FatalError("%s argument %d is not a string", func->PrintableName, index); } template<> void VMCheckParam(VMFunction* func, int index) { - if (func->Proto->ArgumentTypes[index]->isObjectPointer()) - I_FatalError("%s argument %d is not an object", func->PrintableName); + if (!func->Proto->ArgumentTypes[index]->isObjectPointer()) + I_FatalError("%s argument %d is not an object", func->PrintableName, index); } -template<> void VMCheckReturn(VMFunction* func) +template<> void VMCheckReturn(VMFunction* func, int index) { } -template<> void VMCheckReturn(VMFunction* func) +template<> void VMCheckReturn(VMFunction* func, int index) { - if (!func->Proto->ReturnTypes[0]->isIntCompatible()) - I_FatalError("%s return value %d is not an integer", func->PrintableName); + if (!func->Proto->ReturnTypes[index]->isIntCompatible()) + I_FatalError("%s return value %d is not an integer", func->PrintableName, index); } -template<> void VMCheckReturn(VMFunction* func) +template<> void VMCheckReturn(VMFunction* func, int index) { - if (func->Proto->ReturnTypes[0] != TypeFloat64) - I_FatalError("%s return value %d is not a double", func->PrintableName); + if (func->Proto->ReturnTypes[index] != TypeFloat64) + I_FatalError("%s return value %d is not a double", func->PrintableName, index); } -template<> void VMCheckReturn(VMFunction* func) +template<> void VMCheckReturn(VMFunction* func, int index) { - if (func->Proto->ReturnTypes[0] != TypeString) - I_FatalError("%s return value %d is not a string", func->PrintableName); + if (func->Proto->ReturnTypes[index] != TypeString) + I_FatalError("%s return value %d is not a string", func->PrintableName, index); } -template<> void VMCheckReturn(VMFunction* func) +template<> void VMCheckReturn(VMFunction* func, int index) { - if (func->Proto->ReturnTypes[0]->isObjectPointer()) - I_FatalError("%s return value %d is not an object", func->PrintableName); + if (!func->Proto->ReturnTypes[index]->isObjectPointer()) + I_FatalError("%s return value %d is not an object", func->PrintableName, index); } void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMReturn* results, int numresults) From 6cdcad425a50dbf789287154b5bd83e9d92be8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 1 Apr 2025 17:10:18 -0300 Subject: [PATCH 099/384] convert g_game.cpp VMCalls to use VMCallSingle/VMCallVoid (as an example) --- src/g_game.cpp | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/g_game.cpp b/src/g_game.cpp index 30274dd93..6206b4c3d 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -317,9 +317,7 @@ CCMD (slot) // Needs to be redone IFVIRTUALPTRNAME(mo, NAME_PlayerPawn, PickWeapon) { - VMValue param[] = { mo, slot, !(dmflags2 & DF2_DONTCHECKAMMO) }; - VMReturn ret((void**)&SendItemUse); - VMCall(func, param, 3, &ret, 1); + SendItemUse = VMCallSingle(func, mo, slot, (int)!(dmflags2 & DF2_DONTCHECKAMMO)); } } @@ -375,9 +373,7 @@ CCMD (weapnext) // Needs to be redone IFVIRTUALPTRNAME(mo, NAME_PlayerPawn, PickNextWeapon) { - VMValue param[] = { mo }; - VMReturn ret((void**)&SendItemUse); - VMCall(func, param, 1, &ret, 1); + SendItemUse = VMCallSingle(func, mo); } } @@ -402,9 +398,7 @@ CCMD (weapprev) // Needs to be redone IFVIRTUALPTRNAME(mo, NAME_PlayerPawn, PickPrevWeapon) { - VMValue param[] = { mo }; - VMReturn ret((void**)&SendItemUse); - VMCall(func, param, 1, &ret, 1); + SendItemUse = VMCallSingle(func, mo); } } @@ -443,8 +437,7 @@ CCMD (invnext) { IFVM(PlayerPawn, InvNext) { - VMValue param = players[consoleplayer].mo; - VMCall(func, ¶m, 1, nullptr, 0); + VMCallVoid(func, players[consoleplayer].mo); } } } @@ -455,8 +448,7 @@ CCMD(invprev) { IFVM(PlayerPawn, InvPrev) { - VMValue param = players[consoleplayer].mo; - VMCall(func, ¶m, 1, nullptr, 0); + VMCallVoid(func, players[consoleplayer].mo); } } } @@ -531,10 +523,7 @@ CCMD (useflechette) if (players[consoleplayer].mo == nullptr) return; IFVIRTUALPTRNAME(players[consoleplayer].mo, NAME_PlayerPawn, GetFlechetteItem) { - VMValue params[] = { players[consoleplayer].mo }; - AActor *cls; - VMReturn ret((void**)&cls); - VMCall(func, params, 1, &ret, 1); + AActor * cls = VMCallSingle(func, players[consoleplayer].mo); if (cls != nullptr) SendItemUse = cls; } @@ -1300,8 +1289,7 @@ void G_PlayerFinishLevel (int player, EFinishLevelType mode, int flags) { IFVM(PlayerPawn, PlayerFinishLevel) { - VMValue params[] = { players[player].mo, mode, flags }; - VMCall(func, params, 3, nullptr, 0); + VMCallVoid(func, players[player].mo, (int)mode, flags); } } @@ -1374,8 +1362,7 @@ void FLevelLocals::PlayerReborn (int player) IFVIRTUALPTRNAME(actor, NAME_PlayerPawn, GiveDefaultInventory) { - VMValue params[1] = { actor }; - VMCall(func, params, 1, nullptr, 0); + VMCallVoid(func, actor); } p->ReadyWeapon = p->PendingWeapon; } From 8019b56823f003028b09a93b7ea907f20874b58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 1 Apr 2025 17:11:59 -0300 Subject: [PATCH 100/384] fix comment --- src/common/scripting/vm/vm.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/scripting/vm/vm.h b/src/common/scripting/vm/vm.h index 6f6176f28..d4a6b1fa9 100644 --- a/src/common/scripting/vm/vm.h +++ b/src/common/scripting/vm/vm.h @@ -571,7 +571,7 @@ struct vm_decay_pointer_object template struct vm_decay_pointer_void -{ // convert any pointer to a type derived from DObject into a pointer to DObject, and any other to a pointer to void +{ // convert any pointer to a pointer to void using decayed = typename std::pointer_traits::template rebind; }; From 6481f8cce909bc801f7544d67f2bf5f96cd01181 Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Thu, 3 Apr 2025 07:51:03 +0200 Subject: [PATCH 101/384] rewrote XY and XYZ accessors for vectors to be read-only and not use type punning. --- src/common/utility/vectors.h | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/common/utility/vectors.h b/src/common/utility/vectors.h index 47eb59793..299a08985 100644 --- a/src/common/utility/vectors.h +++ b/src/common/utility/vectors.h @@ -539,15 +539,9 @@ struct TVector3 return *this; } - // returns the XY fields as a 2D-vector. - constexpr const Vector2& XY() const + constexpr Vector2 XY() const { - return *reinterpret_cast(this); - } - - constexpr Vector2& XY() - { - return *reinterpret_cast(this); + return Vector2(X, Y); } // Add a 3D vector and a 2D vector. @@ -785,28 +779,16 @@ struct TVector4 } // returns the XY fields as a 2D-vector. - constexpr const Vector2& XY() const + constexpr Vector2 XY() const { - return *reinterpret_cast(this); + return Vector2(X, Y); } - constexpr Vector2& XY() + constexpr Vector3 XYZ() const { - return *reinterpret_cast(this); + return Vector3(X, Y, Z); } - // returns the XY fields as a 2D-vector. - constexpr const Vector3& XYZ() const - { - return *reinterpret_cast(this); - } - - constexpr Vector3& XYZ() - { - return *reinterpret_cast(this); - } - - // Test for approximate equality bool ApproximatelyEquals(const TVector4 &other) const { @@ -1789,7 +1771,9 @@ struct TRotator template inline TVector3::TVector3 (const TRotator &rot) { - XY() = rot.Pitch.Cos() * rot.Yaw.ToVector(); + auto XY = rot.Pitch.Cos() * rot.Yaw.ToVector(); + X = XY.X; + Y = XY.Y; Z = rot.Pitch.Sin(); } From a02892389d55211757b18bd269d093ad203403b7 Mon Sep 17 00:00:00 2001 From: Peppersawce <157759066+Peppersawce@users.noreply.github.com> Date: Sat, 5 Apr 2025 14:18:33 +0200 Subject: [PATCH 102/384] Haiku support patch --- CMakeLists.txt | 2 +- libraries/ZVulkan/CMakeLists.txt | 6 +++++- libraries/ZWidget/CMakeLists.txt | 6 +++++- src/common/engine/i_specialpaths.h | 2 +- src/common/rendering/gl_load/gl_load.c | 2 +- src/version.h | 2 ++ 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ac374e55..92e7d6702 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -309,7 +309,7 @@ else() # If we're compiling with a custom GCC on the Mac (which we know since g++-4.2 doesn't support C++11) statically link libgcc. set( ALL_C_FLAGS "-static-libgcc" ) endif() - elseif( NOT MINGW ) + elseif( NOT MINGW AND NOT HAIKU ) # Generic GCC/Clang requires position independent executable to be enabled explicitly set( ALL_C_FLAGS "${ALL_C_FLAGS} -fPIE" ) set( CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie" ) diff --git a/libraries/ZVulkan/CMakeLists.txt b/libraries/ZVulkan/CMakeLists.txt index 3b47b7b68..4cd713323 100644 --- a/libraries/ZVulkan/CMakeLists.txt +++ b/libraries/ZVulkan/CMakeLists.txt @@ -188,7 +188,11 @@ if(WIN32) add_definitions(-DUNICODE -D_UNICODE) else() set(ZVULKAN_SOURCES ${ZVULKAN_SOURCES} ${ZVULKAN_UNIX_SOURCES}) - set(ZVULKAN_LIBS ${CMAKE_DL_LIBS} -ldl) + if(NOT HAIKU) + set(ZVULKAN_LIBS ${CMAKE_DL_LIBS} -ldl) + else() + set(ZVULKAN_LIBS ${CMAKE_DL_LIBS}) + endif() add_definitions(-DUNIX -D_UNIX) add_link_options(-pthread) endif() diff --git a/libraries/ZWidget/CMakeLists.txt b/libraries/ZWidget/CMakeLists.txt index 68c048e22..329caf96e 100644 --- a/libraries/ZWidget/CMakeLists.txt +++ b/libraries/ZWidget/CMakeLists.txt @@ -130,7 +130,11 @@ elseif(APPLE) add_link_options(-pthread) else() set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES}) - set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) + if(NOT HAIKU) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) + else() + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS}) + endif() add_definitions(-DUNIX -D_UNIX) add_link_options(-pthread) endif() diff --git a/src/common/engine/i_specialpaths.h b/src/common/engine/i_specialpaths.h index 01710b672..fb75230f5 100644 --- a/src/common/engine/i_specialpaths.h +++ b/src/common/engine/i_specialpaths.h @@ -2,7 +2,7 @@ #include "zstring.h" -#ifdef __unix__ +#if defined(__unix__) || defined(__HAIKU__) FString GetUserFile (const char *path); #endif FString M_GetAppDataPath(bool create); diff --git a/src/common/rendering/gl_load/gl_load.c b/src/common/rendering/gl_load/gl_load.c index d5ba4e49f..bb2141e7c 100644 --- a/src/common/rendering/gl_load/gl_load.c +++ b/src/common/rendering/gl_load/gl_load.c @@ -134,7 +134,7 @@ static PROC WinGetProcAddress(const char *name) #if defined(__APPLE__) #define IntGetProcAddress(name) AppleGLGetProcAddress(name) #else - #if defined(__sgi) || defined(__sun) || defined(__unix__) + #if defined(__sgi) || defined(__sun) || defined(__unix__) || defined(__HAIKU__) void* SDL_GL_GetProcAddress(const char* proc); #define IntGetProcAddress(name) SDL_GL_GetProcAddress((const char*)name) //#define IntGetProcAddress(name) PosixGetProcAddress((const GLubyte*)name) diff --git a/src/version.h b/src/version.h index 91f3319b5..ddfd3a346 100644 --- a/src/version.h +++ b/src/version.h @@ -105,6 +105,8 @@ const char *GetVersionString(); #if defined(__APPLE__) || defined(_WIN32) #define GAME_DIR GAMENAME +#elif defined(__HAIKU__) +#define GAME_DIR "config/settings/" GAMENAME #else #define GAME_DIR ".config/" GAMENAMELOWERCASE #endif From 20c234b90f20b86f042d17e876d85398c4a1f239 Mon Sep 17 00:00:00 2001 From: Gene Date: Sat, 5 Apr 2025 22:01:31 -0700 Subject: [PATCH 103/384] Scrap dlight lighthead link lists --- src/g_level.cpp | 3 + src/g_levellocals.h | 10 + src/gamedata/r_defs.h | 1 - src/playsim/a_dynlight.cpp | 213 ++++++++---------- src/playsim/a_dynlight.h | 7 + src/r_data/r_sections.cpp | 1 - src/r_data/r_sections.h | 1 - src/rendering/hwrenderer/scene/hw_drawinfo.h | 2 +- .../hwrenderer/scene/hw_drawstructs.h | 2 +- src/rendering/hwrenderer/scene/hw_flats.cpp | 50 ++-- .../hwrenderer/scene/hw_renderhacks.cpp | 31 +-- .../hwrenderer/scene/hw_spritelight.cpp | 169 +++++++------- src/rendering/hwrenderer/scene/hw_walls.cpp | 172 +++++++++----- src/rendering/swrenderer/line/r_walldraw.cpp | 46 +++- .../swrenderer/plane/r_visibleplane.cpp | 43 ++-- .../swrenderer/plane/r_visibleplane.h | 2 +- .../swrenderer/scene/r_opaque_pass.cpp | 8 +- src/rendering/swrenderer/things/r_sprite.cpp | 66 +++--- 18 files changed, 475 insertions(+), 352 deletions(-) diff --git a/src/g_level.cpp b/src/g_level.cpp index 38b6e5feb..f7c448407 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -623,6 +623,9 @@ void G_InitNew (const char *mapname, bool bTitleLevel) primaryLevel->totaltime = 0; primaryLevel->spawnindex = 0; + primaryLevel->lightlists.wall_dlist.clear(); + primaryLevel->lightlists.flat_dlist.clear(); + if (!multiplayer || !deathmatch) { InitPlayerClasses (); diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 2bb55f332..bbc313f49 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -35,6 +35,8 @@ #pragma once +#include + #include "doomdata.h" #include "g_level.h" #include "r_defs.h" @@ -58,6 +60,12 @@ #include "doom_levelmesh.h" #include "p_visualthinker.h" +struct FGlobalDLightLists +{ + std::unordered_map> flat_dlist; + std::unordered_map> wall_dlist; +}; + //============================================================================ // // This is used to mark processed portals for some collection functions. @@ -745,6 +753,8 @@ public: bool notexturefill; int ImpactDecalCount; + FGlobalDLightLists lightlists; + FDynamicLight *lights; DVisualThinker* VisualThinkerHead = nullptr; diff --git a/src/gamedata/r_defs.h b/src/gamedata/r_defs.h index 663f3aa38..93531b555 100644 --- a/src/gamedata/r_defs.h +++ b/src/gamedata/r_defs.h @@ -1268,7 +1268,6 @@ struct side_t int16_t TierLights[3]; // per-tier light levels uint16_t Flags; int UDMFIndex; // needed to access custom UDMF fields which are stored in loading order. - FLightNode * lighthead; // all dynamic lights that may affect this wall LightmapSurface* lightmap; seg_t **segs; // all segs belonging to this sidedef in ascending order. Used for precise rendering int numsegs; diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 79d8fe768..2548614a9 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -95,6 +95,7 @@ static FDynamicLight *GetLight(FLevelLocals *Level) ret->mShadowmapIndex = 1024; ret->Level = Level; ret->Pos.X = -10000000; // not a valid coordinate. + if (!ret->touchlists) ret->touchlists = new FDynamicLightTouchLists; return ret; } @@ -249,6 +250,7 @@ void FDynamicLight::Tick() if (!target) { // How did we get here? :? + UnlinkLight(); ReleaseLight(); return; } @@ -416,83 +418,6 @@ void FDynamicLight::UpdateLocation() } } -//============================================================================= -// -// These have been copied from the secnode code and modified for the light links -// -// P_AddSecnode() searches the current list to see if this sector is -// already there. If not, it adds a sector node at the head of the list of -// sectors this object appears in. This is called when creating a list of -// nodes that will get linked in later. Returns a pointer to the new node. -// -//============================================================================= - -FLightNode * AddLightNode(FLightNode ** thread, void * linkto, FDynamicLight * light, FLightNode *& nextnode) -{ - FLightNode * node; - - node = nextnode; - while (node) - { - if (node->targ==linkto) // Already have a node for this sector? - { - node->lightsource = light; // Yes. Setting m_thing says 'keep it'. - return(nextnode); - } - node = node->nextTarget; - } - - // Couldn't find an existing node for this sector. Add one at the head - // of the list. - - node = new FLightNode; - - node->targ = linkto; - node->lightsource = light; - - node->prevTarget = &nextnode; - node->nextTarget = nextnode; - - if (nextnode) nextnode->prevTarget = &node->nextTarget; - - // Add new node at head of sector thread starting at s->touching_thinglist - - node->prevLight = thread; - node->nextLight = *thread; - if (node->nextLight) node->nextLight->prevLight=&node->nextLight; - *thread = node; - return(node); -} - - -//============================================================================= -// -// P_DelSecnode() deletes a sector node from the list of -// sectors this object appears in. Returns a pointer to the next node -// on the linked list, or nullptr. -// -//============================================================================= - -static FLightNode * DeleteLightNode(FLightNode * node) -{ - FLightNode * tn; // next node on thing thread - - if (node) - { - - *node->prevTarget = node->nextTarget; - if (node->nextTarget) node->nextTarget->prevTarget=node->prevTarget; - - *node->prevLight = node->nextLight; - if (node->nextLight) node->nextLight->prevLight=node->prevLight; - - // Return this node to the freelist - tn=node->nextTarget; - delete node; - return(tn); - } - return(nullptr); -} @@ -550,7 +475,38 @@ void FDynamicLight::CollectWithinRadius(const DVector3 &opos, FSection *section, auto pos = collected_ss[i].pos; section = collected_ss[i].sect; - touching_sector = AddLightNode(§ion->lighthead, section, this, touching_sector); + auto updateFlatTList = [&](FSection *sec) + { + touchlists->flat_tlist.try_emplace(sec, sec); + }; + auto updateWallTList = [&](side_t *sidedef) + { + touchlists->wall_tlist.try_emplace(sidedef, sidedef); + }; + + FLightNode * node = new FLightNode; + node->targ = section; + node->lightsource = this; + + auto flatLightList = Level->lightlists.flat_dlist.find(section); + if (flatLightList != Level->lightlists.flat_dlist.end()) + { + auto ret = flatLightList->second.try_emplace(this, node); + if (ret.second) + { + updateFlatTList(section); + } + else + { + delete node; + } + } + else + { + std::unordered_map u = { {this, node} }; + Level->lightlists.flat_dlist.try_emplace(section, u); + updateFlatTList(section); + } auto processSide = [&](side_t *sidedef, const vertex_t *v1, const vertex_t *v2) @@ -562,7 +518,30 @@ void FDynamicLight::CollectWithinRadius(const DVector3 &opos, FSection *section, if ((pos.Y - v1->fY()) * (v2->fX() - v1->fX()) + (v1->fX() - pos.X) * (v2->fY() - v1->fY()) <= 0) { linedef->validcount = ::validcount; - touching_sides = AddLightNode(&sidedef->lighthead, sidedef, this, touching_sides); + + FLightNode * node = new FLightNode; + node->targ = sidedef; + node->lightsource = this; + + auto wallLightList = Level->lightlists.wall_dlist.find(sidedef); + if (wallLightList != Level->lightlists.wall_dlist.end()) + { + auto ret = wallLightList->second.try_emplace(this, node); + if (ret.second) + { + updateWallTList(sidedef); + } + else + { + delete node; + } + } + else + { + std::unordered_map u = { {this, node} }; + Level->lightlists.wall_dlist.try_emplace(sidedef, u); + updateWallTList(sidedef); + } } else if (linedef->sidedef[0] == sidedef && linedef->sidedef[1] == nullptr) { @@ -664,22 +643,6 @@ void FDynamicLight::CollectWithinRadius(const DVector3 &opos, FSection *section, void FDynamicLight::LinkLight() { - // mark the old light nodes - FLightNode * node; - - node = touching_sides; - while (node) - { - node->lightsource = nullptr; - node = node->nextTarget; - } - node = touching_sector; - while (node) - { - node->lightsource = nullptr; - node = node->nextTarget; - } - if (radius>0) { // passing in radius*radius allows us to do a distance check without any calls to sqrt @@ -690,31 +653,6 @@ void FDynamicLight::LinkLight() CollectWithinRadius(Pos, sect, float(radius*radius)); } - - // Now delete any nodes that won't be used. These are the ones where - // m_thing is still nullptr. - - node = touching_sides; - while (node) - { - if (node->lightsource == nullptr) - { - node = DeleteLightNode(node); - } - else - node = node->nextTarget; - } - - node = touching_sector; - while (node) - { - if (node->lightsource == nullptr) - { - node = DeleteLightNode(node); - } - else - node = node->nextTarget; - } } @@ -725,8 +663,39 @@ void FDynamicLight::LinkLight() //========================================================================== void FDynamicLight::UnlinkLight () { - while (touching_sides) touching_sides = DeleteLightNode(touching_sides); - while (touching_sector) touching_sector = DeleteLightNode(touching_sector); + for (auto iter = touchlists->wall_tlist.begin(); iter != touchlists->wall_tlist.end(); iter++) + { + auto sidedef = iter->second; + if (!sidedef) continue; + + auto wallLightList = Level->lightlists.wall_dlist.find(sidedef); + if (wallLightList != Level->lightlists.wall_dlist.end()) + { + auto light = wallLightList->second.find(this); + if (light != wallLightList->second.end()) + { + delete light->second; + wallLightList->second.erase(light); + } + } + } + for (auto iter = touchlists->flat_tlist.begin(); iter != touchlists->flat_tlist.end(); iter++) + { + auto sec = iter->second; + if (!sec) continue; + + auto flatLightList = Level->lightlists.flat_dlist.find(sec); + if (flatLightList != Level->lightlists.flat_dlist.end()) + { + auto light = flatLightList->second.find(this); + if (light != flatLightList->second.end()) + { + delete light->second; + flatLightList->second.erase(light); + } + } + } + delete touchlists; shadowmapped = false; } diff --git a/src/playsim/a_dynlight.h b/src/playsim/a_dynlight.h index 0aee31790..9127cfd21 100644 --- a/src/playsim/a_dynlight.h +++ b/src/playsim/a_dynlight.h @@ -195,6 +195,12 @@ struct FLightNode }; }; +struct FDynamicLightTouchLists +{ + std::unordered_map flat_tlist; + std::unordered_map wall_tlist; +}; + struct FDynamicLight { friend class FLightDefaults; @@ -286,6 +292,7 @@ public: bool swapped; bool explicitpitch; + FDynamicLightTouchLists *touchlists; }; diff --git a/src/r_data/r_sections.cpp b/src/r_data/r_sections.cpp index c41492482..ebed5e3ac 100644 --- a/src/r_data/r_sections.cpp +++ b/src/r_data/r_sections.cpp @@ -712,7 +712,6 @@ public: dest.sector = &Level->sectors[group.groupedSections[0].section->sectorindex]; dest.mapsection = (short)group.groupedSections[0].section->mapsection; dest.hacked = false; - dest.lighthead = nullptr; dest.validcount = 0; dest.segments.Set(&output.allLines[numsegments], group.segments.Size()); dest.sides.Set(&output.allSides[numsides], group.sideMap.CountUsed()); diff --git a/src/r_data/r_sections.h b/src/r_data/r_sections.h index fd13a80e7..d844d2d18 100644 --- a/src/r_data/r_sections.h +++ b/src/r_data/r_sections.h @@ -113,7 +113,6 @@ struct FSection TArrayView sides; // contains all sidedefs, including the internal ones that do not make up the outer shape. TArrayView subsectors; // contains all subsectors making up this section sector_t *sector; - FLightNode *lighthead; // Light nodes (blended and additive) BoundingRect bounds; int vertexindex; // This is relative to the start of the entire sector's vertex plane data because it needs to be used with different sources. int vertexcount; diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.h b/src/rendering/hwrenderer/scene/hw_drawinfo.h index e4cbeb98e..f134eddd1 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.h +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.h @@ -290,7 +290,7 @@ public: void AddOtherFloorPlane(int sector, gl_subsectorrendernode * node); void AddOtherCeilingPlane(int sector, gl_subsectorrendernode * node); - void GetDynSpriteLight(AActor *self, float x, float y, float z, FLightNode *node, int portalgroup, float *out); + void GetDynSpriteLight(AActor *self, float x, float y, float z, FSection *sec, int portalgroup, float *out); void GetDynSpriteLight(AActor *thing, particle_t *particle, float *out); void PreparePlayerSprites(sector_t * viewsector, area_t in_area); diff --git a/src/rendering/hwrenderer/scene/hw_drawstructs.h b/src/rendering/hwrenderer/scene/hw_drawstructs.h index 903806c67..77d72f7db 100644 --- a/src/rendering/hwrenderer/scene/hw_drawstructs.h +++ b/src/rendering/hwrenderer/scene/hw_drawstructs.h @@ -333,7 +333,7 @@ public: int dynlightindex; void CreateSkyboxVertices(FFlatVertex *buffer); - void SetupLights(HWDrawInfo *di, FLightNode *head, FDynLightData &lightdata, int portalgroup); + void SetupLights(HWDrawInfo *di, FDynLightData &lightdata, int portalgroup); void PutFlat(HWDrawInfo *di, bool fog = false); void Process(HWDrawInfo *di, sector_t * model, int whichplane, bool notexture); diff --git a/src/rendering/hwrenderer/scene/hw_flats.cpp b/src/rendering/hwrenderer/scene/hw_flats.cpp index 317ba9399..3839accad 100644 --- a/src/rendering/hwrenderer/scene/hw_flats.cpp +++ b/src/rendering/hwrenderer/scene/hw_flats.cpp @@ -148,7 +148,7 @@ void HWFlat::CreateSkyboxVertices(FFlatVertex *vert) // //========================================================================== -void HWFlat::SetupLights(HWDrawInfo *di, FLightNode * node, FDynLightData &lightdata, int portalgroup) +void HWFlat::SetupLights(HWDrawInfo *di, FDynLightData &lightdata, int portalgroup) { Plane p; @@ -158,29 +158,35 @@ void HWFlat::SetupLights(HWDrawInfo *di, FLightNode * node, FDynLightData &light dynlightindex = -1; return; // no lights on additively blended surfaces. } - while (node) + + auto flatLightList = di->Level->lightlists.flat_dlist.find(section); + + if (flatLightList != di->Level->lightlists.flat_dlist.end()) { - FDynamicLight * light = node->lightsource; - - if (!light->IsActive() || light->DontLightMap()) + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) { - node = node->nextLight; - continue; - } - iter_dlightf++; + auto node = nodeIterator->second; + if (!node) continue; + + FDynamicLight * light = node->lightsource; - // we must do the side check here because gl_GetLight needs the correct plane orientation - // which we don't have for Legacy-style 3D-floors - double planeh = plane.plane.ZatPoint(light->Pos); - if ((planehZ() && ceiling) || (planeh>light->Z() && !ceiling)) - { - node = node->nextLight; - continue; - } + if (!light->IsActive() || light->DontLightMap()) + { + continue; + } + iter_dlightf++; - p.Set(plane.plane.Normal(), plane.plane.fD()); - draw_dlightf += GetLight(lightdata, portalgroup, p, light, false); - node = node->nextLight; + // we must do the side check here because gl_GetLight needs the correct plane orientation + // which we don't have for Legacy-style 3D-floors + double planeh = plane.plane.ZatPoint(light->Pos); + if ((planehZ() && ceiling) || (planeh>light->Z() && !ceiling)) + { + continue; + } + + p.Set(plane.plane.Normal(), plane.plane.fD()); + draw_dlightf += GetLight(lightdata, portalgroup, p, light, false); + } } dynlightindex = screen->mLights->UploadLights(lightdata); @@ -196,7 +202,7 @@ void HWFlat::DrawSubsectors(HWDrawInfo *di, FRenderState &state) { if (di->Level->HasDynamicLights && screen->BuffersArePersistent() && !di->isFullbrightScene()) { - SetupLights(di, section->lighthead, lightdata, sector->PortalGroup); + SetupLights(di, lightdata, sector->PortalGroup); } state.SetLightIndex(dynlightindex); @@ -399,7 +405,7 @@ inline void HWFlat::PutFlat(HWDrawInfo *di, bool fog) { if (di->Level->HasDynamicLights && texture != nullptr && !di->isFullbrightScene() && !(hacktype & (SSRF_PLANEHACK|SSRF_FLOODHACK)) ) { - SetupLights(di, section->lighthead, lightdata, sector->PortalGroup); + SetupLights(di, lightdata, sector->PortalGroup); } } di->AddFlat(this, fog); diff --git a/src/rendering/hwrenderer/scene/hw_renderhacks.cpp b/src/rendering/hwrenderer/scene/hw_renderhacks.cpp index 6cb221752..c4c65fd18 100644 --- a/src/rendering/hwrenderer/scene/hw_renderhacks.cpp +++ b/src/rendering/hwrenderer/scene/hw_renderhacks.cpp @@ -115,23 +115,28 @@ int HWDrawInfo::SetupLightsForOtherPlane(subsector_t * sub, FDynLightData &light { Plane p; - FLightNode * node = sub->section->lighthead; - lightdata.Clear(); - while (node) + + auto flatLightList = Level->lightlists.flat_dlist.find(sub->section); + + if (flatLightList != Level->lightlists.flat_dlist.end()) { - FDynamicLight * light = node->lightsource; - - if (!light->IsActive()) + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) { - node = node->nextLight; - continue; - } - iter_dlightf++; + auto node = nodeIterator->second; + if (!node) continue; - p.Set(plane->Normal(), plane->fD()); - draw_dlightf += GetLight(lightdata, sub->sector->PortalGroup, p, light, true); - node = node->nextLight; + FDynamicLight * light = node->lightsource; + + if (!light->IsActive()) + { + continue; + } + iter_dlightf++; + + p.Set(plane->Normal(), plane->fD()); + draw_dlightf += GetLight(lightdata, sub->sector->PortalGroup, p, light, true); + } } return screen->mLights->UploadLights(lightdata); diff --git a/src/rendering/hwrenderer/scene/hw_spritelight.cpp b/src/rendering/hwrenderer/scene/hw_spritelight.cpp index ca67296d7..a7a92f033 100644 --- a/src/rendering/hwrenderer/scene/hw_spritelight.cpp +++ b/src/rendering/hwrenderer/scene/hw_spritelight.cpp @@ -107,7 +107,7 @@ LightProbe* FindLightProbe(FLevelLocals* level, float x, float y, float z) // //========================================================================== -void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FLightNode *node, int portalgroup, float *out) +void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FSection *sec, int portalgroup, float *out) { FDynamicLight *light; float frac, lr, lg, lb; @@ -124,83 +124,90 @@ void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FLig } // Go through both light lists - while (node) + auto flatLightList = Level->lightlists.flat_dlist.find(sec); + + if (flatLightList != Level->lightlists.flat_dlist.end()) { - light=node->lightsource; - if (light->ShouldLightActor(self)) + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) { - float dist; - FVector3 L; + auto node = nodeIterator->second; + if (!node) continue; - // This is a performance critical section of code where we cannot afford to let the compiler decide whether to inline the function or not. - // This will do the calculations explicitly rather than calling one of AActor's utility functions. - if (Level->Displacements.size > 0) + light=node->lightsource; + if (light->ShouldLightActor(self)) { - int fromgroup = light->Sector->PortalGroup; - int togroup = portalgroup; - if (fromgroup == togroup || fromgroup == 0 || togroup == 0) goto direct; + float dist; + FVector3 L; - DVector2 offset = Level->Displacements.getOffset(fromgroup, togroup); - L = FVector3(x - (float)(light->X() + offset.X), y - (float)(light->Y() + offset.Y), z - (float)light->Z()); - } - else - { - direct: - L = FVector3(x - (float)light->X(), y - (float)light->Y(), z - (float)light->Z()); - } - - dist = (float)L.LengthSquared(); - radius = light->GetRadius(); - - if (dist < radius * radius) - { - dist = sqrtf(dist); // only calculate the square root if we really need it. - - frac = 1.0f - (dist / radius); - - if (light->IsSpot()) + // This is a performance critical section of code where we cannot afford to let the compiler decide whether to inline the function or not. + // This will do the calculations explicitly rather than calling one of AActor's utility functions. + if (Level->Displacements.size > 0) { - L *= -1.0f / dist; - DAngle negPitch = -*light->pPitch; - DAngle Angle = light->target->Angles.Yaw; - double xyLen = negPitch.Cos(); - double spotDirX = -Angle.Cos() * xyLen; - double spotDirY = -Angle.Sin() * xyLen; - double spotDirZ = -negPitch.Sin(); - double cosDir = L.X * spotDirX + L.Y * spotDirY + L.Z * spotDirZ; - frac *= (float)smoothstep(light->pSpotOuterAngle->Cos(), light->pSpotInnerAngle->Cos(), cosDir); + int fromgroup = light->Sector->PortalGroup; + int togroup = portalgroup; + if (fromgroup == togroup || fromgroup == 0 || togroup == 0) goto direct; + + DVector2 offset = Level->Displacements.getOffset(fromgroup, togroup); + L = FVector3(x - (float)(light->X() + offset.X), y - (float)(light->Y() + offset.Y), z - (float)light->Z()); + } + else + { + direct: + L = FVector3(x - (float)light->X(), y - (float)light->Y(), z - (float)light->Z()); } - if (frac > 0 && (!light->shadowmapped || (light->GetRadius() > 0 && screen->mShadowMap.ShadowTest(light->Pos, { x, y, z })))) + dist = (float)L.LengthSquared(); + radius = light->GetRadius(); + + if (dist < radius * radius) { - lr = light->GetRed() / 255.0f; - lg = light->GetGreen() / 255.0f; - lb = light->GetBlue() / 255.0f; + dist = sqrtf(dist); // only calculate the square root if we really need it. - if (light->target) + frac = 1.0f - (dist / radius); + + if (light->IsSpot()) { - float alpha = (float)light->target->Alpha; - lr *= alpha; - lg *= alpha; - lb *= alpha; + L *= -1.0f / dist; + DAngle negPitch = -*light->pPitch; + DAngle Angle = light->target->Angles.Yaw; + double xyLen = negPitch.Cos(); + double spotDirX = -Angle.Cos() * xyLen; + double spotDirY = -Angle.Sin() * xyLen; + double spotDirZ = -negPitch.Sin(); + double cosDir = L.X * spotDirX + L.Y * spotDirY + L.Z * spotDirZ; + frac *= (float)smoothstep(light->pSpotOuterAngle->Cos(), light->pSpotInnerAngle->Cos(), cosDir); } - if (light->IsSubtractive()) + if (frac > 0 && (!light->shadowmapped || (light->GetRadius() > 0 && screen->mShadowMap.ShadowTest(light->Pos, { x, y, z })))) { - float bright = (float)FVector3(lr, lg, lb).Length(); - FVector3 lightColor(lr, lg, lb); - lr = (bright - lr) * -1; - lg = (bright - lg) * -1; - lb = (bright - lb) * -1; - } + lr = light->GetRed() / 255.0f; + lg = light->GetGreen() / 255.0f; + lb = light->GetBlue() / 255.0f; - out[0] += lr * frac; - out[1] += lg * frac; - out[2] += lb * frac; + if (light->target) + { + float alpha = (float)light->target->Alpha; + lr *= alpha; + lg *= alpha; + lb *= alpha; + } + + if (light->IsSubtractive()) + { + float bright = (float)FVector3(lr, lg, lb).Length(); + FVector3 lightColor(lr, lg, lb); + lr = (bright - lr) * -1; + lg = (bright - lg) * -1; + lb = (bright - lb) * -1; + } + + out[0] += lr * frac; + out[1] += lg * frac; + out[2] += lb * frac; + } } } } - node = node->nextLight; } } @@ -208,11 +215,11 @@ void HWDrawInfo::GetDynSpriteLight(AActor *thing, particle_t *particle, float *o { if (thing != NULL) { - GetDynSpriteLight(thing, (float)thing->X(), (float)thing->Y(), (float)thing->Center(), thing->section->lighthead, thing->Sector->PortalGroup, out); + GetDynSpriteLight(thing, (float)thing->X(), (float)thing->Y(), (float)thing->Center(), thing->section, thing->Sector->PortalGroup, out); } else if (particle != NULL) { - GetDynSpriteLight(NULL, (float)particle->Pos.X, (float)particle->Pos.Y, (float)particle->Pos.Z, particle->subsector->section->lighthead, particle->subsector->sector->PortalGroup, out); + GetDynSpriteLight(NULL, (float)particle->Pos.X, (float)particle->Pos.Y, (float)particle->Pos.Z, particle->subsector->section, particle->subsector->sector->PortalGroup, out); } } @@ -241,29 +248,33 @@ void hw_GetDynModelLight(AActor *self, FDynLightData &modellightdata) { auto section = subsector->section; if (section->validcount == dl_validcount) return; // already done from a previous subsector. - FLightNode * node = section->lighthead; - while (node) // check all lights touching a subsector + auto flatLightList = self->Level->lightlists.flat_dlist.find(subsector->section); + if (flatLightList != self->Level->lightlists.flat_dlist.end()) { - FDynamicLight *light = node->lightsource; - if (light->ShouldLightActor(self)) - { - int group = subsector->sector->PortalGroup; - DVector3 pos = light->PosRelative(group); - float radius = (float)(light->GetRadius() + actorradius); - double dx = pos.X - x; - double dy = pos.Y - y; - double dz = pos.Z - z; - double distSquared = dx * dx + dy * dy + dz * dz; - if (distSquared < radius * radius) // Light and actor touches + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + { // check all lights touching a subsector + auto node = nodeIterator->second; + if (!node) continue; + FDynamicLight *light = node->lightsource; + if (light->ShouldLightActor(self)) { - if (std::find(addedLights.begin(), addedLights.end(), light) == addedLights.end()) // Check if we already added this light from a different subsector + int group = subsector->sector->PortalGroup; + DVector3 pos = light->PosRelative(group); + float radius = (float)(light->GetRadius() + actorradius); + double dx = pos.X - x; + double dy = pos.Y - y; + double dz = pos.Z - z; + double distSquared = dx * dx + dy * dy + dz * dz; + if (distSquared < radius * radius) // Light and actor touches { - AddLightToList(modellightdata, group, light, true); - addedLights.Push(light); + if (std::find(addedLights.begin(), addedLights.end(), light) == addedLights.end()) // Check if we already added this light from a different subsector + { + AddLightToList(modellightdata, group, light, true); + addedLights.Push(light); + } } } } - node = node->nextLight; } }); } diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index 527c02c0d..ea81f1fc7 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -424,76 +424,134 @@ void HWWall::SetupLights(HWDrawInfo*di, FDynLightData &lightdata) auto normal = glseg.Normal(); p.Set(normal, -normal.X * glseg.x1 - normal.Z * glseg.y1); - FLightNode *node; - if (seg->sidedef == NULL) - { - node = NULL; - } - else if (!(seg->sidedef->Flags & WALLF_POLYOBJ)) - { - node = seg->sidedef->lighthead; - } - else if (sub) + if ((seg->sidedef->Flags & WALLF_POLYOBJ) && sub) { // Polobject segs cannot be checked per sidedef so use the subsector instead. - node = sub->section->lighthead; - } - else node = NULL; + auto flatLightList = di->Level->lightlists.flat_dlist.find(sub->section); - // Iterate through all dynamic lights which touch this wall and render them - while (node) - { - if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) + if (flatLightList != di->Level->lightlists.flat_dlist.end()) { - iter_dlight++; - - DVector3 posrel = node->lightsource->PosRelative(seg->frontsector->PortalGroup); - float x = posrel.X; - float y = posrel.Y; - float z = posrel.Z; - float dist = fabsf(p.DistToPoint(x, z, y)); - float radius = node->lightsource->GetRadius(); - float scale = 1.0f / ((2.f * radius) - dist); - FVector3 fn, pos; - - if (radius > 0.f && dist < radius) + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) { - FVector3 nearPt, up, right; + auto node = nodeIterator->second; + if (!node) continue; - pos = { x, z, y }; - fn = p.Normal(); - - fn.GetRightUp(right, up); - - FVector3 tmpVec = fn * dist; - nearPt = pos + tmpVec; - - FVector3 t1; - int outcnt[4]={0,0,0,0}; - texcoord tcs[4]; - - // do a quick check whether the light touches this polygon - for(int i=0;i<4;i++) + if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) { - t1 = FVector3(&vtx[i*3]); - FVector3 nearToVert = t1 - nearPt; - tcs[i].u = ((nearToVert | right) * scale) + 0.5f; - tcs[i].v = ((nearToVert | up) * scale) + 0.5f; + iter_dlight++; - if (tcs[i].u<0) outcnt[0]++; - if (tcs[i].u>1) outcnt[1]++; - if (tcs[i].v<0) outcnt[2]++; - if (tcs[i].v>1) outcnt[3]++; + DVector3 posrel = node->lightsource->PosRelative(seg->frontsector->PortalGroup); + float x = posrel.X; + float y = posrel.Y; + float z = posrel.Z; + float dist = fabsf(p.DistToPoint(x, z, y)); + float radius = node->lightsource->GetRadius(); + float scale = 1.0f / ((2.f * radius) - dist); + FVector3 fn, pos; - } - if (outcnt[0]!=4 && outcnt[1]!=4 && outcnt[2]!=4 && outcnt[3]!=4) - { - draw_dlight += GetLight(lightdata, seg->frontsector->PortalGroup, p, node->lightsource, true); + if (radius > 0.f && dist < radius) + { + FVector3 nearPt, up, right; + + pos = { x, z, y }; + fn = p.Normal(); + + fn.GetRightUp(right, up); + + FVector3 tmpVec = fn * dist; + nearPt = pos + tmpVec; + + FVector3 t1; + int outcnt[4]={0,0,0,0}; + texcoord tcs[4]; + + // do a quick check whether the light touches this polygon + for(int i=0;i<4;i++) + { + t1 = FVector3(&vtx[i*3]); + FVector3 nearToVert = t1 - nearPt; + tcs[i].u = ((nearToVert | right) * scale) + 0.5f; + tcs[i].v = ((nearToVert | up) * scale) + 0.5f; + + if (tcs[i].u<0) outcnt[0]++; + if (tcs[i].u>1) outcnt[1]++; + if (tcs[i].v<0) outcnt[2]++; + if (tcs[i].v>1) outcnt[3]++; + + } + if (outcnt[0]!=4 && outcnt[1]!=4 && outcnt[2]!=4 && outcnt[3]!=4) + { + draw_dlight += GetLight(lightdata, seg->frontsector->PortalGroup, p, node->lightsource, true); + } + } } } } - node = node->nextLight; } + else + { + auto wallLightList = di->Level->lightlists.wall_dlist.find(seg->sidedef); + + if (wallLightList != di->Level->lightlists.wall_dlist.end()) + { + for (auto nodeIterator = wallLightList->second.begin(); nodeIterator != wallLightList->second.end(); nodeIterator++) + { + auto node = nodeIterator->second; + if (!node) continue; + + if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) + { + iter_dlight++; + + DVector3 posrel = node->lightsource->PosRelative(seg->frontsector->PortalGroup); + float x = posrel.X; + float y = posrel.Y; + float z = posrel.Z; + float dist = fabsf(p.DistToPoint(x, z, y)); + float radius = node->lightsource->GetRadius(); + float scale = 1.0f / ((2.f * radius) - dist); + FVector3 fn, pos; + + if (radius > 0.f && dist < radius) + { + FVector3 nearPt, up, right; + + pos = { x, z, y }; + fn = p.Normal(); + + fn.GetRightUp(right, up); + + FVector3 tmpVec = fn * dist; + nearPt = pos + tmpVec; + + FVector3 t1; + int outcnt[4]={0,0,0,0}; + texcoord tcs[4]; + + // do a quick check whether the light touches this polygon + for(int i=0;i<4;i++) + { + t1 = FVector3(&vtx[i*3]); + FVector3 nearToVert = t1 - nearPt; + tcs[i].u = ((nearToVert | right) * scale) + 0.5f; + tcs[i].v = ((nearToVert | up) * scale) + 0.5f; + + if (tcs[i].u<0) outcnt[0]++; + if (tcs[i].u>1) outcnt[1]++; + if (tcs[i].v<0) outcnt[2]++; + if (tcs[i].v>1) outcnt[3]++; + + } + if (outcnt[0]!=4 && outcnt[1]!=4 && outcnt[2]!=4 && outcnt[3]!=4) + { + draw_dlight += GetLight(lightdata, seg->frontsector->PortalGroup, p, node->lightsource, true); + } + } + } + } + } + } + dynlightindex = screen->mLights->UploadLights(lightdata); } diff --git a/src/rendering/swrenderer/line/r_walldraw.cpp b/src/rendering/swrenderer/line/r_walldraw.cpp index bd7ae48c4..d96b6b21e 100644 --- a/src/rendering/swrenderer/line/r_walldraw.cpp +++ b/src/rendering/swrenderer/line/r_walldraw.cpp @@ -209,11 +209,55 @@ namespace swrenderer FLightNode* RenderWallPart::GetLightList() { + while (light_list) + { + // Clear out the wall part light list + FLightNode *next = nullptr; + if (light_list->nextLight) next = light_list->nextLight; + delete light_list; + if (next) light_list = next; + } + CameraLight* cameraLight = CameraLight::Instance(); if ((cameraLight->FixedLightLevel() >= 0) || cameraLight->FixedColormap()) return nullptr; // [SP] Don't draw dynlights if invul/lightamp active else if (curline && curline->sidedef) - return curline->sidedef->lighthead; + { + auto Level = curline->Subsector->sector->Level; + auto wallLightList = Level->lightlists.wall_dlist.find(curline->sidedef); + if (wallLightList != Level->lightlists.wall_dlist.end()) + { + for (auto nodeIterator = wallLightList->second.begin(); nodeIterator != wallLightList->second.end(); nodeIterator++) + { + auto node = nodeIterator->second; + if (!node) continue; + + if (node->lightsource->IsActive()) + { + bool found = false; + FLightNode *light_node = light_list; + while (light_node) + { + if (light_node->lightsource == node->lightsource) + { + found = true; + break; + } + light_node = light_node->nextLight; + } + if (!found) + { + FLightNode *newlight = new FLightNode; + newlight->nextLight = light_list; + newlight->lightsource = node->lightsource; + light_list = newlight; + } + } + } + } + + return light_list; + } else return nullptr; } diff --git a/src/rendering/swrenderer/plane/r_visibleplane.cpp b/src/rendering/swrenderer/plane/r_visibleplane.cpp index c8517cac4..8a0624992 100644 --- a/src/rendering/swrenderer/plane/r_visibleplane.cpp +++ b/src/rendering/swrenderer/plane/r_visibleplane.cpp @@ -66,7 +66,7 @@ namespace swrenderer fillshort(top, viewwidth, 0x7fff); } - void VisiblePlane::AddLights(RenderThread *thread, FLightNode *node) + void VisiblePlane::AddLights(RenderThread *thread, FSection *sec) { if (!r_dynlights) return; @@ -75,30 +75,37 @@ namespace swrenderer if (cameraLight->FixedColormap() != NULL || cameraLight->FixedLightLevel() >= 0) return; // [SP] no dynlights if invul or lightamp - while (node) + auto Level = sec->sector->Level; + auto flatLightList = Level->lightlists.flat_dlist.find(sec); + if (flatLightList != Level->lightlists.flat_dlist.end()) { - if (node->lightsource->IsActive() && (height.PointOnSide(node->lightsource->Pos) > 0)) + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) { - bool found = false; - VisiblePlaneLight *light_node = lights; - while (light_node) + auto node = nodeIterator->second; + if (!node) continue; + + if (node->lightsource->IsActive() && (height.PointOnSide(node->lightsource->Pos) > 0)) { - if (light_node->lightsource == node->lightsource) + bool found = false; + VisiblePlaneLight *light_node = lights; + while (light_node) { - found = true; - break; + if (light_node->lightsource == node->lightsource) + { + found = true; + break; + } + light_node = light_node->next; + } + if (!found) + { + VisiblePlaneLight *newlight = thread->FrameMemory->NewObject(); + newlight->next = lights; + newlight->lightsource = node->lightsource; + lights = newlight; } - light_node = light_node->next; - } - if (!found) - { - VisiblePlaneLight *newlight = thread->FrameMemory->NewObject(); - newlight->next = lights; - newlight->lightsource = node->lightsource; - lights = newlight; } } - node = node->nextLight; } } diff --git a/src/rendering/swrenderer/plane/r_visibleplane.h b/src/rendering/swrenderer/plane/r_visibleplane.h index fcb5c6786..3189a5d67 100644 --- a/src/rendering/swrenderer/plane/r_visibleplane.h +++ b/src/rendering/swrenderer/plane/r_visibleplane.h @@ -46,7 +46,7 @@ namespace swrenderer { VisiblePlane(RenderThread *thread); - void AddLights(RenderThread *thread, FLightNode *node); + void AddLights(RenderThread *thread, FSection *sec); void Render(RenderThread *thread, fixed_t alpha, bool additive, bool masked); VisiblePlane *next = nullptr; // Next visplane in hash chain -- killough diff --git a/src/rendering/swrenderer/scene/r_opaque_pass.cpp b/src/rendering/swrenderer/scene/r_opaque_pass.cpp index f94244644..49904e6bb 100644 --- a/src/rendering/swrenderer/scene/r_opaque_pass.cpp +++ b/src/rendering/swrenderer/scene/r_opaque_pass.cpp @@ -561,7 +561,7 @@ namespace swrenderer Fake3DOpaque::Normal, 0); - ceilingplane->AddLights(Thread, sub->section->lighthead); + ceilingplane->AddLights(Thread, sub->section); } int adjusted_floorlightlevel = floorlightlevel; @@ -601,7 +601,7 @@ namespace swrenderer Fake3DOpaque::Normal, 0); - floorplane->AddLights(Thread, sub->section->lighthead); + floorplane->AddLights(Thread, sub->section); } Add3DFloorPlanes(sub, frontsector, basecolormap, foggy, adjusted_ceilinglightlevel, adjusted_floorlightlevel); @@ -738,7 +738,7 @@ namespace swrenderer Fake3DOpaque::FakeFloor, fakeAlpha); - floorplane3d->AddLights(Thread, sub->section->lighthead); + floorplane3d->AddLights(Thread, sub->section); FakeDrawLoop(sub, &tempsec, floorplane3d, nullptr, Fake3DOpaque::FakeFloor); } @@ -806,7 +806,7 @@ namespace swrenderer Fake3DOpaque::FakeCeiling, fakeAlpha); - ceilingplane3d->AddLights(Thread, sub->section->lighthead); + ceilingplane3d->AddLights(Thread, sub->section); FakeDrawLoop(sub, &tempsec, nullptr, ceilingplane3d, Fake3DOpaque::FakeCeiling); } diff --git a/src/rendering/swrenderer/things/r_sprite.cpp b/src/rendering/swrenderer/things/r_sprite.cpp index b05a67448..513faa6b5 100644 --- a/src/rendering/swrenderer/things/r_sprite.cpp +++ b/src/rendering/swrenderer/things/r_sprite.cpp @@ -208,42 +208,48 @@ namespace swrenderer float lit_red = 0; float lit_green = 0; float lit_blue = 0; - auto node = vis->section->lighthead; - while (node != nullptr) + auto Level = vis->sector->Level; + auto flatLightList = Level->lightlists.flat_dlist.find(vis->section); + if (flatLightList != Level->lightlists.flat_dlist.end()) { - FDynamicLight *light = node->lightsource; - if (light->ShouldLightActor(thing)) + for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) { - float lx = (float)(light->X() - thing->X()); - float ly = (float)(light->Y() - thing->Y()); - float lz = (float)(light->Z() - thing->Center()); - float LdotL = lx * lx + ly * ly + lz * lz; - float radius = node->lightsource->GetRadius(); - if (radius * radius >= LdotL) + auto node = nodeIterator->second; + if (!node) continue; + + FDynamicLight *light = node->lightsource; + if (light->ShouldLightActor(thing)) { - float distance = sqrt(LdotL); - float attenuation = 1.0f - distance / radius; - if (attenuation > 0.0f) - { - float red = light->GetRed() * (1.0f / 255.0f); - float green = light->GetGreen() * (1.0f / 255.0f); - float blue = light->GetBlue() * (1.0f / 255.0f); - /*if (light->IsSubtractive()) - { - float bright = FVector3(lr, lg, lb).Length(); - FVector3 lightColor(lr, lg, lb); - red = (bright - lr) * -1; - green = (bright - lg) * -1; - blue = (bright - lb) * -1; - }*/ - - lit_red += red * attenuation; - lit_green += green * attenuation; - lit_blue += blue * attenuation; + float lx = (float)(light->X() - thing->X()); + float ly = (float)(light->Y() - thing->Y()); + float lz = (float)(light->Z() - thing->Center()); + float LdotL = lx * lx + ly * ly + lz * lz; + float radius = node->lightsource->GetRadius(); + if (radius * radius >= LdotL) + { + float distance = sqrt(LdotL); + float attenuation = 1.0f - distance / radius; + if (attenuation > 0.0f) + { + float red = light->GetRed() * (1.0f / 255.0f); + float green = light->GetGreen() * (1.0f / 255.0f); + float blue = light->GetBlue() * (1.0f / 255.0f); + /*if (light->IsSubtractive()) + { + float bright = FVector3(lr, lg, lb).Length(); + FVector3 lightColor(lr, lg, lb); + red = (bright - lr) * -1; + green = (bright - lg) * -1; + blue = (bright - lb) * -1; + }*/ + + lit_red += red * attenuation; + lit_green += green * attenuation; + lit_blue += blue * attenuation; + } } } } - node = node->nextLight; } lit_red = clamp(lit_red * 255.0f, 0.0f, 255.0f); lit_green = clamp(lit_green * 255.0f, 0.0f, 255.0f); From 4fb4e1859452c4d9dbec4d9235593d2ac4edea9d Mon Sep 17 00:00:00 2001 From: Gene Date: Sun, 6 Apr 2025 22:05:49 -0700 Subject: [PATCH 104/384] Don't inline AddLightNode --- src/playsim/a_dynlight.cpp | 127 +++++++++++++++++++++---------------- src/playsim/a_dynlight.h | 1 + 2 files changed, 73 insertions(+), 55 deletions(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 2548614a9..2bfcd393d 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -418,6 +418,76 @@ void FDynamicLight::UpdateLocation() } } +//============================================================================= +// +// Attempts to emplace the light node in the unordered_map +// +//============================================================================= + +void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) +{ + auto updateFlatTList = [&](FSection *sec) + { + touchlists->flat_tlist.try_emplace(sec, sec); + }; + auto updateWallTList = [&](side_t *sidedef) + { + touchlists->wall_tlist.try_emplace(sidedef, sidedef); + }; + + FLightNode * node = new FLightNode; + node->lightsource = this; + + if (section) + { + node->targ = section; + + auto flatLightList = Level->lightlists.flat_dlist.find(section); + if (flatLightList != Level->lightlists.flat_dlist.end()) + { + auto ret = flatLightList->second.try_emplace(this, node); + if (ret.second) + { + updateFlatTList(section); + } + else + { + delete node; + } + } + else + { + std::unordered_map u = { {this, node} }; + Level->lightlists.flat_dlist.try_emplace(section, u); + updateFlatTList(section); + } + } + else if (sidedef) + { + node->targ = sidedef; + + auto wallLightList = Level->lightlists.wall_dlist.find(sidedef); + if (wallLightList != Level->lightlists.wall_dlist.end()) + { + auto ret = wallLightList->second.try_emplace(this, node); + if (ret.second) + { + updateWallTList(sidedef); + } + else + { + delete node; + } + } + else + { + std::unordered_map u = { {this, node} }; + Level->lightlists.wall_dlist.try_emplace(sidedef, u); + updateWallTList(sidedef); + } + } +} + @@ -475,38 +545,7 @@ void FDynamicLight::CollectWithinRadius(const DVector3 &opos, FSection *section, auto pos = collected_ss[i].pos; section = collected_ss[i].sect; - auto updateFlatTList = [&](FSection *sec) - { - touchlists->flat_tlist.try_emplace(sec, sec); - }; - auto updateWallTList = [&](side_t *sidedef) - { - touchlists->wall_tlist.try_emplace(sidedef, sidedef); - }; - - FLightNode * node = new FLightNode; - node->targ = section; - node->lightsource = this; - - auto flatLightList = Level->lightlists.flat_dlist.find(section); - if (flatLightList != Level->lightlists.flat_dlist.end()) - { - auto ret = flatLightList->second.try_emplace(this, node); - if (ret.second) - { - updateFlatTList(section); - } - else - { - delete node; - } - } - else - { - std::unordered_map u = { {this, node} }; - Level->lightlists.flat_dlist.try_emplace(section, u); - updateFlatTList(section); - } + AddLightNode(section, NULL); auto processSide = [&](side_t *sidedef, const vertex_t *v1, const vertex_t *v2) @@ -519,29 +558,7 @@ void FDynamicLight::CollectWithinRadius(const DVector3 &opos, FSection *section, { linedef->validcount = ::validcount; - FLightNode * node = new FLightNode; - node->targ = sidedef; - node->lightsource = this; - - auto wallLightList = Level->lightlists.wall_dlist.find(sidedef); - if (wallLightList != Level->lightlists.wall_dlist.end()) - { - auto ret = wallLightList->second.try_emplace(this, node); - if (ret.second) - { - updateWallTList(sidedef); - } - else - { - delete node; - } - } - else - { - std::unordered_map u = { {this, node} }; - Level->lightlists.wall_dlist.try_emplace(sidedef, u); - updateWallTList(sidedef); - } + AddLightNode(NULL, sidedef); } else if (linedef->sidedef[0] == sidedef && linedef->sidedef[1] == nullptr) { diff --git a/src/playsim/a_dynlight.h b/src/playsim/a_dynlight.h index 9127cfd21..a305278c2 100644 --- a/src/playsim/a_dynlight.h +++ b/src/playsim/a_dynlight.h @@ -251,6 +251,7 @@ struct FDynamicLight void Tick(); void UpdateLocation(); + void AddLightNode(FSection *section, side_t *sidedef); void LinkLight(); void UnlinkLight(); void ReleaseLight(); From 81165facf2bd8a2939e9da772ac898de4156f82c Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 7 Apr 2025 00:13:11 -0700 Subject: [PATCH 105/384] Actually null the pointer --- src/playsim/a_dynlight.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 2bfcd393d..4cd278b92 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -713,6 +713,7 @@ void FDynamicLight::UnlinkLight () } } delete touchlists; + touchlists = nullptr; shadowmapped = false; } From 9e32e58c8fcfc6ec4f9d9b24e9b233e9acf8f676 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 7 Apr 2025 04:21:35 -0700 Subject: [PATCH 106/384] Try TMap --- src/g_level.cpp | 4 +- src/g_levellocals.h | 6 +- src/playsim/a_dynlight.cpp | 72 +++++++++++-------- src/playsim/a_dynlight.h | 4 +- src/rendering/hwrenderer/scene/hw_flats.cpp | 10 +-- .../hwrenderer/scene/hw_renderhacks.cpp | 10 +-- .../hwrenderer/scene/hw_spritelight.cpp | 20 +++--- src/rendering/hwrenderer/scene/hw_walls.cpp | 20 +++--- src/rendering/swrenderer/line/r_walldraw.cpp | 10 +-- .../swrenderer/plane/r_visibleplane.cpp | 10 +-- src/rendering/swrenderer/things/r_sprite.cpp | 10 +-- 11 files changed, 101 insertions(+), 75 deletions(-) diff --git a/src/g_level.cpp b/src/g_level.cpp index f7c448407..b42d5885f 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -623,8 +623,8 @@ void G_InitNew (const char *mapname, bool bTitleLevel) primaryLevel->totaltime = 0; primaryLevel->spawnindex = 0; - primaryLevel->lightlists.wall_dlist.clear(); - primaryLevel->lightlists.flat_dlist.clear(); + primaryLevel->lightlists.wall_dlist.Clear(); + primaryLevel->lightlists.flat_dlist.Clear(); if (!multiplayer || !deathmatch) { diff --git a/src/g_levellocals.h b/src/g_levellocals.h index bbc313f49..16b97e5e2 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -35,8 +35,6 @@ #pragma once -#include - #include "doomdata.h" #include "g_level.h" #include "r_defs.h" @@ -62,8 +60,8 @@ struct FGlobalDLightLists { - std::unordered_map> flat_dlist; - std::unordered_map> wall_dlist; + TMap> flat_dlist; + TMap> wall_dlist; }; //============================================================================ diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 4cd278b92..7be316cb8 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -420,7 +420,7 @@ void FDynamicLight::UpdateLocation() //============================================================================= // -// Attempts to emplace the light node in the unordered_map +// Attempts to emplace the light node in the TMap // //============================================================================= @@ -428,11 +428,11 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) { auto updateFlatTList = [&](FSection *sec) { - touchlists->flat_tlist.try_emplace(sec, sec); + touchlists->flat_tlist.Insert(sec, sec); }; auto updateWallTList = [&](side_t *sidedef) { - touchlists->wall_tlist.try_emplace(sidedef, sidedef); + touchlists->wall_tlist.Insert(sidedef, sidedef); }; FLightNode * node = new FLightNode; @@ -442,12 +442,12 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) { node->targ = section; - auto flatLightList = Level->lightlists.flat_dlist.find(section); - if (flatLightList != Level->lightlists.flat_dlist.end()) + auto flatLightList = Level->lightlists.flat_dlist.CheckKey(section); + if (flatLightList) { - auto ret = flatLightList->second.try_emplace(this, node); - if (ret.second) + if (!flatLightList->CheckKey(this)) { + flatLightList->Insert(this, node); updateFlatTList(section); } else @@ -457,8 +457,9 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) } else { - std::unordered_map u = { {this, node} }; - Level->lightlists.flat_dlist.try_emplace(section, u); + TMap u; + u.Insert(this, node); + Level->lightlists.flat_dlist.Insert(section, u); updateFlatTList(section); } } @@ -466,12 +467,12 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) { node->targ = sidedef; - auto wallLightList = Level->lightlists.wall_dlist.find(sidedef); - if (wallLightList != Level->lightlists.wall_dlist.end()) + auto wallLightList = Level->lightlists.wall_dlist.CheckKey(sidedef); + if (wallLightList) { - auto ret = wallLightList->second.try_emplace(this, node); - if (ret.second) + if (!wallLightList->CheckKey(this)) { + wallLightList->Insert(this, node); updateWallTList(sidedef); } else @@ -481,8 +482,9 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) } else { - std::unordered_map u = { {this, node} }; - Level->lightlists.wall_dlist.try_emplace(sidedef, u); + TMap u; + u.Insert(this, node); + Level->lightlists.wall_dlist.Insert(sidedef, u); updateWallTList(sidedef); } } @@ -680,40 +682,48 @@ void FDynamicLight::LinkLight() //========================================================================== void FDynamicLight::UnlinkLight () { - for (auto iter = touchlists->wall_tlist.begin(); iter != touchlists->wall_tlist.end(); iter++) + + TMap::Iterator wit(touchlists->wall_tlist); + TMap::Pair *wpair; + while (wit.NextPair(wpair)) { - auto sidedef = iter->second; + auto sidedef = wpair->Value; if (!sidedef) continue; - auto wallLightList = Level->lightlists.wall_dlist.find(sidedef); - if (wallLightList != Level->lightlists.wall_dlist.end()) + auto wallLightList = Level->lightlists.wall_dlist.CheckKey(sidedef); + if (wallLightList) { - auto light = wallLightList->second.find(this); - if (light != wallLightList->second.end()) + auto light = *wallLightList->CheckKey(this); + if (light) { - delete light->second; - wallLightList->second.erase(light); + delete light; + wallLightList->Remove(this); } } } - for (auto iter = touchlists->flat_tlist.begin(); iter != touchlists->flat_tlist.end(); iter++) + + TMap::Iterator fit(touchlists->flat_tlist); + TMap::Pair *fpair; + while (fit.NextPair(fpair)) { - auto sec = iter->second; + auto sec = fpair->Value; if (!sec) continue; - auto flatLightList = Level->lightlists.flat_dlist.find(sec); - if (flatLightList != Level->lightlists.flat_dlist.end()) + auto flatLightList = Level->lightlists.flat_dlist.CheckKey(sec); + if (flatLightList) { - auto light = flatLightList->second.find(this); - if (light != flatLightList->second.end()) + auto light = *flatLightList->CheckKey(this); + if (light) { - delete light->second; - flatLightList->second.erase(light); + delete light; + flatLightList->Remove(this); } } } + delete touchlists; touchlists = nullptr; + shadowmapped = false; } diff --git a/src/playsim/a_dynlight.h b/src/playsim/a_dynlight.h index a305278c2..00a6a82b2 100644 --- a/src/playsim/a_dynlight.h +++ b/src/playsim/a_dynlight.h @@ -197,8 +197,8 @@ struct FLightNode struct FDynamicLightTouchLists { - std::unordered_map flat_tlist; - std::unordered_map wall_tlist; + TMap flat_tlist; + TMap wall_tlist; }; struct FDynamicLight diff --git a/src/rendering/hwrenderer/scene/hw_flats.cpp b/src/rendering/hwrenderer/scene/hw_flats.cpp index 3839accad..2e5cf86ae 100644 --- a/src/rendering/hwrenderer/scene/hw_flats.cpp +++ b/src/rendering/hwrenderer/scene/hw_flats.cpp @@ -159,13 +159,15 @@ void HWFlat::SetupLights(HWDrawInfo *di, FDynLightData &lightdata, int portalgro return; // no lights on additively blended surfaces. } - auto flatLightList = di->Level->lightlists.flat_dlist.find(section); + auto flatLightList = di->Level->lightlists.flat_dlist.CheckKey(section); - if (flatLightList != di->Level->lightlists.flat_dlist.end()) + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; FDynamicLight * light = node->lightsource; diff --git a/src/rendering/hwrenderer/scene/hw_renderhacks.cpp b/src/rendering/hwrenderer/scene/hw_renderhacks.cpp index c4c65fd18..540923721 100644 --- a/src/rendering/hwrenderer/scene/hw_renderhacks.cpp +++ b/src/rendering/hwrenderer/scene/hw_renderhacks.cpp @@ -117,13 +117,15 @@ int HWDrawInfo::SetupLightsForOtherPlane(subsector_t * sub, FDynLightData &light lightdata.Clear(); - auto flatLightList = Level->lightlists.flat_dlist.find(sub->section); + auto flatLightList = Level->lightlists.flat_dlist.CheckKey(sub->section); - if (flatLightList != Level->lightlists.flat_dlist.end()) + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; FDynamicLight * light = node->lightsource; diff --git a/src/rendering/hwrenderer/scene/hw_spritelight.cpp b/src/rendering/hwrenderer/scene/hw_spritelight.cpp index a7a92f033..63bbf6268 100644 --- a/src/rendering/hwrenderer/scene/hw_spritelight.cpp +++ b/src/rendering/hwrenderer/scene/hw_spritelight.cpp @@ -124,13 +124,15 @@ void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FSec } // Go through both light lists - auto flatLightList = Level->lightlists.flat_dlist.find(sec); + auto flatLightList = Level->lightlists.flat_dlist.CheckKey(sec); - if (flatLightList != Level->lightlists.flat_dlist.end()) + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; light=node->lightsource; @@ -248,12 +250,14 @@ void hw_GetDynModelLight(AActor *self, FDynLightData &modellightdata) { auto section = subsector->section; if (section->validcount == dl_validcount) return; // already done from a previous subsector. - auto flatLightList = self->Level->lightlists.flat_dlist.find(subsector->section); - if (flatLightList != self->Level->lightlists.flat_dlist.end()) + auto flatLightList = self->Level->lightlists.flat_dlist.CheckKey(subsector->section); + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { // check all lights touching a subsector - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; FDynamicLight *light = node->lightsource; if (light->ShouldLightActor(self)) diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index ea81f1fc7..3c0d3e861 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -427,13 +427,15 @@ void HWWall::SetupLights(HWDrawInfo*di, FDynLightData &lightdata) if ((seg->sidedef->Flags & WALLF_POLYOBJ) && sub) { // Polobject segs cannot be checked per sidedef so use the subsector instead. - auto flatLightList = di->Level->lightlists.flat_dlist.find(sub->section); + auto flatLightList = di->Level->lightlists.flat_dlist.CheckKey(sub->section); - if (flatLightList != di->Level->lightlists.flat_dlist.end()) + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) @@ -490,13 +492,15 @@ void HWWall::SetupLights(HWDrawInfo*di, FDynLightData &lightdata) } else { - auto wallLightList = di->Level->lightlists.wall_dlist.find(seg->sidedef); + auto wallLightList = di->Level->lightlists.wall_dlist.CheckKey(seg->sidedef); - if (wallLightList != di->Level->lightlists.wall_dlist.end()) + if (wallLightList) { - for (auto nodeIterator = wallLightList->second.begin(); nodeIterator != wallLightList->second.end(); nodeIterator++) + TMap::Iterator it(*wallLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) diff --git a/src/rendering/swrenderer/line/r_walldraw.cpp b/src/rendering/swrenderer/line/r_walldraw.cpp index d96b6b21e..f49bc8e7b 100644 --- a/src/rendering/swrenderer/line/r_walldraw.cpp +++ b/src/rendering/swrenderer/line/r_walldraw.cpp @@ -224,12 +224,14 @@ namespace swrenderer else if (curline && curline->sidedef) { auto Level = curline->Subsector->sector->Level; - auto wallLightList = Level->lightlists.wall_dlist.find(curline->sidedef); - if (wallLightList != Level->lightlists.wall_dlist.end()) + auto wallLightList = Level->lightlists.wall_dlist.CheckKey(curline->sidedef); + if (wallLightList) { - for (auto nodeIterator = wallLightList->second.begin(); nodeIterator != wallLightList->second.end(); nodeIterator++) + TMap::Iterator it(*wallLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; if (node->lightsource->IsActive()) diff --git a/src/rendering/swrenderer/plane/r_visibleplane.cpp b/src/rendering/swrenderer/plane/r_visibleplane.cpp index 8a0624992..282fc25ee 100644 --- a/src/rendering/swrenderer/plane/r_visibleplane.cpp +++ b/src/rendering/swrenderer/plane/r_visibleplane.cpp @@ -76,12 +76,14 @@ namespace swrenderer return; // [SP] no dynlights if invul or lightamp auto Level = sec->sector->Level; - auto flatLightList = Level->lightlists.flat_dlist.find(sec); - if (flatLightList != Level->lightlists.flat_dlist.end()) + auto flatLightList = Level->lightlists.flat_dlist.CheckKey(sec); + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; if (node->lightsource->IsActive() && (height.PointOnSide(node->lightsource->Pos) > 0)) diff --git a/src/rendering/swrenderer/things/r_sprite.cpp b/src/rendering/swrenderer/things/r_sprite.cpp index 513faa6b5..ff1d81bea 100644 --- a/src/rendering/swrenderer/things/r_sprite.cpp +++ b/src/rendering/swrenderer/things/r_sprite.cpp @@ -209,12 +209,14 @@ namespace swrenderer float lit_green = 0; float lit_blue = 0; auto Level = vis->sector->Level; - auto flatLightList = Level->lightlists.flat_dlist.find(vis->section); - if (flatLightList != Level->lightlists.flat_dlist.end()) + auto flatLightList = Level->lightlists.flat_dlist.CheckKey(vis->section); + if (flatLightList) { - for (auto nodeIterator = flatLightList->second.begin(); nodeIterator != flatLightList->second.end(); nodeIterator++) + TMap::Iterator it(*flatLightList); + TMap::Pair *pair; + while (it.NextPair(pair)) { - auto node = nodeIterator->second; + auto node = pair->Value; if (!node) continue; FDynamicLight *light = node->lightsource; From e2026dca38876da04225167f0a49523b60f8c823 Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 7 Apr 2025 13:57:28 -0700 Subject: [PATCH 107/384] Only allocate when needed --- src/playsim/a_dynlight.cpp | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 7be316cb8..d08ee10ec 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -435,28 +435,27 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) touchlists->wall_tlist.Insert(sidedef, sidedef); }; - FLightNode * node = new FLightNode; - node->lightsource = this; - if (section) { - node->targ = section; - auto flatLightList = Level->lightlists.flat_dlist.CheckKey(section); if (flatLightList) { if (!flatLightList->CheckKey(this)) { + FLightNode * node = new FLightNode; + node->lightsource = this; + node->targ = section; + flatLightList->Insert(this, node); updateFlatTList(section); } - else - { - delete node; - } } else { + FLightNode * node = new FLightNode; + node->lightsource = this; + node->targ = section; + TMap u; u.Insert(this, node); Level->lightlists.flat_dlist.Insert(section, u); @@ -465,23 +464,25 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) } else if (sidedef) { - node->targ = sidedef; - auto wallLightList = Level->lightlists.wall_dlist.CheckKey(sidedef); if (wallLightList) { if (!wallLightList->CheckKey(this)) { + FLightNode * node = new FLightNode; + node->lightsource = this; + node->targ = sidedef; + wallLightList->Insert(this, node); updateWallTList(sidedef); } - else - { - delete node; - } } else { + FLightNode * node = new FLightNode; + node->lightsource = this; + node->targ = sidedef; + TMap u; u.Insert(this, node); Level->lightlists.wall_dlist.Insert(sidedef, u); From 88a55be5619a91dcc1fbbd2608c4e8cede52ff5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 23:01:33 -0300 Subject: [PATCH 108/384] add TryEmplace to TMap --- src/common/utility/tarray.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/common/utility/tarray.h b/src/common/utility/tarray.h index 25d6e0e7f..ac9d8beb1 100644 --- a/src/common/utility/tarray.h +++ b/src/common/utility/tarray.h @@ -1380,6 +1380,18 @@ public: return n->Pair.Value; } + template + VT &TryEmplace(const KT key, Args&&... args) + { + Node *n = FindKey(key); + if (n == NULL) + { + n = NewKey(key); + ::new(&n->Pair.Value) VT(std::forward(args)...); + } + return n->Pair.Value; + } + //======================================================================= // // Remove From d40c5246e5f399e13d3dddac0179c75fecf72d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 23:20:14 -0300 Subject: [PATCH 109/384] optimize memory allocation --- src/playsim/a_dynlight.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index d08ee10ec..4351592f7 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -95,7 +95,7 @@ static FDynamicLight *GetLight(FLevelLocals *Level) ret->mShadowmapIndex = 1024; ret->Level = Level; ret->Pos.X = -10000000; // not a valid coordinate. - if (!ret->touchlists) ret->touchlists = new FDynamicLightTouchLists; + ret->touchlists = new FDynamicLightTouchLists; return ret; } @@ -210,6 +210,8 @@ void FDynamicLight::ReleaseLight() else Level->lights = next; if (next != nullptr) next->prev = prev; next = prev = nullptr; + delete touchlists; + touchlists = nullptr; FreeList.Push(this); } @@ -722,8 +724,8 @@ void FDynamicLight::UnlinkLight () } } - delete touchlists; - touchlists = nullptr; + touchlists->flat_tlist.Clear(); + touchlists->wall_tlist.Clear(); shadowmapped = false; } From 2b72de44d5fb309036b619235ff9c0c68e8dfcce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 23:20:23 -0300 Subject: [PATCH 110/384] use TryEmplace --- src/playsim/a_dynlight.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 4351592f7..934dbc081 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -430,11 +430,11 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) { auto updateFlatTList = [&](FSection *sec) { - touchlists->flat_tlist.Insert(sec, sec); + touchlists->flat_tlist.TryEmplace(sec, sec); }; auto updateWallTList = [&](side_t *sidedef) { - touchlists->wall_tlist.Insert(sidedef, sidedef); + touchlists->wall_tlist.TryEmplace(sidedef, sidedef); }; if (section) @@ -448,7 +448,7 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) node->lightsource = this; node->targ = section; - flatLightList->Insert(this, node); + flatLightList->TryEmplace(this, node); updateFlatTList(section); } } @@ -459,8 +459,8 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) node->targ = section; TMap u; - u.Insert(this, node); - Level->lightlists.flat_dlist.Insert(section, u); + u.TryEmplace(this, node); + Level->lightlists.flat_dlist.TryEmplace(section, u); updateFlatTList(section); } } @@ -475,7 +475,7 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) node->lightsource = this; node->targ = sidedef; - wallLightList->Insert(this, node); + wallLightList->TryEmplace(this, node); updateWallTList(sidedef); } } @@ -486,8 +486,8 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) node->targ = sidedef; TMap u; - u.Insert(this, node); - Level->lightlists.wall_dlist.Insert(sidedef, u); + u.TryEmplace(this, node); + Level->lightlists.wall_dlist.TryEmplace(sidedef, u); updateWallTList(sidedef); } } From bac018d3064016dd4e233b37f5a92fd0a07ea74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 23:25:12 -0300 Subject: [PATCH 111/384] remove extra heap allocation and level of indirection --- src/playsim/a_dynlight.cpp | 18 +++++++++--------- src/playsim/a_dynlight.h | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 934dbc081..cd374e334 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -87,6 +87,7 @@ static FDynamicLight *GetLight(FLevelLocals *Level) } else ret = (FDynamicLight*)DynLightArena.Alloc(sizeof(FDynamicLight)); memset(ret, 0, sizeof(*ret)); + ret = new(ret)FDynamicLight(); ret->m_cycler.m_increment = true; ret->next = Level->lights; Level->lights = ret; @@ -95,7 +96,7 @@ static FDynamicLight *GetLight(FLevelLocals *Level) ret->mShadowmapIndex = 1024; ret->Level = Level; ret->Pos.X = -10000000; // not a valid coordinate. - ret->touchlists = new FDynamicLightTouchLists; + //ret->touchlists = new FDynamicLightTouchLists; return ret; } @@ -210,8 +211,7 @@ void FDynamicLight::ReleaseLight() else Level->lights = next; if (next != nullptr) next->prev = prev; next = prev = nullptr; - delete touchlists; - touchlists = nullptr; + this->~FDynamicLight(); FreeList.Push(this); } @@ -430,11 +430,11 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) { auto updateFlatTList = [&](FSection *sec) { - touchlists->flat_tlist.TryEmplace(sec, sec); + touchlists.flat_tlist.TryEmplace(sec, sec); }; auto updateWallTList = [&](side_t *sidedef) { - touchlists->wall_tlist.TryEmplace(sidedef, sidedef); + touchlists.wall_tlist.TryEmplace(sidedef, sidedef); }; if (section) @@ -686,7 +686,7 @@ void FDynamicLight::LinkLight() void FDynamicLight::UnlinkLight () { - TMap::Iterator wit(touchlists->wall_tlist); + TMap::Iterator wit(touchlists.wall_tlist); TMap::Pair *wpair; while (wit.NextPair(wpair)) { @@ -705,7 +705,7 @@ void FDynamicLight::UnlinkLight () } } - TMap::Iterator fit(touchlists->flat_tlist); + TMap::Iterator fit(touchlists.flat_tlist); TMap::Pair *fpair; while (fit.NextPair(fpair)) { @@ -724,8 +724,8 @@ void FDynamicLight::UnlinkLight () } } - touchlists->flat_tlist.Clear(); - touchlists->wall_tlist.Clear(); + touchlists.flat_tlist.Clear(); + touchlists.wall_tlist.Clear(); shadowmapped = false; } diff --git a/src/playsim/a_dynlight.h b/src/playsim/a_dynlight.h index 00a6a82b2..5424a4a53 100644 --- a/src/playsim/a_dynlight.h +++ b/src/playsim/a_dynlight.h @@ -293,7 +293,7 @@ public: bool swapped; bool explicitpitch; - FDynamicLightTouchLists *touchlists; + FDynamicLightTouchLists touchlists; }; From a43b99bf519bcef16bd6428f30f39e806a749867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 23:36:57 -0300 Subject: [PATCH 112/384] further remove manual memory management --- src/g_levellocals.h | 5 +++-- src/playsim/a_dynlight.cpp | 22 +++++-------------- src/rendering/hwrenderer/scene/hw_flats.cpp | 6 ++--- .../hwrenderer/scene/hw_renderhacks.cpp | 6 ++--- .../hwrenderer/scene/hw_spritelight.cpp | 12 +++++----- src/rendering/hwrenderer/scene/hw_walls.cpp | 12 +++++----- src/rendering/swrenderer/line/r_walldraw.cpp | 6 ++--- .../swrenderer/plane/r_visibleplane.cpp | 6 ++--- src/rendering/swrenderer/things/r_sprite.cpp | 6 ++--- 9 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 16b97e5e2..fb553d984 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -57,11 +57,12 @@ #include "doom_aabbtree.h" #include "doom_levelmesh.h" #include "p_visualthinker.h" +#include struct FGlobalDLightLists { - TMap> flat_dlist; - TMap> wall_dlist; + TMap>> flat_dlist; + TMap>> wall_dlist; }; //============================================================================ diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index cd374e334..37cc4cb22 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -458,9 +458,9 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) node->lightsource = this; node->targ = section; - TMap u; + TMap> u; u.TryEmplace(this, node); - Level->lightlists.flat_dlist.TryEmplace(section, u); + Level->lightlists.flat_dlist.TryEmplace(section, std::move(u)); updateFlatTList(section); } } @@ -485,9 +485,9 @@ void FDynamicLight::AddLightNode(FSection *section, side_t *sidedef) node->lightsource = this; node->targ = sidedef; - TMap u; + TMap> u; u.TryEmplace(this, node); - Level->lightlists.wall_dlist.TryEmplace(sidedef, u); + Level->lightlists.wall_dlist.TryEmplace(sidedef, std::move(u)); updateWallTList(sidedef); } } @@ -696,12 +696,7 @@ void FDynamicLight::UnlinkLight () auto wallLightList = Level->lightlists.wall_dlist.CheckKey(sidedef); if (wallLightList) { - auto light = *wallLightList->CheckKey(this); - if (light) - { - delete light; - wallLightList->Remove(this); - } + wallLightList->Remove(this); } } @@ -715,12 +710,7 @@ void FDynamicLight::UnlinkLight () auto flatLightList = Level->lightlists.flat_dlist.CheckKey(sec); if (flatLightList) { - auto light = *flatLightList->CheckKey(this); - if (light) - { - delete light; - flatLightList->Remove(this); - } + flatLightList->Remove(this); } } diff --git a/src/rendering/hwrenderer/scene/hw_flats.cpp b/src/rendering/hwrenderer/scene/hw_flats.cpp index 2e5cf86ae..3d1839c85 100644 --- a/src/rendering/hwrenderer/scene/hw_flats.cpp +++ b/src/rendering/hwrenderer/scene/hw_flats.cpp @@ -163,11 +163,11 @@ void HWFlat::SetupLights(HWDrawInfo *di, FDynLightData &lightdata, int portalgro if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; FDynamicLight * light = node->lightsource; diff --git a/src/rendering/hwrenderer/scene/hw_renderhacks.cpp b/src/rendering/hwrenderer/scene/hw_renderhacks.cpp index 540923721..8aa077294 100644 --- a/src/rendering/hwrenderer/scene/hw_renderhacks.cpp +++ b/src/rendering/hwrenderer/scene/hw_renderhacks.cpp @@ -121,11 +121,11 @@ int HWDrawInfo::SetupLightsForOtherPlane(subsector_t * sub, FDynLightData &light if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; FDynamicLight * light = node->lightsource; diff --git a/src/rendering/hwrenderer/scene/hw_spritelight.cpp b/src/rendering/hwrenderer/scene/hw_spritelight.cpp index 63bbf6268..0310d1008 100644 --- a/src/rendering/hwrenderer/scene/hw_spritelight.cpp +++ b/src/rendering/hwrenderer/scene/hw_spritelight.cpp @@ -128,11 +128,11 @@ void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FSec if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; light=node->lightsource; @@ -253,11 +253,11 @@ void hw_GetDynModelLight(AActor *self, FDynLightData &modellightdata) auto flatLightList = self->Level->lightlists.flat_dlist.CheckKey(subsector->section); if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { // check all lights touching a subsector - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; FDynamicLight *light = node->lightsource; if (light->ShouldLightActor(self)) diff --git a/src/rendering/hwrenderer/scene/hw_walls.cpp b/src/rendering/hwrenderer/scene/hw_walls.cpp index 3c0d3e861..557004f53 100644 --- a/src/rendering/hwrenderer/scene/hw_walls.cpp +++ b/src/rendering/hwrenderer/scene/hw_walls.cpp @@ -431,11 +431,11 @@ void HWWall::SetupLights(HWDrawInfo*di, FDynLightData &lightdata) if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) @@ -496,11 +496,11 @@ void HWWall::SetupLights(HWDrawInfo*di, FDynLightData &lightdata) if (wallLightList) { - TMap::Iterator it(*wallLightList); - TMap::Pair *pair; + TMap>::Iterator it(*wallLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; if (node->lightsource->IsActive() && !node->lightsource->DontLightMap()) diff --git a/src/rendering/swrenderer/line/r_walldraw.cpp b/src/rendering/swrenderer/line/r_walldraw.cpp index f49bc8e7b..6f12b13fb 100644 --- a/src/rendering/swrenderer/line/r_walldraw.cpp +++ b/src/rendering/swrenderer/line/r_walldraw.cpp @@ -227,11 +227,11 @@ namespace swrenderer auto wallLightList = Level->lightlists.wall_dlist.CheckKey(curline->sidedef); if (wallLightList) { - TMap::Iterator it(*wallLightList); - TMap::Pair *pair; + TMap>::Iterator it(*wallLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; if (node->lightsource->IsActive()) diff --git a/src/rendering/swrenderer/plane/r_visibleplane.cpp b/src/rendering/swrenderer/plane/r_visibleplane.cpp index 282fc25ee..c7d327191 100644 --- a/src/rendering/swrenderer/plane/r_visibleplane.cpp +++ b/src/rendering/swrenderer/plane/r_visibleplane.cpp @@ -79,11 +79,11 @@ namespace swrenderer auto flatLightList = Level->lightlists.flat_dlist.CheckKey(sec); if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; if (node->lightsource->IsActive() && (height.PointOnSide(node->lightsource->Pos) > 0)) diff --git a/src/rendering/swrenderer/things/r_sprite.cpp b/src/rendering/swrenderer/things/r_sprite.cpp index ff1d81bea..102dc429e 100644 --- a/src/rendering/swrenderer/things/r_sprite.cpp +++ b/src/rendering/swrenderer/things/r_sprite.cpp @@ -212,11 +212,11 @@ namespace swrenderer auto flatLightList = Level->lightlists.flat_dlist.CheckKey(vis->section); if (flatLightList) { - TMap::Iterator it(*flatLightList); - TMap::Pair *pair; + TMap>::Iterator it(*flatLightList); + TMap>::Pair *pair; while (it.NextPair(pair)) { - auto node = pair->Value; + auto node = pair->Value.get(); if (!node) continue; FDynamicLight *light = node->lightsource; From fa03385980424e5059103ff27154847907413317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 23:37:40 -0300 Subject: [PATCH 113/384] remove commented out code --- src/playsim/a_dynlight.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index 37cc4cb22..c8982528b 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -96,7 +96,6 @@ static FDynamicLight *GetLight(FLevelLocals *Level) ret->mShadowmapIndex = 1024; ret->Level = Level; ret->Pos.X = -10000000; // not a valid coordinate. - //ret->touchlists = new FDynamicLightTouchLists; return ret; } From ab5091098d43b074ce9fc8cdb0fe881f2500a59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 17 Apr 2025 18:35:43 -0300 Subject: [PATCH 114/384] fix bug with direct cvar assignment being mistakenly allowed --- src/common/console/c_cvars.h | 18 ++++++++++++++++++ src/g_game.cpp | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/common/console/c_cvars.h b/src/common/console/c_cvars.h index ccc3cfa6a..27cf8aa38 100644 --- a/src/common/console/c_cvars.h +++ b/src/common/console/c_cvars.h @@ -589,6 +589,8 @@ class FBoolCVarRef { FBoolCVar* ref; public: + int operator= (const FBoolCVarRef&) = delete; + int operator= (FBoolCVarRef&&) = delete; inline bool operator= (bool val) { *ref = val; return val; } inline operator bool () const { return **ref; } @@ -602,7 +604,11 @@ class FIntCVarRef FIntCVar* ref; public: + int operator= (const FIntCVarRef&) = delete; + int operator= (FIntCVarRef&&) = delete; + int operator= (int val) { *ref = val; return val; } + inline operator int () const { return **ref; } inline int operator *() const { return **ref; } inline FIntCVar* operator->() { return ref; } @@ -613,6 +619,8 @@ class FFloatCVarRef { FFloatCVar* ref; public: + int operator= (const FFloatCVarRef&) = delete; + int operator= (FFloatCVarRef&&) = delete; float operator= (float val) { *ref = val; return val; } inline operator float () const { return **ref; } @@ -625,6 +633,8 @@ class FStringCVarRef { FStringCVar* ref; public: + int operator= (const FStringCVarRef&) = delete; + int operator= (FStringCVarRef&&) = delete; const char* operator= (const char* val) { *ref = val; return val; } inline operator const char* () const { return **ref; } @@ -637,6 +647,8 @@ class FColorCVarRef { FColorCVar* ref; public: + int operator= (const FColorCVarRef&) = delete; + int operator= (FColorCVarRef&&) = delete; //uint32_t operator= (uint32_t val) { *ref = val; return val; } inline operator uint32_t () const { return **ref; } @@ -649,6 +661,9 @@ class FFlagCVarRef { FFlagCVar* ref; public: + int operator= (const FFlagCVarRef&) = delete; + int operator= (FFlagCVarRef&&) = delete; + inline bool operator= (bool val) { *ref = val; return val; } inline bool operator= (const FFlagCVar& val) { *ref = val; return val; } inline operator int () const { return **ref; } @@ -660,6 +675,9 @@ class FMaskCVarRef { FMaskCVar* ref; public: + int operator= (const FMaskCVarRef&) = delete; + int operator= (FMaskCVarRef&&) = delete; + //int operator= (int val) { *ref = val; return val; } inline operator int () const { return **ref; } inline int operator *() const { return **ref; } diff --git a/src/g_game.cpp b/src/g_game.cpp index 6206b4c3d..8e86d86db 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -300,7 +300,7 @@ CCMD (turnspeeds) } if (i <= 4) { - *angleturn[3] = *angleturn[2]; + *angleturn[3] = **angleturn[2]; } } } From 44c9140aadb8b2344ac46a570dd52202bcacc27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 17 Apr 2025 18:38:15 -0300 Subject: [PATCH 115/384] save togglehud to ini so that it can be properly restored on crash/exit --- src/d_main.cpp | 22 +++++++++++----------- src/d_main.h | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/d_main.cpp b/src/d_main.cpp index 9b1f9c530..97bb47ef7 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -310,8 +310,6 @@ CUSTOM_CVAR(Int, I_FriendlyWindowTitle, 1, CVAR_GLOBALCONFIG|CVAR_ARCHIVE|CVAR_N } CVAR(Bool, cl_nointros, false, CVAR_ARCHIVE) - -bool hud_toggled = false; bool wantToRestart; bool DrawFSHUD; // [RH] Draw fullscreen HUD? bool devparm; // started game with -devparm @@ -356,16 +354,18 @@ static int pagetic; // //========================================================================== +CVAR(Int, saved_screenblocks, 10, CVAR_ARCHIVE) +CVAR(Bool, saved_drawplayersprite, true, CVAR_ARCHIVE) +CVAR(Bool, saved_showmessages, true, CVAR_ARCHIVE) +CVAR(Bool, hud_toggled, false, CVAR_ARCHIVE) + void D_ToggleHud() { - static int saved_screenblocks; - static bool saved_drawplayersprite, saved_showmessages; - if ((hud_toggled = !hud_toggled)) { - saved_screenblocks = screenblocks; - saved_drawplayersprite = r_drawplayersprites; - saved_showmessages = show_messages; + saved_screenblocks = *screenblocks; + saved_drawplayersprite = *r_drawplayersprites; + saved_showmessages = *show_messages; screenblocks = 12; r_drawplayersprites = false; show_messages = false; @@ -374,9 +374,9 @@ void D_ToggleHud() } else { - screenblocks = saved_screenblocks; - r_drawplayersprites = saved_drawplayersprite; - show_messages = saved_showmessages; + screenblocks =*saved_screenblocks; + r_drawplayersprites = *saved_drawplayersprite; + show_messages = *saved_showmessages; } } CCMD(togglehud) diff --git a/src/d_main.h b/src/d_main.h index 0d4a33079..b5a08750c 100644 --- a/src/d_main.h +++ b/src/d_main.h @@ -34,7 +34,7 @@ #include "c_cvars.h" extern bool advancedemo; -extern bool hud_toggled; +EXTERN_CVAR(Bool, hud_toggled); void D_ToggleHud(); struct event_t; From 524cd5581353ca42973f8759cf173fae6d4aa19f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 1 Apr 2025 15:20:00 -0400 Subject: [PATCH 116/384] Added OnLoad virtual Allows for things to be reinitialized where needed for Thinkers. Moved hidden state of items over to OnLoad. --- src/playsim/dthinker.cpp | 14 ++++++++++++-- src/playsim/dthinker.h | 1 + src/playsim/p_mobj.cpp | 1 - .../static/zscript/actors/inventory/inventory.zs | 10 ++++++++++ wadsrc/static/zscript/doombase.zs | 1 + 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/playsim/dthinker.cpp b/src/playsim/dthinker.cpp index 05e5e56c3..2d37ffcfd 100644 --- a/src/playsim/dthinker.cpp +++ b/src/playsim/dthinker.cpp @@ -415,12 +415,12 @@ void FThinkerCollection::SerializeThinkers(FSerializer &arc, bool hubLoad) else if (thinker->ObjectFlags & OF_JustSpawned) { FreshThinkers[i].AddTail(thinker); - thinker->PostSerialize(); + thinker->CallPostSerialize(); } else { Thinkers[i].AddTail(thinker); - thinker->PostSerialize(); + thinker->CallPostSerialize(); } } } @@ -833,6 +833,16 @@ void DThinker::PostSerialize() { } +void DThinker::CallPostSerialize() +{ + PostSerialize(); + IFOVERRIDENVIRTUALPTRNAME(this, NAME_Thinker, OnLoad) + { + VMValue params[] = { this }; + VMCall(func, params, 1, nullptr, 0); + } +} + //========================================================================== // // diff --git a/src/playsim/dthinker.h b/src/playsim/dthinker.h index ab4a453b8..95c2bb4bc 100644 --- a/src/playsim/dthinker.h +++ b/src/playsim/dthinker.h @@ -105,6 +105,7 @@ public: virtual void PostBeginPlay (); // Called just before the first tick virtual void CallPostBeginPlay(); // different in actor. virtual void PostSerialize(); + void CallPostSerialize(); void Serialize(FSerializer &arc) override; size_t PropagateMark(); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 4d18899f5..161230a25 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -242,7 +242,6 @@ void AActor::Serialize(FSerializer &arc) A("angles", Angles) A("frame", frame) A("scale", Scale) - A("nolocalrender", NoLocalRender) // Note: This will probably be removed later since a better solution is needed A("renderstyle", RenderStyle) A("renderflags", renderflags) A("renderflags2", renderflags2) diff --git a/wadsrc/static/zscript/actors/inventory/inventory.zs b/wadsrc/static/zscript/actors/inventory/inventory.zs index 607c4f7c4..40a89cb08 100644 --- a/wadsrc/static/zscript/actors/inventory/inventory.zs +++ b/wadsrc/static/zscript/actors/inventory/inventory.zs @@ -85,6 +85,11 @@ class Inventory : Actor Inventory.PickupSound "misc/i_pkup"; Inventory.PickupMessage "$TXT_DEFAULTPICKUPMSG"; } + + override void OnLoad() + { + UpdateLocalPickupStatus(); + } //native override void Tick(); @@ -1113,6 +1118,11 @@ class Inventory : Actor return pickedUp[client.PlayerNumber()]; } + void UpdateLocalPickupStatus() + { + DisableLocalRendering(consoleplayer, pickedUp[consoleplayer]); + } + // When items are dropped, clear their local pick ups. void ClearLocalPickUps() { diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 7cd4d3be4..ffebf625f 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -238,6 +238,7 @@ class Thinker : Object native play virtual native void Tick(); virtual native void PostBeginPlay(); + virtual void OnLoad() {} native void ChangeStatNum(int stat); static clearscope int Tics2Seconds(int tics) From e199fd11d53875bc7653653655a4f1d081225f56 Mon Sep 17 00:00:00 2001 From: dileepvr Date: Sun, 20 Apr 2025 09:46:39 -0600 Subject: [PATCH 117/384] Fixed pitch culling in reflective flats for OoB Viewpoints The vertical clipper needed viewpoint pitch sign to be flipped to work correctly. Only relevant for OoB viewpoints. --- src/rendering/hwrenderer/scene/hw_drawinfo.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 0e647e307..10fce9648 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -486,7 +486,9 @@ void HWDrawInfo::CreateScene(bool drawpsprites) { double a2 = 20.0 + 0.5*Viewpoint.FieldOfView.Degrees(); // FrustumPitch for vertical clipping if (a2 > 179.0) a2 = 179.0; - vClipper->SafeAddClipRangeDegPitches(vp.HWAngles.Pitch.Degrees() - a2, vp.HWAngles.Pitch.Degrees() + a2); // clip the suplex range + double pitchmult = (portalState.PlaneMirrorFlag % 2 != 0) ? -1.0 : 1.0; + vClipper->SafeAddClipRangeDegPitches(pitchmult * vp.HWAngles.Pitch.Degrees() - a2, pitchmult * vp.HWAngles.Pitch.Degrees() + a2); // clip the suplex range + Viewpoint.PitchSin *= pitchmult; } // reset the portal manager From 34451c9acd75753518b4ee005500b1bdbbca3591 Mon Sep 17 00:00:00 2001 From: dileepvr Date: Sun, 20 Apr 2025 11:27:05 -0600 Subject: [PATCH 118/384] Boolean op instead of mod with 2 `( ... & 1)` is simpler than `(... % 2 != 0)`. --- src/rendering/hwrenderer/scene/hw_drawinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index 10fce9648..e8f578dfe 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -486,7 +486,7 @@ void HWDrawInfo::CreateScene(bool drawpsprites) { double a2 = 20.0 + 0.5*Viewpoint.FieldOfView.Degrees(); // FrustumPitch for vertical clipping if (a2 > 179.0) a2 = 179.0; - double pitchmult = (portalState.PlaneMirrorFlag % 2 != 0) ? -1.0 : 1.0; + double pitchmult = (portalState.PlaneMirrorFlag & 1) ? -1.0 : 1.0; vClipper->SafeAddClipRangeDegPitches(pitchmult * vp.HWAngles.Pitch.Degrees() - a2, pitchmult * vp.HWAngles.Pitch.Degrees() + a2); // clip the suplex range Viewpoint.PitchSin *= pitchmult; } From 701d1f71d1c45b68a1be5bc3e5dcd9ea336c7f92 Mon Sep 17 00:00:00 2001 From: dileepvr Date: Sun, 20 Apr 2025 13:27:48 -0600 Subject: [PATCH 119/384] Ensure boolean to suppress compiler warning Finally used `!!(...)` somewhere. --- src/rendering/hwrenderer/scene/hw_drawinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index e8f578dfe..e16a549d9 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -486,7 +486,7 @@ void HWDrawInfo::CreateScene(bool drawpsprites) { double a2 = 20.0 + 0.5*Viewpoint.FieldOfView.Degrees(); // FrustumPitch for vertical clipping if (a2 > 179.0) a2 = 179.0; - double pitchmult = (portalState.PlaneMirrorFlag & 1) ? -1.0 : 1.0; + double pitchmult = !!(portalState.PlaneMirrorFlag & 1) ? -1.0 : 1.0; vClipper->SafeAddClipRangeDegPitches(pitchmult * vp.HWAngles.Pitch.Degrees() - a2, pitchmult * vp.HWAngles.Pitch.Degrees() + a2); // clip the suplex range Viewpoint.PitchSin *= pitchmult; } From 6c4afed10068e0924bcbb3d594e8ce9de9ce05a1 Mon Sep 17 00:00:00 2001 From: MajorCooke Date: Wed, 23 Apr 2025 12:12:06 -0500 Subject: [PATCH 120/384] SpawnBlood now returns an actor spawned by the function. --- src/playsim/p_local.h | 2 +- src/playsim/p_mobj.cpp | 9 +++++---- wadsrc/static/zscript/actors/actor.zs | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/playsim/p_local.h b/src/playsim/p_local.h index 584cfc43f..6575ad3e5 100644 --- a/src/playsim/p_local.h +++ b/src/playsim/p_local.h @@ -106,7 +106,7 @@ enum EPuffFlags }; AActor *P_SpawnPuff(AActor *source, PClassActor *pufftype, const DVector3 &pos, DAngle hitdir, DAngle particledir, int updown, int flags = 0, AActor *vict = NULL); -void P_SpawnBlood (const DVector3 &pos, DAngle angle, int damage, AActor *originator); +AActor *P_SpawnBlood (const DVector3 &pos, DAngle angle, int damage, AActor *originator); void P_BloodSplatter (const DVector3 &pos, AActor *originator, DAngle hitangle); void P_BloodSplatter2 (const DVector3 &pos, AActor *originator, DAngle hitangle); void P_RipperBlood (AActor *mo, AActor *bleeder); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 161230a25..f858a3205 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -6771,9 +6771,9 @@ DEFINE_ACTION_FUNCTION(AActor, SpawnPuff) // //--------------------------------------------------------------------------- -void P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *originator) +AActor *P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *originator) { - AActor *th; + AActor *th = nullptr; PClassActor *bloodcls = originator->GetBloodType(); DVector3 pos = pos1; pos.Z += pr_spawnblood.Random2() / 64.; @@ -6858,6 +6858,8 @@ void P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *origina if (bloodtype >= 1) P_DrawSplash2 (originator->Level, 40, pos, dir, 2, originator->BloodColor); + + return th; } DEFINE_ACTION_FUNCTION(AActor, SpawnBlood) @@ -6868,8 +6870,7 @@ DEFINE_ACTION_FUNCTION(AActor, SpawnBlood) PARAM_FLOAT(z); PARAM_ANGLE(dir); PARAM_INT(damage); - P_SpawnBlood(DVector3(x, y, z), dir, damage, self); - return 0; + ACTION_RETURN_OBJECT(P_SpawnBlood(DVector3(x, y, z), dir, damage, self)); } diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 31b44fa1d..1668b3a8c 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -809,7 +809,7 @@ class Actor : Thinker native native Actor OldSpawnMissile(Actor dest, class type, Actor owner = null); native Actor SpawnPuff(class pufftype, vector3 pos, double hitdir, double particledir, int updown, int flags = 0, Actor victim = null); - native void SpawnBlood (Vector3 pos1, double dir, int damage); + native Actor SpawnBlood (Vector3 pos1, double dir, int damage); native void BloodSplatter (Vector3 pos, double hitangle, bool axe = false); native bool HitWater (sector sec, Vector3 pos, bool checkabove = false, bool alert = true, bool force = false, int flags = 0); native void PlaySpawnSound(Actor missile); From 2a6e354d922bc22535d516aa133fe06ca2ce563a Mon Sep 17 00:00:00 2001 From: MajorCooke Date: Wed, 23 Apr 2025 12:25:55 -0500 Subject: [PATCH 121/384] Replaced the code jumping in P_SpawnBlood with simple boolean checks. --- src/playsim/p_mobj.cpp | 62 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index f858a3205..8c804d55b 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -6780,10 +6780,10 @@ AActor *P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *orig int bloodtype = cl_bloodtype; - if (bloodcls != NULL && !(GetDefaultByType(bloodcls)->flags4 & MF4_ALLOWPARTICLES)) + if (bloodcls != nullptr && !(GetDefaultByType(bloodcls)->flags4 & MF4_ALLOWPARTICLES)) bloodtype = 0; - if (bloodcls != NULL) + if (bloodcls != nullptr) { th = Spawn(originator->Level, bloodcls, pos, NO_REPLACE); // GetBloodType already performed the replacement th->Vel.Z = 2; @@ -6804,6 +6804,7 @@ AActor *P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *orig } // Moved out of the blood actor so that replacing blood is easier + bool foundState = false; if (gameinfo.gametype & GAME_DoomStrifeChex) { if (gameinfo.gametype == GAME_Strife) @@ -6811,48 +6812,53 @@ AActor *P_SpawnBlood (const DVector3 &pos1, DAngle dir, int damage, AActor *orig if (damage > 13) { FState *state = th->FindState(NAME_Spray); - if (state != NULL) + if (state != nullptr) { th->SetState (state); - goto statedone; + foundState = true; } } else damage += 2; } - int advance = 0; - if (damage <= 12 && damage >= 9) + if (!foundState) { - advance = 1; - } - else if (damage < 9) - { - advance = 2; - } - - PClassActor *cls = th->GetClass(); - - while (cls != RUNTIME_CLASS(AActor)) - { - int checked_advance = advance; - if (cls->OwnsState(th->SpawnState)) + int advance = 0; + if (damage <= 12 && damage >= 9) { - for (; checked_advance > 0; --checked_advance) + advance = 1; + } + else if (damage < 9) + { + advance = 2; + } + + PClassActor* cls = th->GetClass(); + bool good = false; + while (cls != RUNTIME_CLASS(AActor)) + { + int checked_advance = advance; + if (cls->OwnsState(th->SpawnState)) { - // [RH] Do not set to a state we do not own. - if (cls->OwnsState(th->SpawnState + checked_advance)) + for (; checked_advance > 0; --checked_advance) { - th->SetState(th->SpawnState + checked_advance); - goto statedone; + // [RH] Do not set to a state we do not own. + if (cls->OwnsState(th->SpawnState + checked_advance)) + { + th->SetState(th->SpawnState + checked_advance); + good = true; + break; + } } + if (good) + break; } + // We can safely assume the ParentClass is of type PClassActor + // since we stop when we see the Actor base class. + cls = static_cast(cls->ParentClass); } - // We can safely assume the ParentClass is of type PClassActor - // since we stop when we see the Actor base class. - cls = static_cast(cls->ParentClass); } } - statedone: if (!(bloodtype <= 1)) th->renderflags |= RF_INVISIBLE; } From 8c66f5c64da1dd3181aeda55b10630a5aca2ab5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 14:20:02 -0300 Subject: [PATCH 122/384] add stb_include --- src/common/rendering/stb_include.h | 295 +++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/common/rendering/stb_include.h diff --git a/src/common/rendering/stb_include.h b/src/common/rendering/stb_include.h new file mode 100644 index 000000000..c5db20138 --- /dev/null +++ b/src/common/rendering/stb_include.h @@ -0,0 +1,295 @@ +// stb_include.h - v0.02 - parse and process #include directives - public domain +// +// To build this, in one source file that includes this file do +// #define STB_INCLUDE_IMPLEMENTATION +// +// This program parses a string and replaces lines of the form +// #include "foo" +// with the contents of a file named "foo". It also embeds the +// appropriate #line directives. Note that all include files must +// reside in the location specified in the path passed to the API; +// it does not check multiple directories. +// +// If the string contains a line of the form +// #inject +// then it will be replaced with the contents of the string 'inject' passed to the API. +// +// Options: +// +// Define STB_INCLUDE_LINE_GLSL to get GLSL-style #line directives +// which use numbers instead of filenames. +// +// Define STB_INCLUDE_LINE_NONE to disable output of #line directives. +// +// Standard libraries: +// +// stdio.h FILE, fopen, fclose, fseek, ftell +// stdlib.h malloc, realloc, free +// string.h strcpy, strncmp, memcpy +// +// Credits: +// +// Written by Sean Barrett. +// +// Fixes: +// Michal Klos + +#ifndef STB_INCLUDE_STB_INCLUDE_H +#define STB_INCLUDE_STB_INCLUDE_H + +// Do include-processing on the string 'str'. To free the return value, pass it to free() +char *stb_include_string(char *str, char *inject, char *path_to_includes, char *filename_for_line_directive, char error[256]); + +// Concatenate the strings 'strs' and do include-processing on the result. To free the return value, pass it to free() +char *stb_include_strings(char **strs, int count, char *inject, char *path_to_includes, char *filename_for_line_directive, char error[256]); + +// Load the file 'filename' and do include-processing on the string therein. note that +// 'filename' is opened directly; 'path_to_includes' is not used. To free the return value, pass it to free() +char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]); + +#endif + + +#ifdef STB_INCLUDE_IMPLEMENTATION + +#include +#include +#include + +static char *stb_include_load_file(char *filename, size_t *plen) +{ + char *text; + size_t len; + FILE *f = fopen(filename, "rb"); + if (f == 0) return 0; + fseek(f, 0, SEEK_END); + len = (size_t) ftell(f); + if (plen) *plen = len; + text = (char *) malloc(len+1); + if (text == 0) return 0; + fseek(f, 0, SEEK_SET); + fread(text, 1, len, f); + fclose(f); + text[len] = 0; + return text; +} + +typedef struct +{ + int offset; + int end; + char *filename; + int next_line_after; +} include_info; + +static include_info *stb_include_append_include(include_info *array, int len, int offset, int end, char *filename, int next_line) +{ + include_info *z = (include_info *) realloc(array, sizeof(*z) * (len+1)); + z[len].offset = offset; + z[len].end = end; + z[len].filename = filename; + z[len].next_line_after = next_line; + return z; +} + +static void stb_include_free_includes(include_info *array, int len) +{ + int i; + for (i=0; i < len; ++i) + free(array[i].filename); + free(array); +} + +static int stb_include_isspace(int ch) +{ + return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); +} + +// find location of all #include and #inject +static int stb_include_find_includes(char *text, include_info **plist) +{ + int line_count = 1; + int inc_count = 0; + char *s = text, *start; + include_info *list = NULL; + while (*s) { + // parse is always at start of line when we reach here + start = s; + while (*s == ' ' || *s == '\t') + ++s; + if (*s == '#') { + ++s; + while (*s == ' ' || *s == '\t') + ++s; + if (0==strncmp(s, "include", 7) && stb_include_isspace(s[7])) { + s += 7; + while (*s == ' ' || *s == '\t') + ++s; + if (*s == '"') { + char *t = ++s; + while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) + ++t; + if (*t == '"') { + char *filename = (char *) malloc(t-s+1); + memcpy(filename, s, t-s); + filename[t-s] = 0; + s=t; + while (*s != '\r' && *s != '\n' && *s != 0) + ++s; + // s points to the newline, so s-start is everything except the newline + list = stb_include_append_include(list, inc_count++, start-text, s-text, filename, line_count+1); + } + } + } else if (0==strncmp(s, "inject", 6) && (stb_include_isspace(s[6]) || s[6]==0)) { + while (*s != '\r' && *s != '\n' && *s != 0) + ++s; + list = stb_include_append_include(list, inc_count++, start-text, s-text, NULL, line_count+1); + } + } + while (*s != '\r' && *s != '\n' && *s != 0) + ++s; + if (*s == '\r' || *s == '\n') { + s = s + (s[0] + s[1] == '\r' + '\n' ? 2 : 1); + } + ++line_count; + } + *plist = list; + return inc_count; +} + +// avoid dependency on sprintf() +static void stb_include_itoa(char str[9], int n) +{ + int i; + for (i=0; i < 8; ++i) + str[i] = ' '; + str[i] = 0; + + for (i=1; i < 8; ++i) { + str[7-i] = '0' + (n % 10); + n /= 10; + if (n == 0) + break; + } +} + +static char *stb_include_append(char *str, size_t *curlen, char *addstr, size_t addlen) +{ + str = (char *) realloc(str, *curlen + addlen); + memcpy(str + *curlen, addstr, addlen); + *curlen += addlen; + return str; +} + +char *stb_include_string(char *str, char *inject, char *path_to_includes, char *filename, char error[256]) +{ + char temp[4096]; + include_info *inc_list; + int i, num = stb_include_find_includes(str, &inc_list); + size_t source_len = strlen(str); + char *text=0; + size_t textlen=0, last=0; + for (i=0; i < num; ++i) { + text = stb_include_append(text, &textlen, str+last, inc_list[i].offset - last); + // write out line directive for the include + #ifndef STB_INCLUDE_LINE_NONE + #ifdef STB_INCLUDE_LINE_GLSL + if (textlen != 0) // GLSL #version must appear first, so don't put a #line at the top + #endif + { + strcpy(temp, "#line "); + stb_include_itoa(temp+6, 1); + strcat(temp, " "); + #ifdef STB_INCLUDE_LINE_GLSL + stb_include_itoa(temp+15, i+1); + #else + strcat(temp, "\""); + if (inc_list[i].filename == 0) + strcmp(temp, "INJECT"); + else + strcat(temp, inc_list[i].filename); + strcat(temp, "\""); + #endif + strcat(temp, "\n"); + text = stb_include_append(text, &textlen, temp, strlen(temp)); + } + #endif + if (inc_list[i].filename == 0) { + if (inject != 0) + text = stb_include_append(text, &textlen, inject, strlen(inject)); + } else { + char *inc; + strcpy(temp, path_to_includes); + strcat(temp, "/"); + strcat(temp, inc_list[i].filename); + inc = stb_include_file(temp, inject, path_to_includes, error); + if (inc == NULL) { + stb_include_free_includes(inc_list, num); + return NULL; + } + text = stb_include_append(text, &textlen, inc, strlen(inc)); + free(inc); + } + // write out line directive + #ifndef STB_INCLUDE_LINE_NONE + strcpy(temp, "\n#line "); + stb_include_itoa(temp+6, inc_list[i].next_line_after); + strcat(temp, " "); + #ifdef STB_INCLUDE_LINE_GLSL + stb_include_itoa(temp+15, 0); + #else + strcat(temp, filename != 0 ? filename : "source-file"); + #endif + text = stb_include_append(text, &textlen, temp, strlen(temp)); + // no newlines, because we kept the #include newlines, which will get appended next + #endif + last = inc_list[i].end; + } + text = stb_include_append(text, &textlen, str+last, source_len - last + 1); // append '\0' + stb_include_free_includes(inc_list, num); + return text; +} + +char *stb_include_strings(char **strs, int count, char *inject, char *path_to_includes, char *filename, char error[256]) +{ + char *text; + char *result; + int i; + size_t length=0; + for (i=0; i < count; ++i) + length += strlen(strs[i]); + text = (char *) malloc(length+1); + length = 0; + for (i=0; i < count; ++i) { + strcpy(text + length, strs[i]); + length += strlen(strs[i]); + } + result = stb_include_string(text, inject, path_to_includes, filename, error); + free(text); + return result; +} + +char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]) +{ + size_t len; + char *result; + char *text = stb_include_load_file(filename, &len); + if (text == NULL) { + strcpy(error, "Error: couldn't load '"); + strcat(error, filename); + strcat(error, "'"); + return 0; + } + result = stb_include_string(text, inject, path_to_includes, filename, error); + free(text); + return result; +} + +#if 0 // @TODO, GL_ARB_shader_language_include-style system that doesn't touch filesystem +char *stb_include_preloaded(char *str, char *inject, char *includes[][2], char error[256]) +{ + +} +#endif + +#endif // STB_INCLUDE_IMPLEMENTATION From 817a3778d833face333e8b755e6e9d9211dee414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 14:23:45 -0300 Subject: [PATCH 123/384] split stb_include into .cpp and .h --- src/CMakeLists.txt | 1 + src/common/rendering/stb_include.cpp | 270 +++++++++++++++++++++++++++ src/common/rendering/stb_include.h | 245 ------------------------ 3 files changed, 271 insertions(+), 245 deletions(-) create mode 100644 src/common/rendering/stb_include.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 95b67a2d8..ddeba6029 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1124,6 +1124,7 @@ set (PCH_SOURCES common/rendering/v_video.cpp common/rendering/r_thread.cpp common/rendering/r_videoscale.cpp + common/rendering/stb_include.cpp common/rendering/hwrenderer/hw_draw2d.cpp common/rendering/hwrenderer/data/hw_clock.cpp common/rendering/hwrenderer/data/hw_skydome.cpp diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp new file mode 100644 index 000000000..a7dbcc9eb --- /dev/null +++ b/src/common/rendering/stb_include.cpp @@ -0,0 +1,270 @@ +// stb_include.h - v0.02 - parse and process #include directives - public domain +// +// To build this, in one source file that includes this file do +// #define STB_INCLUDE_IMPLEMENTATION +// +// This program parses a string and replaces lines of the form +// #include "foo" +// with the contents of a file named "foo". It also embeds the +// appropriate #line directives. Note that all include files must +// reside in the location specified in the path passed to the API; +// it does not check multiple directories. +// +// If the string contains a line of the form +// #inject +// then it will be replaced with the contents of the string 'inject' passed to the API. +// +// Options: +// +// Define STB_INCLUDE_LINE_GLSL to get GLSL-style #line directives +// which use numbers instead of filenames. +// +// Define STB_INCLUDE_LINE_NONE to disable output of #line directives. +// +// Standard libraries: +// +// stdio.h FILE, fopen, fclose, fseek, ftell +// stdlib.h malloc, realloc, free +// string.h strcpy, strncmp, memcpy +// +// Credits: +// +// Written by Sean Barrett. +// +// Fixes: +// Michal Klos + +#include "stb_include.h" + +#include +#include +#include + +static char *stb_include_load_file(char *filename, size_t *plen) +{ + char *text; + size_t len; + FILE *f = fopen(filename, "rb"); + if (f == 0) return 0; + fseek(f, 0, SEEK_END); + len = (size_t) ftell(f); + if (plen) *plen = len; + text = (char *) malloc(len+1); + if (text == 0) return 0; + fseek(f, 0, SEEK_SET); + fread(text, 1, len, f); + fclose(f); + text[len] = 0; + return text; +} + +typedef struct +{ + int offset; + int end; + char *filename; + int next_line_after; +} include_info; + +static include_info *stb_include_append_include(include_info *array, int len, int offset, int end, char *filename, int next_line) +{ + include_info *z = (include_info *) realloc(array, sizeof(*z) * (len+1)); + z[len].offset = offset; + z[len].end = end; + z[len].filename = filename; + z[len].next_line_after = next_line; + return z; +} + +static void stb_include_free_includes(include_info *array, int len) +{ + int i; + for (i=0; i < len; ++i) + free(array[i].filename); + free(array); +} + +static int stb_include_isspace(int ch) +{ + return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); +} + +// find location of all #include and #inject +static int stb_include_find_includes(char *text, include_info **plist) +{ + int line_count = 1; + int inc_count = 0; + char *s = text, *start; + include_info *list = NULL; + while (*s) { + // parse is always at start of line when we reach here + start = s; + while (*s == ' ' || *s == '\t') + ++s; + if (*s == '#') { + ++s; + while (*s == ' ' || *s == '\t') + ++s; + if (0==strncmp(s, "include", 7) && stb_include_isspace(s[7])) { + s += 7; + while (*s == ' ' || *s == '\t') + ++s; + if (*s == '"') { + char *t = ++s; + while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) + ++t; + if (*t == '"') { + char *filename = (char *) malloc(t-s+1); + memcpy(filename, s, t-s); + filename[t-s] = 0; + s=t; + while (*s != '\r' && *s != '\n' && *s != 0) + ++s; + // s points to the newline, so s-start is everything except the newline + list = stb_include_append_include(list, inc_count++, start-text, s-text, filename, line_count+1); + } + } + } else if (0==strncmp(s, "inject", 6) && (stb_include_isspace(s[6]) || s[6]==0)) { + while (*s != '\r' && *s != '\n' && *s != 0) + ++s; + list = stb_include_append_include(list, inc_count++, start-text, s-text, NULL, line_count+1); + } + } + while (*s != '\r' && *s != '\n' && *s != 0) + ++s; + if (*s == '\r' || *s == '\n') { + s = s + (s[0] + s[1] == '\r' + '\n' ? 2 : 1); + } + ++line_count; + } + *plist = list; + return inc_count; +} + +// avoid dependency on sprintf() +static void stb_include_itoa(char str[9], int n) +{ + int i; + for (i=0; i < 8; ++i) + str[i] = ' '; + str[i] = 0; + + for (i=1; i < 8; ++i) { + str[7-i] = '0' + (n % 10); + n /= 10; + if (n == 0) + break; + } +} + +static char *stb_include_append(char *str, size_t *curlen, char *addstr, size_t addlen) +{ + str = (char *) realloc(str, *curlen + addlen); + memcpy(str + *curlen, addstr, addlen); + *curlen += addlen; + return str; +} + +char *stb_include_string(char *str, char *inject, char *path_to_includes, char *filename, char error[256]) +{ + char temp[4096]; + include_info *inc_list; + int i, num = stb_include_find_includes(str, &inc_list); + size_t source_len = strlen(str); + char *text=0; + size_t textlen=0, last=0; + for (i=0; i < num; ++i) { + text = stb_include_append(text, &textlen, str+last, inc_list[i].offset - last); + // write out line directive for the include + #ifndef STB_INCLUDE_LINE_NONE + #ifdef STB_INCLUDE_LINE_GLSL + if (textlen != 0) // GLSL #version must appear first, so don't put a #line at the top + #endif + { + strcpy(temp, "#line "); + stb_include_itoa(temp+6, 1); + strcat(temp, " "); + #ifdef STB_INCLUDE_LINE_GLSL + stb_include_itoa(temp+15, i+1); + #else + strcat(temp, "\""); + if (inc_list[i].filename == 0) + strcmp(temp, "INJECT"); + else + strcat(temp, inc_list[i].filename); + strcat(temp, "\""); + #endif + strcat(temp, "\n"); + text = stb_include_append(text, &textlen, temp, strlen(temp)); + } + #endif + if (inc_list[i].filename == 0) { + if (inject != 0) + text = stb_include_append(text, &textlen, inject, strlen(inject)); + } else { + char *inc; + strcpy(temp, path_to_includes); + strcat(temp, "/"); + strcat(temp, inc_list[i].filename); + inc = stb_include_file(temp, inject, path_to_includes, error); + if (inc == NULL) { + stb_include_free_includes(inc_list, num); + return NULL; + } + text = stb_include_append(text, &textlen, inc, strlen(inc)); + free(inc); + } + // write out line directive + #ifndef STB_INCLUDE_LINE_NONE + strcpy(temp, "\n#line "); + stb_include_itoa(temp+6, inc_list[i].next_line_after); + strcat(temp, " "); + #ifdef STB_INCLUDE_LINE_GLSL + stb_include_itoa(temp+15, 0); + #else + strcat(temp, filename != 0 ? filename : "source-file"); + #endif + text = stb_include_append(text, &textlen, temp, strlen(temp)); + // no newlines, because we kept the #include newlines, which will get appended next + #endif + last = inc_list[i].end; + } + text = stb_include_append(text, &textlen, str+last, source_len - last + 1); // append '\0' + stb_include_free_includes(inc_list, num); + return text; +} + +char *stb_include_strings(char **strs, int count, char *inject, char *path_to_includes, char *filename, char error[256]) +{ + char *text; + char *result; + int i; + size_t length=0; + for (i=0; i < count; ++i) + length += strlen(strs[i]); + text = (char *) malloc(length+1); + length = 0; + for (i=0; i < count; ++i) { + strcpy(text + length, strs[i]); + length += strlen(strs[i]); + } + result = stb_include_string(text, inject, path_to_includes, filename, error); + free(text); + return result; +} + +char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]) +{ + size_t len; + char *result; + char *text = stb_include_load_file(filename, &len); + if (text == NULL) { + strcpy(error, "Error: couldn't load '"); + strcat(error, filename); + strcat(error, "'"); + return 0; + } + result = stb_include_string(text, inject, path_to_includes, filename, error); + free(text); + return result; +} diff --git a/src/common/rendering/stb_include.h b/src/common/rendering/stb_include.h index c5db20138..ab7e21d1e 100644 --- a/src/common/rendering/stb_include.h +++ b/src/common/rendering/stb_include.h @@ -48,248 +48,3 @@ char *stb_include_strings(char **strs, int count, char *inject, char *path_to_in char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]); #endif - - -#ifdef STB_INCLUDE_IMPLEMENTATION - -#include -#include -#include - -static char *stb_include_load_file(char *filename, size_t *plen) -{ - char *text; - size_t len; - FILE *f = fopen(filename, "rb"); - if (f == 0) return 0; - fseek(f, 0, SEEK_END); - len = (size_t) ftell(f); - if (plen) *plen = len; - text = (char *) malloc(len+1); - if (text == 0) return 0; - fseek(f, 0, SEEK_SET); - fread(text, 1, len, f); - fclose(f); - text[len] = 0; - return text; -} - -typedef struct -{ - int offset; - int end; - char *filename; - int next_line_after; -} include_info; - -static include_info *stb_include_append_include(include_info *array, int len, int offset, int end, char *filename, int next_line) -{ - include_info *z = (include_info *) realloc(array, sizeof(*z) * (len+1)); - z[len].offset = offset; - z[len].end = end; - z[len].filename = filename; - z[len].next_line_after = next_line; - return z; -} - -static void stb_include_free_includes(include_info *array, int len) -{ - int i; - for (i=0; i < len; ++i) - free(array[i].filename); - free(array); -} - -static int stb_include_isspace(int ch) -{ - return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); -} - -// find location of all #include and #inject -static int stb_include_find_includes(char *text, include_info **plist) -{ - int line_count = 1; - int inc_count = 0; - char *s = text, *start; - include_info *list = NULL; - while (*s) { - // parse is always at start of line when we reach here - start = s; - while (*s == ' ' || *s == '\t') - ++s; - if (*s == '#') { - ++s; - while (*s == ' ' || *s == '\t') - ++s; - if (0==strncmp(s, "include", 7) && stb_include_isspace(s[7])) { - s += 7; - while (*s == ' ' || *s == '\t') - ++s; - if (*s == '"') { - char *t = ++s; - while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) - ++t; - if (*t == '"') { - char *filename = (char *) malloc(t-s+1); - memcpy(filename, s, t-s); - filename[t-s] = 0; - s=t; - while (*s != '\r' && *s != '\n' && *s != 0) - ++s; - // s points to the newline, so s-start is everything except the newline - list = stb_include_append_include(list, inc_count++, start-text, s-text, filename, line_count+1); - } - } - } else if (0==strncmp(s, "inject", 6) && (stb_include_isspace(s[6]) || s[6]==0)) { - while (*s != '\r' && *s != '\n' && *s != 0) - ++s; - list = stb_include_append_include(list, inc_count++, start-text, s-text, NULL, line_count+1); - } - } - while (*s != '\r' && *s != '\n' && *s != 0) - ++s; - if (*s == '\r' || *s == '\n') { - s = s + (s[0] + s[1] == '\r' + '\n' ? 2 : 1); - } - ++line_count; - } - *plist = list; - return inc_count; -} - -// avoid dependency on sprintf() -static void stb_include_itoa(char str[9], int n) -{ - int i; - for (i=0; i < 8; ++i) - str[i] = ' '; - str[i] = 0; - - for (i=1; i < 8; ++i) { - str[7-i] = '0' + (n % 10); - n /= 10; - if (n == 0) - break; - } -} - -static char *stb_include_append(char *str, size_t *curlen, char *addstr, size_t addlen) -{ - str = (char *) realloc(str, *curlen + addlen); - memcpy(str + *curlen, addstr, addlen); - *curlen += addlen; - return str; -} - -char *stb_include_string(char *str, char *inject, char *path_to_includes, char *filename, char error[256]) -{ - char temp[4096]; - include_info *inc_list; - int i, num = stb_include_find_includes(str, &inc_list); - size_t source_len = strlen(str); - char *text=0; - size_t textlen=0, last=0; - for (i=0; i < num; ++i) { - text = stb_include_append(text, &textlen, str+last, inc_list[i].offset - last); - // write out line directive for the include - #ifndef STB_INCLUDE_LINE_NONE - #ifdef STB_INCLUDE_LINE_GLSL - if (textlen != 0) // GLSL #version must appear first, so don't put a #line at the top - #endif - { - strcpy(temp, "#line "); - stb_include_itoa(temp+6, 1); - strcat(temp, " "); - #ifdef STB_INCLUDE_LINE_GLSL - stb_include_itoa(temp+15, i+1); - #else - strcat(temp, "\""); - if (inc_list[i].filename == 0) - strcmp(temp, "INJECT"); - else - strcat(temp, inc_list[i].filename); - strcat(temp, "\""); - #endif - strcat(temp, "\n"); - text = stb_include_append(text, &textlen, temp, strlen(temp)); - } - #endif - if (inc_list[i].filename == 0) { - if (inject != 0) - text = stb_include_append(text, &textlen, inject, strlen(inject)); - } else { - char *inc; - strcpy(temp, path_to_includes); - strcat(temp, "/"); - strcat(temp, inc_list[i].filename); - inc = stb_include_file(temp, inject, path_to_includes, error); - if (inc == NULL) { - stb_include_free_includes(inc_list, num); - return NULL; - } - text = stb_include_append(text, &textlen, inc, strlen(inc)); - free(inc); - } - // write out line directive - #ifndef STB_INCLUDE_LINE_NONE - strcpy(temp, "\n#line "); - stb_include_itoa(temp+6, inc_list[i].next_line_after); - strcat(temp, " "); - #ifdef STB_INCLUDE_LINE_GLSL - stb_include_itoa(temp+15, 0); - #else - strcat(temp, filename != 0 ? filename : "source-file"); - #endif - text = stb_include_append(text, &textlen, temp, strlen(temp)); - // no newlines, because we kept the #include newlines, which will get appended next - #endif - last = inc_list[i].end; - } - text = stb_include_append(text, &textlen, str+last, source_len - last + 1); // append '\0' - stb_include_free_includes(inc_list, num); - return text; -} - -char *stb_include_strings(char **strs, int count, char *inject, char *path_to_includes, char *filename, char error[256]) -{ - char *text; - char *result; - int i; - size_t length=0; - for (i=0; i < count; ++i) - length += strlen(strs[i]); - text = (char *) malloc(length+1); - length = 0; - for (i=0; i < count; ++i) { - strcpy(text + length, strs[i]); - length += strlen(strs[i]); - } - result = stb_include_string(text, inject, path_to_includes, filename, error); - free(text); - return result; -} - -char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]) -{ - size_t len; - char *result; - char *text = stb_include_load_file(filename, &len); - if (text == NULL) { - strcpy(error, "Error: couldn't load '"); - strcat(error, filename); - strcat(error, "'"); - return 0; - } - result = stb_include_string(text, inject, path_to_includes, filename, error); - free(text); - return result; -} - -#if 0 // @TODO, GL_ARB_shader_language_include-style system that doesn't touch filesystem -char *stb_include_preloaded(char *str, char *inject, char *includes[][2], char error[256]) -{ - -} -#endif - -#endif // STB_INCLUDE_IMPLEMENTATION From d23faa6ab845ecce20d1e84ed373a71c2111d35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 15:03:32 -0300 Subject: [PATCH 124/384] convert stb_include to FString, trim out some stuff --- src/common/rendering/stb_include.cpp | 214 +++++++++------------------ src/common/rendering/stb_include.h | 9 +- 2 files changed, 75 insertions(+), 148 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index a7dbcc9eb..0179adcfc 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -35,53 +35,32 @@ // Michal Klos #include "stb_include.h" +#include "filesystem.h" +#include "cmdlib.h" #include #include #include -static char *stb_include_load_file(char *filename, size_t *plen) +static bool stb_include_load_file(FString filename, FString &out) { - char *text; - size_t len; - FILE *f = fopen(filename, "rb"); - if (f == 0) return 0; - fseek(f, 0, SEEK_END); - len = (size_t) ftell(f); - if (plen) *plen = len; - text = (char *) malloc(len+1); - if (text == 0) return 0; - fseek(f, 0, SEEK_SET); - fread(text, 1, len, f); - fclose(f); - text[len] = 0; - return text; + int f = fileSystem.FindFile(filename.GetChars()); + if(f < 0) return false; + out = GetStringFromLump(f, false); + return true; } typedef struct { int offset; int end; - char *filename; + FString filename; int next_line_after; } include_info; -static include_info *stb_include_append_include(include_info *array, int len, int offset, int end, char *filename, int next_line) +static void stb_include_append_include(TArray &array, int offset, int end, FString filename, int next_line) { - include_info *z = (include_info *) realloc(array, sizeof(*z) * (len+1)); - z[len].offset = offset; - z[len].end = end; - z[len].filename = filename; - z[len].next_line_after = next_line; - return z; -} - -static void stb_include_free_includes(include_info *array, int len) -{ - int i; - for (i=0; i < len; ++i) - free(array[i].filename); - free(array); + array.Push({offset, end, filename, next_line}); } static int stb_include_isspace(int ch) @@ -90,12 +69,13 @@ static int stb_include_isspace(int ch) } // find location of all #include and #inject -static int stb_include_find_includes(char *text, include_info **plist) +static int stb_include_find_includes(const char *text, TArray &plist) { int line_count = 1; int inc_count = 0; - char *s = text, *start; - include_info *list = NULL; + const char *s = text, *start; + + TArray list; while (*s) { // parse is always at start of line when we reach here start = s; @@ -110,24 +90,20 @@ static int stb_include_find_includes(char *text, include_info **plist) while (*s == ' ' || *s == '\t') ++s; if (*s == '"') { - char *t = ++s; + const char *t = ++s; while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) ++t; if (*t == '"') { - char *filename = (char *) malloc(t-s+1); - memcpy(filename, s, t-s); - filename[t-s] = 0; + FString filename; + filename.AppendCStrPart(s, t-s); s=t; while (*s != '\r' && *s != '\n' && *s != 0) ++s; // s points to the newline, so s-start is everything except the newline - list = stb_include_append_include(list, inc_count++, start-text, s-text, filename, line_count+1); + stb_include_append_include(list, start-text, s-text, filename, line_count+1); + inc_count++; } } - } else if (0==strncmp(s, "inject", 6) && (stb_include_isspace(s[6]) || s[6]==0)) { - while (*s != '\r' && *s != '\n' && *s != 0) - ++s; - list = stb_include_append_include(list, inc_count++, start-text, s-text, NULL, line_count+1); } } while (*s != '\r' && *s != '\n' && *s != 0) @@ -137,7 +113,9 @@ static int stb_include_find_includes(char *text, include_info **plist) } ++line_count; } - *plist = list; + + plist = std::move(list); + return inc_count; } @@ -157,114 +135,64 @@ static void stb_include_itoa(char str[9], int n) } } -static char *stb_include_append(char *str, size_t *curlen, char *addstr, size_t addlen) +static void stb_include_append(FString &str, FString &addstr) { - str = (char *) realloc(str, *curlen + addlen); - memcpy(str + *curlen, addstr, addlen); - *curlen += addlen; - return str; + str += addstr; } -char *stb_include_string(char *str, char *inject, char *path_to_includes, char *filename, char error[256]) +static void stb_include_append(FString &str, const char * addstr, size_t addlen) { - char temp[4096]; - include_info *inc_list; - int i, num = stb_include_find_includes(str, &inc_list); - size_t source_len = strlen(str); - char *text=0; - size_t textlen=0, last=0; - for (i=0; i < num; ++i) { - text = stb_include_append(text, &textlen, str+last, inc_list[i].offset - last); - // write out line directive for the include - #ifndef STB_INCLUDE_LINE_NONE - #ifdef STB_INCLUDE_LINE_GLSL - if (textlen != 0) // GLSL #version must appear first, so don't put a #line at the top - #endif - { - strcpy(temp, "#line "); - stb_include_itoa(temp+6, 1); - strcat(temp, " "); - #ifdef STB_INCLUDE_LINE_GLSL - stb_include_itoa(temp+15, i+1); - #else - strcat(temp, "\""); - if (inc_list[i].filename == 0) - strcmp(temp, "INJECT"); - else - strcat(temp, inc_list[i].filename); - strcat(temp, "\""); - #endif - strcat(temp, "\n"); - text = stb_include_append(text, &textlen, temp, strlen(temp)); - } - #endif - if (inc_list[i].filename == 0) { - if (inject != 0) - text = stb_include_append(text, &textlen, inject, strlen(inject)); - } else { - char *inc; - strcpy(temp, path_to_includes); - strcat(temp, "/"); - strcat(temp, inc_list[i].filename); - inc = stb_include_file(temp, inject, path_to_includes, error); - if (inc == NULL) { - stb_include_free_includes(inc_list, num); - return NULL; - } - text = stb_include_append(text, &textlen, inc, strlen(inc)); - free(inc); - } - // write out line directive - #ifndef STB_INCLUDE_LINE_NONE - strcpy(temp, "\n#line "); - stb_include_itoa(temp+6, inc_list[i].next_line_after); - strcat(temp, " "); - #ifdef STB_INCLUDE_LINE_GLSL - stb_include_itoa(temp+15, 0); - #else - strcat(temp, filename != 0 ? filename : "source-file"); - #endif - text = stb_include_append(text, &textlen, temp, strlen(temp)); - // no newlines, because we kept the #include newlines, which will get appended next - #endif - last = inc_list[i].end; - } - text = stb_include_append(text, &textlen, str+last, source_len - last + 1); // append '\0' - stb_include_free_includes(inc_list, num); - return text; + str.AppendCStrPart(addstr, addlen); } -char *stb_include_strings(char **strs, int count, char *inject, char *path_to_includes, char *filename, char error[256]) +FString stb_include_string(FString str, FString &error) { - char *text; - char *result; - int i; - size_t length=0; - for (i=0; i < count; ++i) - length += strlen(strs[i]); - text = (char *) malloc(length+1); - length = 0; - for (i=0; i < count; ++i) { - strcpy(text + length, strs[i]); - length += strlen(strs[i]); - } - result = stb_include_string(text, inject, path_to_includes, filename, error); - free(text); - return result; + error = ""; + char temp[4096]; + TArray inc_list; + int i, num = stb_include_find_includes(str.GetChars(), inc_list); + size_t source_len = str.Len(); + FString text; + size_t last = 0; + for (i=0; i < num; ++i) + { + stb_include_append(text, str.GetChars() + last, inc_list[i].offset - last); + // write out line directive for the include + strcpy(temp, "#line "); + stb_include_itoa(temp+6, 1); + strcat(temp, " "); + stb_include_itoa(temp+15, i+1); + strcat(temp, "\n"); + stb_include_append(text, temp, strlen(temp)); + + FString inc = stb_include_file(inc_list[i].filename.GetChars(), error); + if (!error.IsEmpty()) + { + return ""; + } + stb_include_append(text, inc); + + // write out line directive + strcpy(temp, "\n#line "); + stb_include_itoa(temp+6, inc_list[i].next_line_after); + strcat(temp, " "); + stb_include_itoa(temp+15, 0); + stb_include_append(text, temp, strlen(temp)); + // no newlines, because we kept the #include newlines, which will get appended next + last = inc_list[i].end; + } + return text; } -char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]) +FString stb_include_file(FString filename, FString &error) { - size_t len; - char *result; - char *text = stb_include_load_file(filename, &len); - if (text == NULL) { - strcpy(error, "Error: couldn't load '"); - strcat(error, filename); - strcat(error, "'"); - return 0; + FString text; + if (!stb_include_load_file(filename, text)) + { + error += "Error: couldn't load '"; + error += filename; + error += "'"; + return ""; } - result = stb_include_string(text, inject, path_to_includes, filename, error); - free(text); - return result; + return stb_include_string(text, error); } diff --git a/src/common/rendering/stb_include.h b/src/common/rendering/stb_include.h index ab7e21d1e..357e4137f 100644 --- a/src/common/rendering/stb_include.h +++ b/src/common/rendering/stb_include.h @@ -37,14 +37,13 @@ #ifndef STB_INCLUDE_STB_INCLUDE_H #define STB_INCLUDE_STB_INCLUDE_H -// Do include-processing on the string 'str'. To free the return value, pass it to free() -char *stb_include_string(char *str, char *inject, char *path_to_includes, char *filename_for_line_directive, char error[256]); +#include "zstring.h" -// Concatenate the strings 'strs' and do include-processing on the result. To free the return value, pass it to free() -char *stb_include_strings(char **strs, int count, char *inject, char *path_to_includes, char *filename_for_line_directive, char error[256]); +// Do include-processing on the string 'str'. To free the return value, pass it to free() +FString stb_include_string(FString str, FString &error); // Load the file 'filename' and do include-processing on the string therein. note that // 'filename' is opened directly; 'path_to_includes' is not used. To free the return value, pass it to free() -char *stb_include_file(char *filename, char *inject, char *path_to_includes, char error[256]); +FString stb_include_file(FString filename, FString &error); #endif From 82e5dc2db62b9b4e6c95b658a4a172c5167ecfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 15:11:34 -0300 Subject: [PATCH 125/384] remove raw C string manipulation, add missing chunk --- src/common/rendering/stb_include.cpp | 79 +++++++--------------------- 1 file changed, 19 insertions(+), 60 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 0179adcfc..65ff0cd98 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -41,6 +41,7 @@ #include #include #include +#include static bool stb_include_load_file(FString filename, FString &out) { @@ -52,27 +53,22 @@ static bool stb_include_load_file(FString filename, FString &out) typedef struct { - int offset; - int end; - FString filename; - int next_line_after; + int64_t offset; + int64_t end; + FString filename; + int64_t next_line_after; } include_info; -static void stb_include_append_include(TArray &array, int offset, int end, FString filename, int next_line) -{ - array.Push({offset, end, filename, next_line}); -} - -static int stb_include_isspace(int ch) +static bool stb_include_isspace(int ch) { return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); } // find location of all #include and #inject -static int stb_include_find_includes(const char *text, TArray &plist) +static int64_t stb_include_find_includes(const char *text, TArray &plist) { - int line_count = 1; - int inc_count = 0; + int64_t line_count = 1; + int64_t inc_count = 0; const char *s = text, *start; TArray list; @@ -100,7 +96,7 @@ static int stb_include_find_includes(const char *text, TArray &pli while (*s != '\r' && *s != '\n' && *s != 0) ++s; // s points to the newline, so s-start is everything except the newline - stb_include_append_include(list, start-text, s-text, filename, line_count+1); + list.Push({start-text, s-text, filename, line_count+1}); inc_count++; } } @@ -119,68 +115,31 @@ static int stb_include_find_includes(const char *text, TArray &pli return inc_count; } -// avoid dependency on sprintf() -static void stb_include_itoa(char str[9], int n) -{ - int i; - for (i=0; i < 8; ++i) - str[i] = ' '; - str[i] = 0; - - for (i=1; i < 8; ++i) { - str[7-i] = '0' + (n % 10); - n /= 10; - if (n == 0) - break; - } -} - -static void stb_include_append(FString &str, FString &addstr) -{ - str += addstr; -} - -static void stb_include_append(FString &str, const char * addstr, size_t addlen) -{ - str.AppendCStrPart(addstr, addlen); -} - FString stb_include_string(FString str, FString &error) { error = ""; - char temp[4096]; TArray inc_list; - int i, num = stb_include_find_includes(str.GetChars(), inc_list); - size_t source_len = str.Len(); - FString text; + int64_t num = stb_include_find_includes(str.GetChars(), inc_list); + FString text = ""; size_t last = 0; - for (i=0; i < num; ++i) + for (int64_t i = 0; i < num; ++i) { - stb_include_append(text, str.GetChars() + last, inc_list[i].offset - last); - // write out line directive for the include - strcpy(temp, "#line "); - stb_include_itoa(temp+6, 1); - strcat(temp, " "); - stb_include_itoa(temp+15, i+1); - strcat(temp, "\n"); - stb_include_append(text, temp, strlen(temp)); + text.AppendCStrPart(str.GetChars() + last, inc_list[i].offset - last); + + text.AppendFormat("#line 1 %d\n", i + 1); FString inc = stb_include_file(inc_list[i].filename.GetChars(), error); if (!error.IsEmpty()) { return ""; } - stb_include_append(text, inc); + text += inc; - // write out line directive - strcpy(temp, "\n#line "); - stb_include_itoa(temp+6, inc_list[i].next_line_after); - strcat(temp, " "); - stb_include_itoa(temp+15, 0); - stb_include_append(text, temp, strlen(temp)); + text.AppendFormat("#line %d 0\n", inc_list[i].next_line_after); // no newlines, because we kept the #include newlines, which will get appended next last = inc_list[i].end; } + text.AppendCStrPart(str.GetChars() + last, str.Len() - last); return text; } From 3f65c2191a1c5c04376f9836a2fd3281bec604cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 15:15:07 -0300 Subject: [PATCH 126/384] better formatting --- src/common/rendering/stb_include.cpp | 104 ++++++++++++++++----------- 1 file changed, 61 insertions(+), 43 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 65ff0cd98..165f9a158 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -51,68 +51,86 @@ static bool stb_include_load_file(FString filename, FString &out) return true; } -typedef struct +struct include_info { int64_t offset; int64_t end; FString filename; int64_t next_line_after; -} include_info; +}; static bool stb_include_isspace(int ch) { - return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); + return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); } // find location of all #include and #inject static int64_t stb_include_find_includes(const char *text, TArray &plist) { - int64_t line_count = 1; - int64_t inc_count = 0; - const char *s = text, *start; + int64_t line_count = 1; + int64_t inc_count = 0; + const char *s = text, *start; - TArray list; - while (*s) { - // parse is always at start of line when we reach here - start = s; - while (*s == ' ' || *s == '\t') - ++s; - if (*s == '#') { - ++s; - while (*s == ' ' || *s == '\t') + TArray list; + while (*s) + { + // parse is always at start of line when we reach here + start = s; + while (*s == ' ' || *s == '\t') + { + ++s; + } + + if (*s == '#') + { ++s; - if (0==strncmp(s, "include", 7) && stb_include_isspace(s[7])) { - s += 7; while (*s == ' ' || *s == '\t') - ++s; - if (*s == '"') { - const char *t = ++s; - while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) - ++t; - if (*t == '"') { - FString filename; - filename.AppendCStrPart(s, t-s); - s=t; - while (*s != '\r' && *s != '\n' && *s != 0) - ++s; - // s points to the newline, so s-start is everything except the newline - list.Push({start-text, s-text, filename, line_count+1}); - inc_count++; - } + { + ++s; } - } - } - while (*s != '\r' && *s != '\n' && *s != 0) - ++s; - if (*s == '\r' || *s == '\n') { - s = s + (s[0] + s[1] == '\r' + '\n' ? 2 : 1); - } - ++line_count; - } + + if (0==strncmp(s, "include", 7) && stb_include_isspace(s[7])) + { + s += 7; + while (*s == ' ' || *s == '\t') + { + ++s; + } + if (*s == '"') + { + const char *t = ++s; + while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) + ++t; + if (*t == '"') + { + FString filename; + filename.AppendCStrPart(s, t-s); + s = t; + while (*s != '\r' && *s != '\n' && *s != 0) ++s; + + // s points to the newline, so s-start is everything except the newline + list.Push({start - text, s - text, filename, line_count + 1}); + inc_count++; + } + } + } + } + + while (*s != '\r' && *s != '\n' && *s != 0) + { + ++s; + } + + if (*s == '\r' || *s == '\n') + { + s = s + (s[0] + s[1] == '\r' + '\n' ? 2 : 1); + } + ++line_count; + } - plist = std::move(list); + plist = std::move(list); - return inc_count; + return inc_count; } FString stb_include_string(FString str, FString &error) From 44f1e239e4f436b0ec6ed0007f45827043c368ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 15:18:17 -0300 Subject: [PATCH 127/384] deduplicate code --- src/common/rendering/stb_include.cpp | 40 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 165f9a158..04e65f0dc 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -64,49 +64,57 @@ static bool stb_include_isspace(int ch) return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); } +static void skip_whitespace(const char * &s) +{ + while (*s == ' ' || *s == '\t') + { + ++s; + } +} + // find location of all #include and #inject static int64_t stb_include_find_includes(const char *text, TArray &plist) { int64_t line_count = 1; int64_t inc_count = 0; - const char *s = text, *start; + const char *s = text; + const char *start; TArray list; while (*s) { // parse is always at start of line when we reach here start = s; - while (*s == ' ' || *s == '\t') - { - ++s; - } + skip_whitespace(s); if (*s == '#') { ++s; - while (*s == ' ' || *s == '\t') - { - ++s; - } + skip_whitespace(s); if (0==strncmp(s, "include", 7) && stb_include_isspace(s[7])) { s += 7; - while (*s == ' ' || *s == '\t') - { - ++s; - } + skip_whitespace(s); + if (*s == '"') { const char *t = ++s; while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) - ++t; + { + ++t; + } + if (*t == '"') { FString filename; - filename.AppendCStrPart(s, t-s); + filename.AppendCStrPart(s, t - s); s = t; - while (*s != '\r' && *s != '\n' && *s != 0) ++s; + + while (*s != '\r' && *s != '\n' && *s != 0) + { + ++s; + } // s points to the newline, so s-start is everything except the newline list.Push({start - text, s - text, filename, line_count + 1}); From 0d7bb406486dbe58b5404e65d31e2cf0e4a597f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 13 Apr 2025 15:33:34 -0300 Subject: [PATCH 128/384] make code more readable --- src/common/rendering/stb_include.cpp | 62 +++++++++++++++++++--------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 04e65f0dc..71e625d4b 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -72,6 +72,40 @@ static void skip_whitespace(const char * &s) } } +static void skip_newline(const char * &s) +{ + + while (*s != '\r' && *s != '\n' && *s != 0) + { + ++s; + } + + if(s[0] == '\r' && s[1] == '\n') + { + s += 2; + } + else if (*s == '\r' || *s == '\n') + { + s++; + } +} + +static void find_end_quote(const char * &t) +{ + while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) + { + ++t; + } +} + +static void find_newline(const char * &s) +{ + while (*s != '\r' && *s != '\n' && *s != 0) + { + ++s; + } +} + // find location of all #include and #inject static int64_t stb_include_find_includes(const char *text, TArray &plist) { @@ -99,22 +133,17 @@ static int64_t stb_include_find_includes(const char *text, TArray if (*s == '"') { - const char *t = ++s; - while (*t != '"' && *t != '\n' && *t != '\r' && *t != 0) - { - ++t; - } + const char * include_filename = ++s; - if (*t == '"') + find_end_quote(include_filename); + + if (*include_filename == '"') { FString filename; - filename.AppendCStrPart(s, t - s); - s = t; + filename.AppendCStrPart(s, include_filename - s); + s = include_filename; - while (*s != '\r' && *s != '\n' && *s != 0) - { - ++s; - } + find_newline(s); // s points to the newline, so s-start is everything except the newline list.Push({start - text, s - text, filename, line_count + 1}); @@ -124,15 +153,8 @@ static int64_t stb_include_find_includes(const char *text, TArray } } - while (*s != '\r' && *s != '\n' && *s != 0) - { - ++s; - } + skip_newline(s); - if (*s == '\r' || *s == '\n') - { - s = s + (s[0] + s[1] == '\r' + '\n' ? 2 : 1); - } ++line_count; } From 7bcbc8f824e61d367f1f722e26099b6bac65e4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 13:09:36 -0300 Subject: [PATCH 129/384] add filename info to stb_include --- src/common/rendering/stb_include.cpp | 20 ++++++++++++-------- src/common/rendering/stb_include.h | 9 ++++----- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 71e625d4b..8e5f7a595 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -138,7 +138,7 @@ static int64_t stb_include_find_includes(const char *text, TArray find_end_quote(include_filename); if (*include_filename == '"') - { + { // ignore unterminated quotes FString filename; filename.AppendCStrPart(s, include_filename - s); s = include_filename; @@ -163,27 +163,31 @@ static int64_t stb_include_find_includes(const char *text, TArray return inc_count; } -FString stb_include_string(FString str, FString &error) +FString stb_include_string(FString str, FString filename, TArray &filenames, FString &error) { error = ""; TArray inc_list; int64_t num = stb_include_find_includes(str.GetChars(), inc_list); FString text = ""; size_t last = 0; + + filenames.Push(filename); + size_t curIndex = filenames.Size(); + + text.AppendFormat("#line 1 %d // %s\n", curIndex, filename.GetChars()); + for (int64_t i = 0; i < num; ++i) { text.AppendCStrPart(str.GetChars() + last, inc_list[i].offset - last); - text.AppendFormat("#line 1 %d\n", i + 1); - - FString inc = stb_include_file(inc_list[i].filename.GetChars(), error); + FString inc = stb_include_file(inc_list[i].filename.GetChars(), filenames, error); if (!error.IsEmpty()) { return ""; } text += inc; - text.AppendFormat("#line %d 0\n", inc_list[i].next_line_after); + text.AppendFormat("#line %d %d // %s\n", inc_list[i].next_line_after, curIndex, filename.GetChars()); // no newlines, because we kept the #include newlines, which will get appended next last = inc_list[i].end; } @@ -191,7 +195,7 @@ FString stb_include_string(FString str, FString &error) return text; } -FString stb_include_file(FString filename, FString &error) +FString stb_include_file(FString filename, TArray &filenames, FString &error) { FString text; if (!stb_include_load_file(filename, text)) @@ -201,5 +205,5 @@ FString stb_include_file(FString filename, FString &error) error += "'"; return ""; } - return stb_include_string(text, error); + return stb_include_string(text, filename, filenames, error); } diff --git a/src/common/rendering/stb_include.h b/src/common/rendering/stb_include.h index 357e4137f..5bed93def 100644 --- a/src/common/rendering/stb_include.h +++ b/src/common/rendering/stb_include.h @@ -39,11 +39,10 @@ #include "zstring.h" -// Do include-processing on the string 'str'. To free the return value, pass it to free() -FString stb_include_string(FString str, FString &error); +// Do include-processing on the string 'str'. +FString stb_include_string(FString str, FString filename, TArray &filenames, FString &error); -// Load the file 'filename' and do include-processing on the string therein. note that -// 'filename' is opened directly; 'path_to_includes' is not used. To free the return value, pass it to free() -FString stb_include_file(FString filename, FString &error); +// Load the file 'filename' and do include-processing on the string therein. +FString stb_include_file(FString filename, TArray &filenames, FString &error); #endif From 009b84bb1ab7bb3b7dcb314fe5701a6c4de06073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 13:09:56 -0300 Subject: [PATCH 130/384] print warning for deprecated uniform declarations --- .../hwrenderer/data/hw_shaderpatcher.cpp | 11 +++++++++-- src/common/utility/zstring.cpp | 15 +++++++++------ src/common/utility/zstring.h | 10 +++++----- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp b/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp index d32b09e23..24ffa23f3 100644 --- a/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp +++ b/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp @@ -39,6 +39,7 @@ #include "textures.h" #include "hw_renderstate.h" #include "v_video.h" +#include "printf.h" static bool IsGlslWhitespace(char c) @@ -81,8 +82,8 @@ FString RemoveLegacyUserUniforms(FString code) { // User shaders must declare their uniforms via the GLDEFS file. - code.Substitute("uniform sampler2D tex;", " "); - code.Substitute("uniform float timer;", " "); + bool found = code.Substitute("uniform sampler2D tex;", " "); + found = code.Substitute("uniform float timer;", " ") || found; // The following code searches for legacy uniform declarations in the shader itself and replaces them with whitespace. @@ -120,6 +121,7 @@ FString RemoveLegacyUserUniforms(FString code) chars[i] = ' '; } startIndex = statementEndIndex; + found = true; } else { @@ -127,6 +129,11 @@ FString RemoveLegacyUserUniforms(FString code) } } + if(found) + { + DPrintf(DMSG_WARNING, TEXTCOLOR_ORANGE "timer and tex uniforms should not be explicitly declared.\n"); + } + // Also remove all occurences of the token 'texture2d'. Some shaders may still use this deprecated function to access a sampler. // Modern GLSL only allows use of 'texture'. while (true) diff --git a/src/common/utility/zstring.cpp b/src/common/utility/zstring.cpp index 488c09eb6..0a5405e43 100644 --- a/src/common/utility/zstring.cpp +++ b/src/common/utility/zstring.cpp @@ -1031,29 +1031,30 @@ void FString::MergeChars (const char *charset, char newchar) UnlockBuffer(); } -void FString::Substitute (const FString &oldstr, const FString &newstr) +bool FString::Substitute (const FString &oldstr, const FString &newstr) { return Substitute (oldstr.Chars, newstr.Chars, oldstr.Len(), newstr.Len()); } -void FString::Substitute (const char *oldstr, const FString &newstr) +bool FString::Substitute (const char *oldstr, const FString &newstr) { return Substitute (oldstr, newstr.Chars, strlen(oldstr), newstr.Len()); } -void FString::Substitute (const FString &oldstr, const char *newstr) +bool FString::Substitute (const FString &oldstr, const char *newstr) { return Substitute (oldstr.Chars, newstr, oldstr.Len(), strlen(newstr)); } -void FString::Substitute (const char *oldstr, const char *newstr) +bool FString::Substitute (const char *oldstr, const char *newstr) { return Substitute (oldstr, newstr, strlen(oldstr), strlen(newstr)); } -void FString::Substitute (const char *oldstr, const char *newstr, size_t oldstrlen, size_t newstrlen) +bool FString::Substitute (const char *oldstr, const char *newstr, size_t oldstrlen, size_t newstrlen) { - if (oldstr == nullptr || newstr == nullptr || *oldstr == 0) return; + if (oldstr == nullptr || newstr == nullptr || *oldstr == 0) return false; + bool found = false; LockBuffer(); for (size_t checkpt = 0; checkpt < Len(); ) { @@ -1061,6 +1062,7 @@ void FString::Substitute (const char *oldstr, const char *newstr, size_t oldstrl size_t len = Len(); if (match != NULL) { + found = true; size_t matchpt = match - Chars; if (oldstrlen != newstrlen) { @@ -1076,6 +1078,7 @@ void FString::Substitute (const char *oldstr, const char *newstr, size_t oldstrl } } UnlockBuffer(); + return found; } bool FString::IsInt () const diff --git a/src/common/utility/zstring.h b/src/common/utility/zstring.h index 284027712..2bc27d7b3 100644 --- a/src/common/utility/zstring.h +++ b/src/common/utility/zstring.h @@ -297,11 +297,11 @@ public: void MergeChars (char merger, char newchar); void MergeChars (const char *charset, char newchar); - void Substitute (const FString &oldstr, const FString &newstr); - void Substitute (const char *oldstr, const FString &newstr); - void Substitute (const FString &oldstr, const char *newstr); - void Substitute (const char *oldstr, const char *newstr); - void Substitute (const char *oldstr, const char *newstr, size_t oldstrlen, size_t newstrlen); + bool Substitute (const FString &oldstr, const FString &newstr); + bool Substitute (const char *oldstr, const FString &newstr); + bool Substitute (const FString &oldstr, const char *newstr); + bool Substitute (const char *oldstr, const char *newstr); + bool Substitute (const char *oldstr, const char *newstr, size_t oldstrlen, size_t newstrlen); void Format (const char *fmt, ...) PRINTFISH(3); void AppendFormat (const char *fmt, ...) PRINTFISH(3); From c0b6427b120fb53f0eada08da86494f562afcaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 13:18:37 -0300 Subject: [PATCH 131/384] print warning for texture2d usage --- .../rendering/hwrenderer/data/hw_shaderpatcher.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp b/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp index 24ffa23f3..3bef81e0b 100644 --- a/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp +++ b/src/common/rendering/hwrenderer/data/hw_shaderpatcher.cpp @@ -134,6 +134,8 @@ FString RemoveLegacyUserUniforms(FString code) DPrintf(DMSG_WARNING, TEXTCOLOR_ORANGE "timer and tex uniforms should not be explicitly declared.\n"); } + bool foundtexture2d = false; + // Also remove all occurences of the token 'texture2d'. Some shaders may still use this deprecated function to access a sampler. // Modern GLSL only allows use of 'texture'. while (true) @@ -142,6 +144,8 @@ FString RemoveLegacyUserUniforms(FString code) if (matchIndex == -1) break; + foundtexture2d = true; + // Check if this is a real token. bool isKeywordStart = matchIndex == 0 || !isalnum(chars[matchIndex - 1] & 255); bool isKeywordEnd = matchIndex + 9 == len || !isalnum(chars[matchIndex + 9] & 255); @@ -152,6 +156,11 @@ FString RemoveLegacyUserUniforms(FString code) startIndex = matchIndex + 9; } + if(foundtexture2d) + { + DPrintf(DMSG_WARNING, TEXTCOLOR_ORANGE "texture2d is deprecated, use texture instead.\n"); + } + code.UnlockBuffer(); return code; From b6bdb7b4e7597f8076de7c8a2b326889c91c4bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 16:24:22 -0300 Subject: [PATCH 132/384] fix missing newline in stb_include --- src/common/rendering/stb_include.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 8e5f7a595..97e846d3b 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -174,7 +174,7 @@ FString stb_include_string(FString str, FString filename, TArray &filen filenames.Push(filename); size_t curIndex = filenames.Size(); - text.AppendFormat("#line 1 %d // %s\n", curIndex, filename.GetChars()); + text.AppendFormat("\n#line 1 %d // %s\n", curIndex, filename.GetChars()); for (int64_t i = 0; i < num; ++i) { @@ -187,7 +187,7 @@ FString stb_include_string(FString str, FString filename, TArray &filen } text += inc; - text.AppendFormat("#line %d %d // %s\n", inc_list[i].next_line_after, curIndex, filename.GetChars()); + text.AppendFormat("\n#line %d %d // %s\n", inc_list[i].next_line_after, curIndex, filename.GetChars()); // no newlines, because we kept the #include newlines, which will get appended next last = inc_list[i].end; } From 317cd358d19fc5f89ff6e9ea7feb23ed00438668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 16:24:35 -0300 Subject: [PATCH 133/384] implement includes for opengl --- src/common/rendering/gl/gl_shader.cpp | 204 ++++++++++++++++++++++---- 1 file changed, 178 insertions(+), 26 deletions(-) diff --git a/src/common/rendering/gl/gl_shader.cpp b/src/common/rendering/gl/gl_shader.cpp index bafbf89b8..b891a1440 100644 --- a/src/common/rendering/gl/gl_shader.cpp +++ b/src/common/rendering/gl/gl_shader.cpp @@ -42,6 +42,7 @@ #include "i_specialpaths.h" #include "printf.h" #include "version.h" +#include "stb_include.h" #include "gl_interface.h" #include "gl_debug.h" @@ -210,9 +211,101 @@ void SaveCachedProgramBinary(const FString &vertex, const FString &fragment, con SaveShaders(); } +FString ProcessShaderError(const char * shaderError, TArray &filenames_for_error) +{ + //ugh, intel, amd and nvidia handle things differently so this has to be a mess + enum + { + READING_LUMP, + READING_LINE_COLON, + READING_LINE_PARENTHESES, + SKIP_TO_NEWLINE, + }; + + FString err(shaderError); + size_t cur = 0; + size_t state_start = 0; + + size_t line_start = 0; + size_t num_end = 0; + + int state = READING_LUMP; + + int64_t lump_num = 0; + + while(cur < err.Len()) + { + if(state != SKIP_TO_NEWLINE) + { + while(err[cur] >= '0' && err[cur] <= '9') + { + cur++; + } + + if(cur == state_start) + { + state = SKIP_TO_NEWLINE; + } + else if(state == READING_LUMP && (err[cur] == '(' || err[cur] == ':')) + { + FString lump_num_str = err.Mid(state_start, cur - state_start); + lump_num = lump_num_str.ToLong(); + line_start = state_start; + state = (err[cur] == ':') ? READING_LINE_COLON : READING_LINE_PARENTHESES; + cur++; + state_start = cur; + } + else if((state == READING_LINE_COLON && err[cur] == ':') || (state == READING_LINE_PARENTHESES && err[cur] == ')')) + { + FString line_num_str = err.Mid(state_start, cur - state_start); + + if(state == READING_LINE_PARENTHESES) + { + cur+= 3; // skip ") :" + } + else + { + cur++; // skip ":" + } + + int64_t old_len = cur - line_start; + FString new_err = "File '" + filenames_for_error[lump_num - 1] + "', Line " + line_num_str + ": "; + + int64_t diff = new_err.Len() - old_len; + + err = err.Left(line_start) + new_err + err.Mid(line_start + old_len); + + cur += diff; + state = SKIP_TO_NEWLINE; + } + else + { // couldn't find a valid num, skip line + state = SKIP_TO_NEWLINE; + } + } + //not 'else if' to allow this to run immediately after + if(state == SKIP_TO_NEWLINE) + { + if(err[cur] == '\n' || err[cur] == '\r') + { + while(cur < err.Len() && (err[cur] == '\n' || err[cur] == '\r')) + { + cur++; + } + state_start = cur; + state = READING_LUMP; + } + else + { + cur++; + } + } + } + return err; +} + bool FShader::Load(const char * name, const char * vert_prog_lump, const char * frag_prog_lump, const char * proc_prog_lump, const char * light_fragprog, const char * defines) { - static char buffer[10000]; FString error; FString i_data = R"( @@ -411,6 +504,7 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * vp_comb << RemoveLayoutLocationDecl(GetStringFromLump(vp_lump), "out").GetChars() << "\n"; fp_comb << RemoveLayoutLocationDecl(GetStringFromLump(fp_lump), "in").GetChars() << "\n"; FString placeholder = "\n"; + TArray filenames_for_error; if (proc_prog_lump != NULL) { @@ -418,10 +512,33 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * if (*proc_prog_lump != '#') { + FString lump_filename(proc_prog_lump); + FString pp_data; int pp_lump = fileSystem.CheckNumForFullName(proc_prog_lump, 0); // if it's a core shader, ignore overrides by user mods. - if (pp_lump == -1) pp_lump = fileSystem.CheckNumForFullName(proc_prog_lump); - if (pp_lump == -1) I_Error("Unable to load '%s'", proc_prog_lump); - FString pp_data = GetStringFromLump(pp_lump); + if (pp_lump == -1) + { + pp_lump = fileSystem.CheckNumForFullName(proc_prog_lump); + if (pp_lump == -1) + { + I_Error("Unable to load '%s'", proc_prog_lump); + } + else + { + FString error = ""; + + pp_data = stb_include_string(GetStringFromLump(pp_lump), lump_filename, filenames_for_error, error); + + if(!error.IsEmpty()) + { + I_Error("Unable to load '%s': %s", proc_prog_lump, error.GetChars()); + } + } + } + else + { // skip includes processing for code shaders + pp_data = GetStringFromLump(pp_lump); + filenames_for_error.Push(lump_filename); + } if (pp_data.IndexOf("ProcessMaterial") < 0 && pp_data.IndexOf("SetupMaterial") < 0) { @@ -529,37 +646,72 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * glShaderSource(hVertProg, 1, &vp_ptr, &vp_size); glShaderSource(hFragProg, 1, &fp_ptr, &fp_size); + GLint status = 0; + + bool errored = false; + glCompileShader(hVertProg); + + if (glGetShaderiv(hVertProg, GL_COMPILE_STATUS, &status); status == GL_FALSE) + { + TArray buffer; + GLint info_log_length = 1; + glGetShaderiv(hVertProg, GL_INFO_LOG_LENGTH, &info_log_length); + buffer.Resize(info_log_length + 1); + + glGetShaderInfoLog(hVertProg, info_log_length + 1, NULL, buffer.Data()); + if (*buffer.Data()) + { + //error << "Vertex shader:\n" << buffer.Data() << "\n"; + error << "Vertex shader:\n" << ProcessShaderError(buffer.Data(), filenames_for_error) << "\n"; + } + + errored = true; + } + glCompileShader(hFragProg); + if (glGetShaderiv(hFragProg, GL_COMPILE_STATUS, &status); status == GL_FALSE) + { + TArray buffer; + GLint info_log_length = 1; + glGetShaderiv(hFragProg, GL_INFO_LOG_LENGTH, &info_log_length); + buffer.Resize(info_log_length + 1); + + glGetShaderInfoLog(hFragProg, info_log_length + 1, NULL, buffer.Data()); + if (*buffer.Data()) + { + error << "Fragment shader:\n" << ProcessShaderError(buffer.Data(), filenames_for_error) << "\n"; + } + + errored = true; + } + + if(errored) + { + // only print message if there's an error. + I_Error("Errors Compiliong Shader '%s':\n%s\n", name, error.GetChars()); + } + glAttachShader(hShader, hVertProg); glAttachShader(hShader, hFragProg); glLinkProgram(hShader); - glGetShaderInfoLog(hVertProg, 10000, NULL, buffer); - if (*buffer) + if (glGetProgramiv(hShader, GL_LINK_STATUS, &status); status == GL_FALSE) { - error << "Vertex shader:\n" << buffer << "\n"; - } - glGetShaderInfoLog(hFragProg, 10000, NULL, buffer); - if (*buffer) - { - error << "Fragment shader:\n" << buffer << "\n"; - } + TArray buffer; + GLint info_log_length = 1; + glGetProgramiv(hShader, GL_INFO_LOG_LENGTH, &info_log_length); + buffer.Resize(info_log_length + 1); - glGetProgramInfoLog(hShader, 10000, NULL, buffer); - if (*buffer) - { - error << "Linking:\n" << buffer << "\n"; - } - GLint status = 0; - glGetProgramiv(hShader, GL_LINK_STATUS, &status); - linked = (status == GL_TRUE); - if (!linked) - { - // only print message if there's an error. - I_Error("Init Shader '%s':\n%s\n", name, error.GetChars()); + glGetProgramInfoLog(hShader, info_log_length + 1, NULL, buffer.Data()); + if (*buffer.Data()) + { + error << "Linking:\n" << buffer.Data() << "\n"; + } + + I_Error("Errors Linking Shader '%s':\n%s\n", name, error.GetChars()); } else if (glProgramBinary && IsShaderCacheActive()) { @@ -643,7 +795,7 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * if (lightmapindex != -1) glUniform1i(lightmapindex, 17); glUseProgram(0); - return linked; + return true; } //========================================================================== From d56b84d09484b1a33ad09a75340c6fedfeba481b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 16:24:48 -0300 Subject: [PATCH 134/384] hook up includes for vulkan --- .../rendering/vulkan/shaders/vk_ppshader.cpp | 6 +++- .../rendering/vulkan/shaders/vk_shader.cpp | 32 +++++++++++++++++++ .../rendering/vulkan/shaders/vk_shader.h | 4 +++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/common/rendering/vulkan/shaders/vk_ppshader.cpp b/src/common/rendering/vulkan/shaders/vk_ppshader.cpp index dcf72a06d..5366481f7 100644 --- a/src/common/rendering/vulkan/shaders/vk_ppshader.cpp +++ b/src/common/rendering/vulkan/shaders/vk_ppshader.cpp @@ -39,12 +39,16 @@ VkPPShader::VkPPShader(VulkanRenderDevice* fb, PPShader *shader) : fb(fb) .Type(ShaderType::Vertex) .AddSource(shader->VertexShader.GetChars(), LoadShaderCode(shader->VertexShader, "", shader->Version).GetChars()) .DebugName(shader->VertexShader.GetChars()) + .OnIncludeLocal(VkShaderManager::OnInclude) + .OnIncludeSystem(VkShaderManager::OnInclude) .Create(shader->VertexShader.GetChars(), fb->device.get()); FragmentShader = ShaderBuilder() .Type(ShaderType::Fragment) .AddSource(shader->FragmentShader.GetChars(), LoadShaderCode(shader->FragmentShader, prolog, shader->Version).GetChars()) .DebugName(shader->FragmentShader.GetChars()) + .OnIncludeLocal(VkShaderManager::OnInclude) + .OnIncludeSystem(VkShaderManager::OnInclude) .Create(shader->FragmentShader.GetChars(), fb->device.get()); fb->GetShaderManager()->AddVkPPShader(this); @@ -72,7 +76,7 @@ FString VkPPShader::LoadShaderCode(const FString &lumpName, const FString &defin FString code = GetStringFromLump(lump); FString patchedCode; - patchedCode.AppendFormat("#version %d\n", 450); + patchedCode.AppendFormat("#version %d\n#extension GL_GOOGLE_include_directive : enable\n", 450); patchedCode << defines; patchedCode << "#line 1\n"; patchedCode << code; diff --git a/src/common/rendering/vulkan/shaders/vk_shader.cpp b/src/common/rendering/vulkan/shaders/vk_shader.cpp index 00734ac7f..8206e4137 100644 --- a/src/common/rendering/vulkan/shaders/vk_shader.cpp +++ b/src/common/rendering/vulkan/shaders/vk_shader.cpp @@ -30,6 +30,32 @@ #include "version.h" #include "cmdlib.h" +ShaderIncludeResult VkShaderManager::OnInclude(FString headerName, FString includerName, size_t depth) +{ + if (depth > 8) + I_Error("Too much include recursion!"); + + FString includeguardname; + includeguardname << "_HEADERGUARD_" << headerName.GetChars(); + includeguardname.ReplaceChars("/\\.", '_'); + + FString code; + code << "#ifndef " << includeguardname.GetChars() << "\n"; + code << "#define " << includeguardname.GetChars() << "\n"; + code << "#line 1\n"; + + int lumpNum = fileSystem.FindFile(headerName.GetChars()); + + if(lumpNum >= 0) + { + code << GetStringFromLump(lumpNum, false); + } + + code << "\n#endif\n"; + + return ShaderIncludeResult(headerName.GetChars(), code.GetChars()); +} + bool VkShaderManager::CompileNextShader() { const char *mainvp = "shaders/glsl/main.vp"; @@ -341,6 +367,7 @@ static const char *shaderBindings = R"( std::unique_ptr VkShaderManager::LoadVertShader(FString shadername, const char *vert_lump, const char *defines) { FString code = GetTargetGlslVersion(); + code << "#extension GL_GOOGLE_include_directive : enable\n"; code << defines; code << "\n#define MAX_STREAM_DATA " << std::to_string(MAX_STREAM_DATA).c_str() << "\n"; #ifdef NPOT_EMULATION @@ -355,12 +382,15 @@ std::unique_ptr VkShaderManager::LoadVertShader(FString shadername .Type(ShaderType::Vertex) .AddSource(shadername.GetChars(), code.GetChars()) .DebugName(shadername.GetChars()) + .OnIncludeLocal(OnInclude) + .OnIncludeSystem(OnInclude) .Create(shadername.GetChars(), fb->device.get()); } std::unique_ptr VkShaderManager::LoadFragShader(FString shadername, const char *frag_lump, const char *material_lump, const char *light_lump, const char *defines, bool alphatest, bool gbufferpass) { FString code = GetTargetGlslVersion(); + code << "#extension GL_GOOGLE_include_directive : enable\n"; if (fb->RaytracingEnabled()) code << "\n#define SUPPORTS_RAYTRACING\n"; code << defines; @@ -448,6 +478,8 @@ std::unique_ptr VkShaderManager::LoadFragShader(FString shadername .Type(ShaderType::Fragment) .AddSource(shadername.GetChars(), code.GetChars()) .DebugName(shadername.GetChars()) + .OnIncludeLocal(OnInclude) + .OnIncludeSystem(OnInclude) .Create(shadername.GetChars(), fb->device.get()); } diff --git a/src/common/rendering/vulkan/shaders/vk_shader.h b/src/common/rendering/vulkan/shaders/vk_shader.h index cac924916..8880daeea 100644 --- a/src/common/rendering/vulkan/shaders/vk_shader.h +++ b/src/common/rendering/vulkan/shaders/vk_shader.h @@ -7,6 +7,7 @@ #include "matrix.h" #include "name.h" #include "hw_renderstate.h" +#include "zvulkan/vulkanbuilders.h" #include #define SHADER_MIN_REQUIRED_TEXTURE_LAYERS 11 @@ -89,6 +90,8 @@ private: FString LoadPublicShaderLump(const char *lumpname); FString LoadPrivateShaderLump(const char *lumpname); + static ShaderIncludeResult OnInclude(FString headerName, FString includerName, size_t depth); + VulkanRenderDevice* fb = nullptr; std::vector mMaterialShaders[MAX_PASS_TYPES]; @@ -98,4 +101,5 @@ private: int compileIndex = 0; std::list PPShaders; + friend class VkPPShader; }; From 880ebfd94cdd0ed8536fe9a50bbae87a45bfa6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 16 Apr 2025 17:28:01 -0300 Subject: [PATCH 135/384] fix up header comment --- src/common/rendering/stb_include.cpp | 25 +++++-------------------- src/common/rendering/stb_include.h | 25 +++++-------------------- 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/src/common/rendering/stb_include.cpp b/src/common/rendering/stb_include.cpp index 97e846d3b..cf9c70acd 100644 --- a/src/common/rendering/stb_include.cpp +++ b/src/common/rendering/stb_include.cpp @@ -1,4 +1,4 @@ -// stb_include.h - v0.02 - parse and process #include directives - public domain +// stb_include.h - v0.02gz - parse and process #include directives - public domain // // To build this, in one source file that includes this file do // #define STB_INCLUDE_IMPLEMENTATION @@ -7,25 +7,7 @@ // #include "foo" // with the contents of a file named "foo". It also embeds the // appropriate #line directives. Note that all include files must -// reside in the location specified in the path passed to the API; -// it does not check multiple directories. -// -// If the string contains a line of the form -// #inject -// then it will be replaced with the contents of the string 'inject' passed to the API. -// -// Options: -// -// Define STB_INCLUDE_LINE_GLSL to get GLSL-style #line directives -// which use numbers instead of filenames. -// -// Define STB_INCLUDE_LINE_NONE to disable output of #line directives. -// -// Standard libraries: -// -// stdio.h FILE, fopen, fclose, fseek, ftell -// stdlib.h malloc, realloc, free -// string.h strcpy, strncmp, memcpy +// reside in the location specified in the gzdoom filesystem. // // Credits: // @@ -33,6 +15,9 @@ // // Fixes: // Michal Klos +// +// GZDoom Conversion: +// Jay #include "stb_include.h" #include "filesystem.h" diff --git a/src/common/rendering/stb_include.h b/src/common/rendering/stb_include.h index 5bed93def..dc554f383 100644 --- a/src/common/rendering/stb_include.h +++ b/src/common/rendering/stb_include.h @@ -1,4 +1,4 @@ -// stb_include.h - v0.02 - parse and process #include directives - public domain +// stb_include.h - v0.02gz - parse and process #include directives - public domain // // To build this, in one source file that includes this file do // #define STB_INCLUDE_IMPLEMENTATION @@ -7,25 +7,7 @@ // #include "foo" // with the contents of a file named "foo". It also embeds the // appropriate #line directives. Note that all include files must -// reside in the location specified in the path passed to the API; -// it does not check multiple directories. -// -// If the string contains a line of the form -// #inject -// then it will be replaced with the contents of the string 'inject' passed to the API. -// -// Options: -// -// Define STB_INCLUDE_LINE_GLSL to get GLSL-style #line directives -// which use numbers instead of filenames. -// -// Define STB_INCLUDE_LINE_NONE to disable output of #line directives. -// -// Standard libraries: -// -// stdio.h FILE, fopen, fclose, fseek, ftell -// stdlib.h malloc, realloc, free -// string.h strcpy, strncmp, memcpy +// reside in the location specified in the gzdoom filesystem. // // Credits: // @@ -33,6 +15,9 @@ // // Fixes: // Michal Klos +// +// GZDoom Conversion: +// Jay #ifndef STB_INCLUDE_STB_INCLUDE_H #define STB_INCLUDE_STB_INCLUDE_H From b7d6f0b1a45fc6fd93344f795d113f30efe81c35 Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Sat, 26 Apr 2025 23:46:38 +0400 Subject: [PATCH 136/384] Added am_showlevelname CVar Allows controlling the visibility of the level name on the automap. --- src/am_map.cpp | 1 + src/g_statusbar/shared_sbar.cpp | 3 +++ src/scripting/vmthunks.cpp | 13 ++++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/am_map.cpp b/src/am_map.cpp index 627d6ea1e..ed9eea562 100644 --- a/src/am_map.cpp +++ b/src/am_map.cpp @@ -170,6 +170,7 @@ CVAR(Bool, am_showmonsters, true, CVAR_ARCHIVE); CVAR(Bool, am_showitems, false, CVAR_ARCHIVE); CVAR(Bool, am_showtime, true, CVAR_ARCHIVE); CVAR(Bool, am_showtotaltime, false, CVAR_ARCHIVE); +CVAR(Bool, am_showlevelname, true, CVAR_ARCHIVE); CVAR(Int, am_colorset, 0, CVAR_ARCHIVE); CVAR(Bool, am_customcolors, true, CVAR_ARCHIVE); CVAR(Int, am_map_secrets, 1, CVAR_ARCHIVE); diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index d336808b1..5c4fa25d9 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -85,6 +85,7 @@ EXTERN_CVAR (Bool, am_showsecrets) EXTERN_CVAR (Bool, am_showitems) EXTERN_CVAR (Bool, am_showtime) EXTERN_CVAR (Bool, am_showtotaltime) +EXTERN_CVAR (Bool, am_showlevelname) EXTERN_CVAR(Bool, inter_subtitles) EXTERN_CVAR(Bool, ui_screenborder_classic_scaling) @@ -598,6 +599,8 @@ void DBaseStatusBar::DoDrawAutomapHUD(int crdefault, int highlight) } FormatMapName(primaryLevel, crdefault, &textbuffer); + if (textbuffer.IsEmpty()) + return; if (!generic_ui) { diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 6ff757b96..977d72de4 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2282,6 +2282,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetSpotState, GetSpotState) //--------------------------------------------------------------------------- EXTERN_CVAR(Int, am_showmaplabel) +EXTERN_CVAR(Bool, am_showlevelname) void FormatMapName(FLevelLocals *self, int cr, FString *result) { @@ -2295,13 +2296,19 @@ void FormatMapName(FLevelLocals *self, int cr, FString *result) if (self->info->MapLabel.IsNotEmpty()) { if (self->info->MapLabel.Compare("*")) - *result << self->info->MapLabel << ": "; + *result << self->info->MapLabel; } else if (am_showmaplabel == 1 || (am_showmaplabel == 2 && !ishub)) { - *result << self->MapName << ": "; + *result << self->MapName; + } + + if (am_showlevelname) + { + if (!result->IsEmpty()) + *result << ": "; + *result << mapnamecolor << self->LevelName; } - *result << mapnamecolor << self->LevelName; } DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, FormatMapName, FormatMapName) From a393a6ac56b94517988fb96d60be1d6bf04b134a Mon Sep 17 00:00:00 2001 From: Xaser Acheron Date: Mon, 28 Apr 2025 20:24:42 -0500 Subject: [PATCH 137/384] fix dsdhacked actors not spawning when placed in a map --- src/gamedata/d_dehacked.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gamedata/d_dehacked.cpp b/src/gamedata/d_dehacked.cpp index 8ddcc7935..040735874 100644 --- a/src/gamedata/d_dehacked.cpp +++ b/src/gamedata/d_dehacked.cpp @@ -139,6 +139,7 @@ static PClassActor* FindInfoName(int index, bool mustexist = false) cls = static_cast(RUNTIME_CLASS(AActor)->CreateDerivedClass(name.GetChars(), (unsigned)sizeof(AActor))); NewClassType(cls, -1); // This needs a VM type to work as intended. cls->InitializeDefaults(); + PClassActor::AllActorClasses.Push(cls); } if (cls) { @@ -3714,7 +3715,11 @@ void FinishDehPatch () mysnprintf(typeNameBuilder, countof(typeNameBuilder), "DehackedPickup%d", nameindex++); bool newlycreated; subclass = static_cast(dehtype->CreateDerivedClass(typeNameBuilder, dehtype->Size, &newlycreated, 0)); - if (newlycreated) subclass->InitializeDefaults(); + if (newlycreated) + { + subclass->InitializeDefaults(); + PClassActor::AllActorClasses.Push(subclass); + } } while (subclass == nullptr); NewClassType(subclass, 0); // This needs a VM type to work as intended. From 80c36d4069db5ed92c100d4d26c3add3576b2d41 Mon Sep 17 00:00:00 2001 From: Xaser Acheron Date: Tue, 29 Apr 2025 01:21:53 -0500 Subject: [PATCH 138/384] add a few commonly-used gzdoom-specific properties to the dehacked parser --- src/gamedata/d_dehacked.cpp | 69 +++++++++++++++++++++++++++++++++++-- src/namedef_custom.h | 3 ++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/gamedata/d_dehacked.cpp b/src/gamedata/d_dehacked.cpp index 040735874..1f6fc92c2 100644 --- a/src/gamedata/d_dehacked.cpp +++ b/src/gamedata/d_dehacked.cpp @@ -1204,6 +1204,8 @@ static int PatchThing (int thingy, int flags) AActor *info; uint8_t dummy[sizeof(AActor)]; bool hadHeight = false; + bool hadProjHeight = false; + bool hadPhysHeight = false; bool hadTranslucency = false; bool hadStyle = false; FStateDefinitions statedef; @@ -1263,8 +1265,17 @@ static int PatchThing (int thingy, int flags) } else if (linelen == 6 && stricmp (Line1, "Height") == 0) { - info->Height = DEHToDouble(val); - info->projectilepassheight = 0; // needs to be disabled + // [XA] This is a bit more complex now, since projectilepassheight + // and "physical height" now both exist. if either of these are + // defined, then override the Height field accordingly. + if(!hadPhysHeight) + { + info->Height = DEHToDouble(val); + } + if(!hadProjHeight) + { + info->projectilepassheight = 0; // needs to be disabled + } hadHeight = true; } else if (linelen == 14 && stricmp (Line1, "Missile damage") == 0) @@ -1460,6 +1471,47 @@ static int PatchThing (int thingy, int flags) info->missilechancemult = DEHToDouble(val); } + // [XA] Fields for common GZDoom-specific values that + // are desirable to set in a cross-port DEHACKED mod. + // adding these prevents users from having to reimplement + // actors in zscript just to define these few fields. + else if (!stricmp(Line1, "Projectile pass height")) + { + info->projectilepassheight = DEHToDouble(val); + hadProjHeight = true; + } + else if (!stricmp(Line1, "Physical height")) + { + // [XA] This is a synonym for Height that is intended + // to be ignored in any ports that don't support + // projectilepassheight -- this is needed because + // other ports' "Height x" is actually equivalent + // to Zdoom's "ProjectilePassHeight x; Height y", + // i.e. the definition of the Height var is different. + info->Height = DEHToDouble(val); + hadPhysHeight = true; + } + else if (!stricmp(Line1, "Tag")) + { + stripwhite(Line2); + info->SetTag(Line2); + } + else if (!stricmp(Line1, "Obituary")) + { + stripwhite(Line2); + info->StringVar(NAME_Obituary) = Line2; + } + else if (!stricmp(Line1, "Melee obituary")) + { + stripwhite(Line2); + info->StringVar(NAME_HitObituary) = Line2; + } + else if (!stricmp(Line1, "Self obituary")) + { + stripwhite(Line2); + info->StringVar(NAME_SelfObituary) = Line2; + } + else if (linelen > 6) { if (stricmp (Line1 + linelen - 6, " frame") == 0) @@ -1728,12 +1780,23 @@ static int PatchThing (int thingy, int flags) else Printf (unknown_str, Line1, "Thing", thingy); } + // [XA] sanity check: to avoid ambiguity, an actor that defines + // ProjectilePassHeight must also define PhysicalHeight, and vice-versa. + if(hadPhysHeight && !hadProjHeight) + { + I_Error("Thing %d: DEHACKED actors that set 'Physical height' must also set 'Projectile pass height'\n", thingy); + } + else if(!hadPhysHeight && hadProjHeight) + { + I_Error("Thing %d: DEHACKED actors that set 'Projectile pass height' must also set 'Physical height'\n", thingy); + } + if (info != (AActor *)&dummy) { // Reset heights for things hanging from the ceiling that // don't specify a new height. if (info->flags & MF_SPAWNCEILING && - !hadHeight && + !hadHeight && !hadPhysHeight && thingy <= (int)OrgHeights.Size() && thingy > 0) { info->Height = OrgHeights[thingy - 1]; diff --git a/src/namedef_custom.h b/src/namedef_custom.h index c2925bb83..cefcd6787 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -482,6 +482,9 @@ xx(PowerupType) xx(PlayerPawn) xx(RipSound) xx(Archvile) +xx(Obituary) +xx(HitObituary) +xx(SelfObituary) xx(ResolveState) From c0d17bbda34015b214d5c96cb141ad02d8b0ddb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 29 Apr 2025 15:37:28 -0300 Subject: [PATCH 139/384] limit light alpha mult to renderflag --- src/playsim/actor.h | 1 + src/rendering/hwrenderer/hw_dynlightdata.cpp | 2 +- src/rendering/hwrenderer/scene/hw_spritelight.cpp | 2 +- src/scripting/thingdef_data.cpp | 1 + wadsrc/static/zscript/actors/shared/dynlights.zs | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 3339adeb9..00993106d 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -508,6 +508,7 @@ enum ActorRenderFlag2 RF2_ISOMETRICSPRITES = 0x0080, RF2_SQUAREPIXELS = 0x0100, // apply +ROLLSPRITE scaling math so that non rolling sprites get the same scaling RF2_STRETCHPIXELS = 0x0200, // don't apply SQUAREPIXELS for ROLLSPRITES + RF2_LIGHTMULTALPHA = 0x0400, // attached lights use alpha as intensity multiplier }; // This translucency value produces the closest match to Heretic's TINTTAB. diff --git a/src/rendering/hwrenderer/hw_dynlightdata.cpp b/src/rendering/hwrenderer/hw_dynlightdata.cpp index 0c264a1f7..bf516e637 100644 --- a/src/rendering/hwrenderer/hw_dynlightdata.cpp +++ b/src/rendering/hwrenderer/hw_dynlightdata.cpp @@ -92,7 +92,7 @@ void AddLightToList(FDynLightData &dld, int group, FDynamicLight * light, bool f cs = 1.0f; } - if (light->target) + if (light->target && (light->target->renderflags2 & RF2_LIGHTMULTALPHA)) cs *= (float)light->target->Alpha; float r = light->GetRed() / 255.0f * cs; diff --git a/src/rendering/hwrenderer/scene/hw_spritelight.cpp b/src/rendering/hwrenderer/scene/hw_spritelight.cpp index 0310d1008..ad2c59d7f 100644 --- a/src/rendering/hwrenderer/scene/hw_spritelight.cpp +++ b/src/rendering/hwrenderer/scene/hw_spritelight.cpp @@ -186,7 +186,7 @@ void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FSec lg = light->GetGreen() / 255.0f; lb = light->GetBlue() / 255.0f; - if (light->target) + if (light->target && (light->target->renderflags2 & RF2_LIGHTMULTALPHA)) { float alpha = (float)light->target->Alpha; lr *= alpha; diff --git a/src/scripting/thingdef_data.cpp b/src/scripting/thingdef_data.cpp index c1e8dd045..c8d37c398 100644 --- a/src/scripting/thingdef_data.cpp +++ b/src/scripting/thingdef_data.cpp @@ -390,6 +390,7 @@ static FFlagDef ActorFlagDefs[]= DEFINE_FLAG(RF2, ISOMETRICSPRITES, AActor, renderflags2), DEFINE_FLAG(RF2, SQUAREPIXELS, AActor, renderflags2), DEFINE_FLAG(RF2, STRETCHPIXELS, AActor, renderflags2), + DEFINE_FLAG(RF2, LIGHTMULTALPHA, AActor, renderflags2), // Bounce flags DEFINE_FLAG2(BOUNCE_Walls, BOUNCEONWALLS, AActor, BounceFlags), diff --git a/wadsrc/static/zscript/actors/shared/dynlights.zs b/wadsrc/static/zscript/actors/shared/dynlights.zs index 8c5d0850d..5e4670c99 100644 --- a/wadsrc/static/zscript/actors/shared/dynlights.zs +++ b/wadsrc/static/zscript/actors/shared/dynlights.zs @@ -73,6 +73,7 @@ class DynamicLight : Actor +FIXMAPTHINGPOS +INVISIBLE +NOTONAUTOMAP + +LIGHTMULTALPHA } //========================================================================== From 3780c5910ae378fb2759e60554943cdcc024ce37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 1 May 2025 10:15:46 -0300 Subject: [PATCH 140/384] Bone Manip part 1 - bone setters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * split frame info calculation from RenderFrameModels * clean up fvec4 being used as quat in iqms * initial work on bone setting * implement bone setters * clean up FindModelFrame * refactor model overrides into its own function * refactor frame rendering into RenderModelFrame * split frame processing into ProcessModelFrame * refactor BoneOverride in preparation for translation/scale overrides * clean up macros * SetBoneTranslation/SetBoneScaling * GetBoneOffset * fix compilation on linux/mac (fuck you MSVC) * fix typo 😅 * make sure bone overrides are cleared on model switches, add ClearBoneOffsets to clear them manually * bone info getters * fix joint lengths, add joint dirs * serialize bone overrides * fix bone dir return type * GetBoneIndex/GetBoneCount * helper functions for working with non-quat angles * add mode enum, add more helper functions * fix up formatting --- src/common/engine/serializer.h | 17 + src/common/models/bonecomponents.h | 107 ++++ src/common/models/model.cpp | 2 +- src/common/models/model.h | 18 +- src/common/models/model_iqm.h | 54 +- src/common/models/models_iqm.cpp | 144 +++-- src/common/utility/TRS.h | 23 +- src/common/utility/matrix.cpp | 16 + src/common/utility/matrix.h | 2 + src/common/utility/quaternion.h | 19 +- src/playsim/actor.h | 17 +- src/playsim/p_actionfunctions.cpp | 637 ++++++++++++++++++++++- src/playsim/p_mobj.cpp | 25 + src/r_data/models.cpp | 472 ++++++++++------- src/r_data/models.h | 37 +- src/rendering/hwrenderer/hw_precache.cpp | 2 +- wadsrc/static/zscript/actors/actor.zs | 102 ++++ wadsrc/static/zscript/constants.zs | 7 + 18 files changed, 1422 insertions(+), 279 deletions(-) diff --git a/src/common/engine/serializer.h b/src/common/engine/serializer.h index fb906aee8..d72e44440 100644 --- a/src/common/engine/serializer.h +++ b/src/common/engine/serializer.h @@ -22,6 +22,7 @@ union FRenderStyle; class DObject; class FTextureID; struct FTranslationID; +struct BoneOverride; inline bool nullcmp(const void *buffer, size_t length) { @@ -253,6 +254,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, struct AnimModelOverri FSerializer &Serialize(FSerializer &arc, const char *key, ModelAnim &ao, ModelAnim *def); FSerializer &Serialize(FSerializer &arc, const char *key, ModelAnimFrame &ao, ModelAnimFrame *def); FSerializer &Serialize(FSerializer& arc, const char* key, FTranslationID& value, FTranslationID* defval); +FSerializer &Serialize(FSerializer& arc, const char* key, BoneOverride& value, BoneOverride* defval); void SerializeFunctionPointer(FSerializer &arc, const char *key, FunctionPointerValue *&p); @@ -347,6 +349,16 @@ inline FSerializer &Serialize(FSerializer &arc, const char *key, DVector3 &p, DV return arc.Array(key, &p[0], def? &(*def)[0] : nullptr, 3, true); } +inline FSerializer &Serialize(FSerializer &arc, const char *key, DVector4 &p, DVector4 *def) +{ + return arc.Array(key, &p[0], def? &(*def)[0] : nullptr, 4, true); +} + +inline FSerializer& Serialize(FSerializer& arc, const char* key, DQuaternion& p, DQuaternion* def) +{ + return arc.Array(key, &p[0], def ? &(*def)[0] : nullptr, 4, true); +} + inline FSerializer &Serialize(FSerializer &arc, const char *key, DRotator &p, DRotator *def) { return arc.Array(key, &p[0], def? &(*def)[0] : nullptr, 3, true); @@ -362,6 +374,11 @@ inline FSerializer& Serialize(FSerializer& arc, const char* key, FVector4& p, FV return arc.Array(key, &p[0], def ? &(*def)[0] : nullptr, 4, true); } +inline FSerializer& Serialize(FSerializer& arc, const char* key, FQuaternion& p, FQuaternion* def) +{ + return arc.Array(key, &p[0], def ? &(*def)[0] : nullptr, 4, true); +} + inline FSerializer& Serialize(FSerializer& arc, const char* key, FVector3& p, FVector3* def) { return arc.Array(key, &p[0], def ? &(*def)[0] : nullptr, 3, true); diff --git a/src/common/models/bonecomponents.h b/src/common/models/bonecomponents.h index f4f221f3b..db8957f8a 100644 --- a/src/common/models/bonecomponents.h +++ b/src/common/models/bonecomponents.h @@ -24,6 +24,113 @@ enum EModelAnimFlags MODELANIM_LOOP = 1 << 1, // animation loops, otherwise it stays on the last frame once it ends }; +FQuaternion InterpolateQuat(const FQuaternion &from, const FQuaternion &to, float t, float invt); + +template +inline constexpr T DefVal() +{ // [Jay] can't use T DefVal as a template parameter to BoneOverrideComponent without C++20 so we're stuck with this for now, TODO replace with template parameter once gzdoom switches to C++20 + if constexpr(std::is_same_v) + { + return FQuaternion(0,0,0,1); + } + return {}; +} + +template +struct BoneOverrideComponent +{ + int mode = 0; // 0 = no override, 1 = rotate, 2 = replace + int prev_mode = 0; + double switchtic = 0.0; + double interplen = 0.0; + T prev = DefVal(); + T cur = DefVal(); + + void Set(const T &newValue, double tic, double newInterplen, int newMode) + { + double prev_interp_amt = interplen > 0.0 ? std::clamp(((tic - switchtic) / interplen), 0.0, 1.0) : 1.0; + double prev_interp_amt_inv = 1.0 - prev_interp_amt; + + prev = mode > 0 ? Lerp(prev, cur, prev_interp_amt, prev_interp_amt_inv) : DefVal(); + + // might break slightly if value is switched from absolute to additive before interpolation finishes, + // but shouldn't matter too much, since people will probably mostly stick to one single mode per value + // so not worth the extra complexity (and adding the requirement of needing bone calculation for setters to work) to properly support it + prev_mode = (mode == 0 && prev_mode != 0 && prev_interp_amt_inv > 0.0) ? 1 : mode; + + cur = (newMode == 0 ? DefVal() : newValue); + switchtic = tic; + interplen = newInterplen; + mode = newMode; + } + + void Modify(T &value, double tic) const + { + + double lerp_amt = interplen > 0.0 ? std::clamp(((tic - switchtic) / interplen), 0.0, 1.0) : 1.0; + + if(mode > 0 || (prev_mode > 0 && lerp_amt < 1.0)) + { + T from = ModifyValue(value, prev, prev_mode); + T to = ModifyValue(value, cur, mode); + value = Lerp(from, to, lerp_amt, 1.0 - lerp_amt); + } + } + + T Get(const T &value, double tic) const + { + T newVal = value; + Modify(newVal, tic); + return newVal; + } + +private: + + inline static T ModifyValue(const T &orig, const T &cur, int mode) + { + if(mode == 0) return orig; + if(mode == 1) return Add(orig, cur); + return cur; + } + +}; + +struct BoneOverride +{ + static inline FQuaternion AddQuat(const FQuaternion &from, const FQuaternion &to) + { + return (from * to).Unit(); + } + + static inline FVector3 LerpVec3(const FVector3 &from, const FVector3 &to, float t, float invt) + { + return from * invt + to * t; + } + + static inline FVector3 AddVec3(const FVector3 &from, const FVector3 &to) + { + return from + to; + } + + BoneOverrideComponent translation; + BoneOverrideComponent rotation; + BoneOverrideComponent scaling; + + void Modify(TRS &trs, double tic) const + { + translation.Modify(trs.translation, tic); + rotation.Modify(trs.rotation, tic); + scaling.Modify(trs.scaling, tic); + } +}; + +struct BoneInfo +{ + TArray bones_anim_only; + TArray bones_with_override; + TArray positions; +}; + struct ModelAnim { int firstFrame = 0; diff --git a/src/common/models/model.cpp b/src/common/models/model.cpp index 0f1603415..bc1a02efd 100644 --- a/src/common/models/model.cpp +++ b/src/common/models/model.cpp @@ -45,7 +45,7 @@ TDeletingArray Models; TArray SpriteModelFrames; -TMap BaseSpriteModelFrames; +TMap BaseSpriteModelFrames; ///////////////////////////////////////////////////////////////////////////// diff --git a/src/common/models/model.h b/src/common/models/model.h index b6d3f4803..23381bed1 100644 --- a/src/common/models/model.h +++ b/src/common/models/model.h @@ -16,14 +16,16 @@ class FModelRenderer; class FGameTexture; class IModelVertexBuffer; class FModel; +class PClass; struct FSpriteModelFrame; FTextureID LoadSkin(const char* path, const char* fn); void FlushModels(); + extern TDeletingArray Models; extern TArray SpriteModelFrames; -extern TMap BaseSpriteModelFrames; +extern TMap BaseSpriteModelFrames; #define MD3_MAX_SURFACES 32 #define MIN_MODELS 4 @@ -86,6 +88,18 @@ public: virtual int FindFrame(const char * name, bool nodefault = false) = 0; + virtual int NumJoints() { return 0; } + virtual int FindJoint(FName name) { return -1; } + + virtual int GetJointParent(int joint) { return -1; } + virtual double GetJointLength(int joint) { return 0.0; } + virtual FName GetJointName(int joint) { return NAME_None; } + virtual FVector3 GetJointDir(int joint) { return FVector3(0.0f,0.0f,0.0f); } + + virtual void GetJointChildren(int joint, TArray &out) {} + + virtual void GetRootJoints(TArray &out) {} + // [RL0] these are used for decoupled iqm animations virtual int FindFirstFrame(FName name) { return FErr_NotFound; } virtual int FindLastFrame(FName name) { return FErr_NotFound; } @@ -99,7 +113,7 @@ public: virtual ModelAnimFrame PrecalculateFrame(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData) { return nullptr; }; - virtual const TArray* CalculateBones(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData) { return nullptr; }; + virtual const TArray* CalculateBones(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData, TArray *in, BoneInfo *out, double time) { return nullptr; }; void SetVertexBuffer(int type, IModelVertexBuffer *buffer) { mVBuf[type] = buffer; } IModelVertexBuffer *GetVertexBuffer(int type) const { return mVBuf[type]; } diff --git a/src/common/models/model_iqm.h b/src/common/models/model_iqm.h index cf221595f..9374e5f85 100644 --- a/src/common/models/model_iqm.h +++ b/src/common/models/model_iqm.h @@ -67,10 +67,11 @@ struct IQMAdjacency struct IQMJoint { - FString Name; + FName Name; int32_t Parent; // parent < 0 means this is a root bone + TArray Children; // indices of children FVector3 Translate; - FVector4 Quaternion; + FQuaternion Quaternion; FVector3 Scale; }; @@ -122,10 +123,10 @@ public: const TArray* AttachAnimationData() override; ModelAnimFrame PrecalculateFrame(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData) override; - const TArray* CalculateBones(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData) override; + const TArray* CalculateBones(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData, TArray *in, BoneInfo *out, double time) override; ModelAnimFramePrecalculatedIQM CalculateFrameIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData); - const TArray* CalculateBonesIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData); + const TArray* CalculateBonesIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData, TArray *in, BoneInfo *out, double time); private: void LoadGeometry(); @@ -140,11 +141,15 @@ private: int mLumpNum = -1; TMap NamedAnimations; + TMap NamedJoints; TArray Meshes; TArray Triangles; TArray Adjacency; TArray Joints; + + TArray RootJoints; + TArray Poses; TArray Anims; TArray Bounds; @@ -158,6 +163,47 @@ private: TArray baseframe; TArray inversebaseframe; TArray TRSData; +public: + int NumJoints() override { return Joints.Size(); } + int FindJoint(FName name) override + { + int *j = NamedJoints.CheckKey(name); + + return j ? *j : -1; + } + + int GetJointParent(int joint) override + { + return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Parent : -1; + } + + FName GetJointName(int joint) override + { + return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Name : FName(NAME_None); + } + + void GetRootJoints(TArray &out) override + { + out = RootJoints; + } + + void GetJointChildren(int joint, TArray &out) override + { + if(joint >= 0 && joint < Joints.Size()) + { + out = Joints[joint].Children; + } + } + + double GetJointLength(int joint) override + { + return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Translate.Length() : 0.0; + } + + FVector3 GetJointDir(int joint) override + { + return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Translate.Unit() : FVector3(0.0f,0.0f,0.0f); + } }; struct IQMReadErrorException { }; diff --git a/src/common/models/models_iqm.cpp b/src/common/models/models_iqm.cpp index d5fb6a152..719f15062 100644 --- a/src/common/models/models_iqm.cpp +++ b/src/common/models/models_iqm.cpp @@ -109,13 +109,26 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt } reader.SeekTo(ofs_joints); - for (IQMJoint& joint : Joints) + for (int i = 0; i < Joints.Size(); i++) { - joint.Name = reader.ReadName(text); + IQMJoint& joint = Joints[i]; + + FString name = reader.ReadName(text); + + joint.Name = name; + + if(!name.IsEmpty()) + { + NamedJoints.Insert(joint.Name, i); + } + joint.Parent = reader.ReadInt32(); joint.Translate.X = reader.ReadFloat(); joint.Translate.Y = reader.ReadFloat(); joint.Translate.Z = reader.ReadFloat(); + + int len = joint.Translate.Length(); + joint.Quaternion.X = reader.ReadFloat(); joint.Quaternion.Y = reader.ReadFloat(); joint.Quaternion.Z = reader.ReadFloat(); @@ -124,6 +137,19 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt joint.Scale.X = reader.ReadFloat(); joint.Scale.Y = reader.ReadFloat(); joint.Scale.Z = reader.ReadFloat(); + + if(joint.Parent < 0) + { + RootJoints.Push(i); + } + else if(joint.Parent >= Joints.Size()) + { + I_FatalError("Joint parent index out of bounds in IQM Model"); + } + else + { + Joints[joint.Parent].Children.Push(i); + } } reader.SeekTo(ofs_poses); @@ -188,7 +214,7 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt translate.Y = p.ChannelOffset[1]; if (p.ChannelMask & 0x02) translate.Y += reader.ReadUInt16() * p.ChannelScale[1]; translate.Z = p.ChannelOffset[2]; if (p.ChannelMask & 0x04) translate.Z += reader.ReadUInt16() * p.ChannelScale[2]; - FVector4 quaternion; + FQuaternion quaternion; quaternion.X = p.ChannelOffset[3]; if (p.ChannelMask & 0x08) quaternion.X += reader.ReadUInt16() * p.ChannelScale[3]; quaternion.Y = p.ChannelOffset[4]; if (p.ChannelMask & 0x10) quaternion.Y += reader.ReadUInt16() * p.ChannelScale[4]; quaternion.Z = p.ChannelOffset[5]; if (p.ChannelMask & 0x20) quaternion.Z += reader.ReadUInt16() * p.ChannelScale[5]; @@ -211,30 +237,7 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt { num_frames = 1; TRSData.Resize(num_joints); - - for (uint32_t j = 0; j < num_joints; j++) - { - FVector3 translate; - translate.X = Joints[j].Translate.X; - translate.Y = Joints[j].Translate.Y; - translate.Z = Joints[j].Translate.Z; - - FVector4 quaternion; - quaternion.X = Joints[j].Quaternion.X; - quaternion.Y = Joints[j].Quaternion.Y; - quaternion.Z = Joints[j].Quaternion.Z; - quaternion.W = Joints[j].Quaternion.W; - quaternion.MakeUnit(); - - FVector3 scale; - scale.X = Joints[j].Scale.X; - scale.Y = Joints[j].Scale.Y; - scale.Z = Joints[j].Scale.Z; - - TRSData[j].translation = translate; - TRSData[j].rotation = quaternion; - TRSData[j].scaling = scale; - } + std::copy(Joints.begin(), Joints.end(), TRSData.begin()); } reader.SeekTo(ofs_bounds); @@ -541,20 +544,29 @@ const TArray* IQMModel::AttachAnimationData() return &TRSData; } +FQuaternion InterpolateQuat(const FQuaternion &from, const FQuaternion &to, float t, float invt) +{ + FQuaternion rot = from * invt; + + if ((rot | to * t) < 0) + { + rot.X *= -1; + rot.Y *= -1; + rot.Z *= -1; + rot.W *= -1; + } + + rot += to * t; + + return rot.Unit(); +} + static TRS InterpolateBone(const TRS &from, const TRS &to, float t, float invt) { TRS bone; bone.translation = from.translation * invt + to.translation * t; - bone.rotation = from.rotation * invt; - - if ((bone.rotation | to.rotation * t) < 0) - { - bone.rotation.X *= -1; bone.rotation.Y *= -1; bone.rotation.Z *= -1; bone.rotation.W *= -1; - } - - bone.rotation += to.rotation * t; - bone.rotation.MakeUnit(); + bone.rotation = InterpolateQuat(from.rotation, to.rotation, t, invt); bone.scaling = from.scaling * invt + to.scaling * t; return bone; @@ -585,28 +597,34 @@ ModelAnimFrame IQMModel::PrecalculateFrame(const ModelAnimFrame &from, const Mod } } -const TArray* IQMModel::CalculateBones(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData) +const TArray* IQMModel::CalculateBones(const ModelAnimFrame &from, const ModelAnimFrameInterp &to, float inter, const TArray* animationData, TArray *in, BoneInfo *out, double time) { if(inter <= 0) { - return CalculateBonesIQM(to.frame1, to.frame2, to.inter, 0, -1.f, 0, -1.f, nullptr, animationData); + return CalculateBonesIQM(to.frame1, to.frame2, to.inter, 0, -1.f, 0, -1.f, nullptr, animationData, in, out, time); } else if(std::holds_alternative(from)) { auto &from_interp = std::get(from); - return CalculateBonesIQM(from_interp.frame2, to.frame2, inter, from_interp.frame1, from_interp.inter, to.frame1, to.inter, nullptr, animationData); + return CalculateBonesIQM(from_interp.frame2, to.frame2, inter, from_interp.frame1, from_interp.inter, to.frame1, to.inter, nullptr, animationData, in, out, time); } else if(std::holds_alternative(from)) { - return CalculateBonesIQM(0, to.frame2, inter, 0, -1.f, to.frame1, to.inter, &std::get(from), animationData); + return CalculateBonesIQM(0, to.frame2, inter, 0, -1.f, to.frame1, to.inter, &std::get(from), animationData, in, out, time); } else { - return CalculateBonesIQM(to.frame1, to.frame2, to.inter, 0, -1.f, 0, -1.f, nullptr, animationData); + return CalculateBonesIQM(to.frame1, to.frame2, to.inter, 0, -1.f, 0, -1.f, nullptr, animationData, in, out, time); } } +inline void ModifyBone(const BoneOverride& mod, TRS &bone, double time) +{ + mod.Modify(bone, time); +} + +// explicitly don't pass modelBoneOverrides when precalculating animation for interpolation, as it's applied _after_ animation ModelAnimFramePrecalculatedIQM IQMModel::CalculateFrameIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData) { ModelAnimFramePrecalculatedIQM out; @@ -614,7 +632,7 @@ ModelAnimFramePrecalculatedIQM IQMModel::CalculateFrameIQM(int frame1, int frame out.precalcBones.Resize(Joints.Size()); - if (Joints.Size() > 0) + if (Joints.Size() > 0 && animationFrames.Size() > 0) { int numbones = Joints.SSize(); @@ -661,12 +679,25 @@ ModelAnimFramePrecalculatedIQM IQMModel::CalculateFrameIQM(int frame1, int frame return out; } -const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData) +const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData, TArray *in, BoneInfo *out, double time) { const TArray& animationFrames = animationData ? *animationData : TRSData; - if (Joints.Size() > 0) + + TArray* outMatrix = out ? &out->positions : &boneData; + + int numbones = Joints.SSize(); + outMatrix->Resize(numbones); + + if(out) + { + out->bones_anim_only.Resize(numbones); + out->bones_with_override.Resize(numbones); + } + + if(in && in->size() != Joints.Size()) in = nullptr; + + if (numbones > 0 && animationFrames.Size() > 0) { - int numbones = Joints.SSize(); frame1 = clamp(frame1, 0, (animationFrames.SSize() - 1) / numbones); frame2 = clamp(frame2, 0, (animationFrames.SSize() - 1) / numbones); @@ -689,8 +720,6 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa 0.0f, 0.0f, 0.0f, 1.0f }; - boneData.Resize(numbones); - for (int i = 0; i < numbones; i++) { TRS prev; @@ -721,16 +750,33 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa bone = inter < 0 ? animationFrames[offset1 + i] : InterpolateBone(prev, next , inter, invt); } + if(out) + { + out->bones_anim_only[i] = bone; + + if(in) + { + (*in)[i].Modify(bone, time); + } + + out->bones_with_override[i] = bone; + } + else if(in) + { + (*in)[i].Modify(bone, time); + } + VSMatrix m; m.loadIdentity(); m.translate(bone.translation.X, bone.translation.Y, bone.translation.Z); m.multQuaternion(bone.rotation); m.scale(bone.scaling.X, bone.scaling.Y, bone.scaling.Z); - VSMatrix& result = boneData[i]; + VSMatrix& result = (*outMatrix)[i]; if (Joints[i].Parent >= 0) { - result = boneData[Joints[i].Parent]; + assert(Joints[i].Parent < i); + result = (*outMatrix)[Joints[i].Parent]; result.multMatrix(swapYZ); result.multMatrix(baseframe[Joints[i].Parent]); result.multMatrix(m); diff --git a/src/common/utility/TRS.h b/src/common/utility/TRS.h index c39a870ad..dd72ab5c5 100644 --- a/src/common/utility/TRS.h +++ b/src/common/utility/TRS.h @@ -33,24 +33,27 @@ #pragma once #include "vectors.h" +#include "quaternion.h" class TRS { public: - FVector3 translation; - FVector4 rotation; - FVector3 scaling; + FVector3 translation = FVector3(0,0,0); + FQuaternion rotation = FQuaternion::Identity(); + FVector3 scaling = FVector3(0,0,0); - TRS() + bool operator==(const TRS& other) const { - translation = FVector3(0,0,0); - rotation = FVector4(0,0,0,1); - scaling = FVector3(0,0,0); + return other.translation == translation && other.rotation == rotation && other.scaling == scaling; } - bool Equals(TRS& compare) - { - return compare.translation == this->translation && compare.rotation == this->rotation && compare.scaling == this->scaling; + template + TRS& operator=(const T& other) + { // templated because IQMJoint is defined in model_iqm.h + translation = other.Translate; + rotation = other.Quaternion; + scaling = other.Scale; + return *this; } }; diff --git a/src/common/utility/matrix.cpp b/src/common/utility/matrix.cpp index 3d84d48e7..eb6abdb1f 100644 --- a/src/common/utility/matrix.cpp +++ b/src/common/utility/matrix.cpp @@ -114,6 +114,22 @@ void VSMatrix::multQuaternion(const TVector4& q) multMatrix(m); } +void VSMatrix::multQuaternion(const TQuaternion& q) +{ + FLOATTYPE m[16] = { FLOATTYPE(0.0) }; + m[0 * 4 + 0] = FLOATTYPE(1.0) - FLOATTYPE(2.0) * q.Y * q.Y - FLOATTYPE(2.0) * q.Z * q.Z; + m[1 * 4 + 0] = FLOATTYPE(2.0) * q.X * q.Y - FLOATTYPE(2.0) * q.W * q.Z; + m[2 * 4 + 0] = FLOATTYPE(2.0) * q.X * q.Z + FLOATTYPE(2.0) * q.W * q.Y; + m[0 * 4 + 1] = FLOATTYPE(2.0) * q.X * q.Y + FLOATTYPE(2.0) * q.W * q.Z; + m[1 * 4 + 1] = FLOATTYPE(1.0) - FLOATTYPE(2.0) * q.X * q.X - FLOATTYPE(2.0) * q.Z * q.Z; + m[2 * 4 + 1] = FLOATTYPE(2.0) * q.Y * q.Z - FLOATTYPE(2.0) * q.W * q.X; + m[0 * 4 + 2] = FLOATTYPE(2.0) * q.X * q.Z - FLOATTYPE(2.0) * q.W * q.Y; + m[1 * 4 + 2] = FLOATTYPE(2.0) * q.Y * q.Z + FLOATTYPE(2.0) * q.W * q.X; + m[2 * 4 + 2] = FLOATTYPE(1.0) - FLOATTYPE(2.0) * q.X * q.X - FLOATTYPE(2.0) * q.Y * q.Y; + m[3 * 4 + 3] = FLOATTYPE(1.0); + multMatrix(m); +} + // gl LoadMatrix implementation void diff --git a/src/common/utility/matrix.h b/src/common/utility/matrix.h index 265981e1b..74d764bb2 100644 --- a/src/common/utility/matrix.h +++ b/src/common/utility/matrix.h @@ -22,6 +22,7 @@ #include #include "vectors.h" +#include "quaternion.h" #ifdef USE_DOUBLE typedef double FLOATTYPE; @@ -54,6 +55,7 @@ class VSMatrix { multMatrix(aMatrix.mMatrix); } void multQuaternion(const TVector4& q); + void multQuaternion(const TQuaternion& q); void loadMatrix(const FLOATTYPE *aMatrix); #ifdef USE_DOUBLE void loadMatrix(const float *aMatrix); diff --git a/src/common/utility/quaternion.h b/src/common/utility/quaternion.h index 1437b56be..a1c506e6e 100644 --- a/src/common/utility/quaternion.h +++ b/src/common/utility/quaternion.h @@ -13,7 +13,7 @@ public: TQuaternion() = default; - TQuaternion(vec_t x, vec_t y, vec_t z, vec_t w) + constexpr TQuaternion(vec_t x, vec_t y, vec_t z, vec_t w) : X(x), Y(y), Z(z), W(w) { } @@ -40,6 +40,17 @@ public: Z = Y = X = W = 0; } + void MakeIdentity() + { + Z = Y = X = 0; + W = 1; + } + + static constexpr TQuaternion Identity() + { + return {0,0,0,1}; + } + bool isZero() const { return X == 0 && Y == 0 && Z == 0 && W == 0; @@ -346,6 +357,12 @@ public: return (from * scale0 + to * scale1).Unit(); } } + + template + explicit operator TVector4() + { + return TVector4(U(X),U(Y),U(Z),U(W)); + } }; typedef TQuaternion FQuaternion; diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 00993106d..66b85ea79 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -733,14 +733,15 @@ class DActorModelData : public DObject { DECLARE_CLASS(DActorModelData, DObject); public: - PClass * modelDef; - TArray models; - TArray skinIDs; - TArray animationIDs; - TArray modelFrameGenerators; - int flags; - int overrideFlagsSet; - int overrideFlagsClear; + PClass * modelDef; + TArray models; + TArray skinIDs; + TArray animationIDs; + TArray modelFrameGenerators; + TArray> modelBoneOverrides; + int flags; + int overrideFlagsSet; + int overrideFlagsClear; ModelAnim curAnim; ModelAnimFrame prevAnim; // used for interpolation when switching anims diff --git a/src/playsim/p_actionfunctions.cpp b/src/playsim/p_actionfunctions.cpp index 1c5a91d6d..98897d873 100644 --- a/src/playsim/p_actionfunctions.cpp +++ b/src/playsim/p_actionfunctions.cpp @@ -5113,6 +5113,7 @@ static void CleanupModelData(AActor * mobj) && mobj->modelData->modelFrameGenerators.Size() == 0 && mobj->modelData->skinIDs.Size() == 0 && mobj->modelData->animationIDs.Size() == 0 + && mobj->modelData->modelBoneOverrides.Size() == 0 && mobj->modelData->modelDef == nullptr &&(mobj->modelData->flags & ~MODELDATA_HADMODEL) == 0 ) { @@ -5122,6 +5123,626 @@ static void CleanupModelData(AActor * mobj) } } +FQuaternion InterpolateQuat(const FQuaternion &from, const FQuaternion &to, float t, float invt); + +static void SetModelBoneRotationInternal(AActor * self, FModel * mdl, int model_index, int index, FQuaternion rotation, int mode, double interpolation_duration, double switchTic) +{ + if(self->modelData->modelBoneOverrides.Size() <= model_index) self->modelData->modelBoneOverrides.Resize(model_index + 1); + + self->modelData->modelBoneOverrides[model_index].Resize(mdl->NumJoints()); + + self->modelData->modelBoneOverrides[model_index][index].rotation.Set(rotation, switchTic, interpolation_duration, mode); +} + +template +FModel * SetGetBoneShared(AActor * self, int model_index) +{ + if(!(self->flags9 & MF9_DECOUPLEDANIMATIONS)) + { + ThrowAbortException(X_OTHER, isSet ? "Cannot set bone offset for non-decoupled actors" : (isOffset ? "Cannot get bone for non-decoupled actors" : "Cannot get bone offset for non-decoupled actors")); + } + + if(!BaseSpriteModelFrames.CheckKey(self->GetClass())) + { + ThrowAbortException(X_OTHER, "Actor class is missing a MODELDEF definition or a MODELDEF BaseFrame"); + } + + EnsureModelData(self); + + if(self->modelData->models.Size() > model_index && self->modelData->models[model_index].modelID >= 0 && self->modelData->models[model_index].modelID < Models.Size()) + { + return Models[self->modelData->models[model_index].modelID]; + } + else if(BaseSpriteModelFrames[self->GetClass()].modelIDs.Size() > model_index) + { + return Models[BaseSpriteModelFrames[self->GetClass()].modelIDs[model_index]]; + } + else + { + ThrowAbortException(X_OTHER, "Model Index out of range"); + } +} + +template +FModel * SetGetBoneSharedIndex(AActor * self, int model_index, int &bone_index, FName * bone_name) +{ + FModel * mdl = SetGetBoneShared(self, model_index); + + if(bone_name) + { + bone_index = mdl->FindJoint(*bone_name); + if(bone_index < 0 || bone_index >= mdl->NumJoints()) + { + Printf(PRINT_NONOTIFY, "Could not find bone '%s'", bone_name->GetChars()); + return nullptr; + } + } + else if(bone_index < 0 || bone_index >= mdl->NumJoints()) + { + ThrowAbortException(X_OTHER, "bone index out of range"); + } + + if(self->modelData->modelBoneOverrides.Size() <= model_index) self->modelData->modelBoneOverrides.Resize(model_index + 1); + + self->modelData->modelBoneOverrides[model_index].Resize(mdl->NumJoints()); + + return mdl; +} + +FModel * SetBoneOffsetShared(AActor * self, int model_index, int &bone_index, FName * bone_name, int mode, double &interpolation_duration) +{ + if(interpolation_duration < 0) interpolation_duration = 0; + + if(mode < 0 || mode > 2) + { + ThrowAbortException(X_OTHER, "Invalid mode for setbone"); + } + + return SetGetBoneSharedIndex(self, model_index, bone_index, bone_name); +} + +FModel * GetBoneOffsetShared(AActor * self, int model_index, int &bone_index, FName * bone_name) +{ + return SetGetBoneSharedIndex(self, model_index, bone_index, bone_name); +} + +FModel * GetBoneShared(AActor * self, int model_index, int &bone_index, FName * bone_name) +{ + return SetGetBoneSharedIndex(self, model_index, bone_index, bone_name); +} + +//================================================ +// +// SetBoneRotation +// +//================================================ + +static void SetModelBoneRotationNative(AActor * self, int model_index, int bone_index, double rot_x, double rot_y, double rot_z, double rot_w, int mode, double interpolation_duration, double ticFrac) +{ + FModel * mdl = SetBoneOffsetShared(self, model_index, bone_index, nullptr, mode, interpolation_duration); + + if(!mdl) return; + + self->modelData->modelBoneOverrides[model_index][bone_index].rotation.Set(FQuaternion(rot_x, rot_y, rot_z, rot_w), self->Level->totaltime + ticFrac, interpolation_duration, mode); +} + +static void SetBoneRotationNative(AActor * self, int bone_index, double rot_x, double rot_y, double rot_z, double rot_w, int mode, double interpolation_duration, double ticFrac) +{ + SetModelBoneRotationNative(self, 0, bone_index, rot_x, rot_y, rot_z, rot_w, mode, interpolation_duration, ticFrac); +} + +static void SetModelNamedBoneRotationNative(AActor * self, int model_index, int boneName_i, double rot_x, double rot_y, double rot_z, double rot_w, int mode, double interpolation_duration, double ticFrac) +{ + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = SetBoneOffsetShared(self, model_index, bone_index, &bone_name, mode, interpolation_duration); + + if(!mdl) return; + + self->modelData->modelBoneOverrides[model_index][bone_index].rotation.Set(FQuaternion(rot_x, rot_y, rot_z, rot_w), self->Level->totaltime + ticFrac, interpolation_duration, mode); +} + +static void SetNamedBoneRotationNative(AActor * self, int boneName_i, double rot_x, double rot_y, double rot_z, double rot_w, int mode, double interpolation_duration, double ticFrac) +{ + SetModelNamedBoneRotationNative(self, 0, boneName_i, rot_x, rot_y, rot_z, rot_w, mode, interpolation_duration, ticFrac); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetBoneRotation, SetBoneRotationNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + PARAM_FLOAT(rot_x); + PARAM_FLOAT(rot_y); + PARAM_FLOAT(rot_z); + PARAM_FLOAT(rot_w); + PARAM_INT(mode); + PARAM_FLOAT(interplen); + + SetBoneRotationNative(self, boneindex, rot_x, rot_y, rot_z, rot_w, mode, interplen, 1.0); + + return 0; +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetNamedBoneRotation, SetNamedBoneRotationNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + PARAM_FLOAT(rot_x); + PARAM_FLOAT(rot_y); + PARAM_FLOAT(rot_z); + PARAM_FLOAT(rot_w); + PARAM_INT(mode); + PARAM_FLOAT(interplen); + + SetNamedBoneRotationNative(self, bonename.GetIndex(), rot_x, rot_y, rot_z, rot_w, mode, interplen, 1.0); + + return 0; +} + +//================================================ +// +// SetBoneTranslation +// +//================================================ + +static void SetModelBoneTranslationNative(AActor * self, int model_index, int bone_index, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + FModel * mdl = SetBoneOffsetShared(self, model_index, bone_index, nullptr, mode, interpolation_duration); + + if(!mdl) return; + + self->modelData->modelBoneOverrides[model_index][bone_index].translation.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); +} + +static void SetBoneTranslationNative(AActor * self, int bone_index, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + SetModelBoneTranslationNative(self, 0, bone_index, rot_x, rot_y, rot_z, mode, interpolation_duration, ticFrac); +} + +static void SetModelNamedBoneTranslationNative(AActor * self, int model_index, int boneName_i, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = SetBoneOffsetShared(self, model_index, bone_index, &bone_name, mode, interpolation_duration); + + if(!mdl) return; + + self->modelData->modelBoneOverrides[model_index][bone_index].translation.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); +} + +static void SetNamedBoneTranslationNative(AActor * self, int boneName_i, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + SetModelNamedBoneTranslationNative(self, 0, boneName_i, rot_x, rot_y, rot_z, mode, interpolation_duration, ticFrac); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetBoneTranslation, SetBoneTranslationNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + PARAM_FLOAT(rot_x); + PARAM_FLOAT(rot_y); + PARAM_FLOAT(rot_z); + PARAM_INT(mode); + PARAM_FLOAT(interplen); + + SetBoneTranslationNative(self, boneindex, rot_x, rot_y, rot_z, mode, interplen, 1.0); + + return 0; +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetNamedBoneTranslation, SetNamedBoneTranslationNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + PARAM_FLOAT(rot_x); + PARAM_FLOAT(rot_y); + PARAM_FLOAT(rot_z); + PARAM_INT(mode); + PARAM_FLOAT(interplen); + + SetNamedBoneTranslationNative(self, bonename.GetIndex(), rot_x, rot_y, rot_z, mode, interplen, 1.0); + + return 0; +} + +//================================================ +// +// SetBoneScaling +// +//================================================ + +static void SetModelBoneScalingNative(AActor * self, int model_index, int bone_index, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + FModel * mdl = SetBoneOffsetShared(self, model_index, bone_index, nullptr, mode, interpolation_duration); + + if(!mdl) return; + + self->modelData->modelBoneOverrides[model_index][bone_index].scaling.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); +} + +static void SetBoneScalingNative(AActor * self, int bone_index, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + SetModelBoneScalingNative(self, 0, bone_index, rot_x, rot_y, rot_z, mode, interpolation_duration, ticFrac); +} + +static void SetModelNamedBoneScalingNative(AActor * self, int model_index, int boneName_i, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = SetBoneOffsetShared(self, model_index, bone_index, &bone_name, mode, interpolation_duration); + + if(!mdl) return; + + self->modelData->modelBoneOverrides[model_index][bone_index].scaling.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); +} + +static void SetNamedBoneScalingNative(AActor * self, int boneName_i, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) +{ + SetModelNamedBoneScalingNative(self, 0, boneName_i, rot_x, rot_y, rot_z, mode, interpolation_duration, ticFrac); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetBoneScaling, SetBoneScalingNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + PARAM_FLOAT(rot_x); + PARAM_FLOAT(rot_y); + PARAM_FLOAT(rot_z); + PARAM_INT(mode); + PARAM_FLOAT(interplen); + + SetBoneScalingNative(self, boneindex, rot_x, rot_y, rot_z, mode, interplen, 1.0); + + return 0; +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetNamedBoneScaling, SetNamedBoneScalingNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + PARAM_FLOAT(rot_x); + PARAM_FLOAT(rot_y); + PARAM_FLOAT(rot_z); + PARAM_INT(mode); + PARAM_FLOAT(interplen); + + SetNamedBoneScalingNative(self, bonename.GetIndex(), rot_x, rot_y, rot_z, mode, interplen, 1.0); + + return 0; +} + + +//================================================ +// +// ClearBoneOffsets +// +//================================================ + +static void ClearBoneOffsetsNative(AActor * self) +{ + if(self->modelData) self->modelData->modelBoneOverrides[0].Clear(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, ClearBoneOffsets, ClearBoneOffsetsNative) +{ + PARAM_SELF_PROLOGUE(AActor); + + ClearBoneOffsetsNative(self); + + return 0; +} + +//================================================ +// +// GetBoneOffset +// +//================================================ + +DEFINE_ACTION_FUNCTION(AActor, GetBoneOffset) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + + FModel * mdl = GetBoneOffsetShared(self, 0, bone_index, nullptr); + + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl) + { + auto &mod = self->modelData->modelBoneOverrides[0][bone_index]; + + translation = DVector3(mod.translation.Get(FVector3(0,0,0), self->Level->totaltime + 1.0)); + rotation = DVector4(mod.rotation.Get(FQuaternion(0,0,0,1), self->Level->totaltime + 1.0)); + scaling = DVector3(mod.scaling.Get(FVector3(0,0,0), self->Level->totaltime + 1.0)); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneOffset) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + + int bone_index; + + FModel * mdl = GetBoneOffsetShared(self, 0, bone_index, &bone_name); + + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl) + { + auto &mod = self->modelData->modelBoneOverrides[0][bone_index]; + + translation = DVector3(mod.translation.Get(FVector3(0,0,0), self->Level->totaltime + 1.0)); + rotation = DVector4(mod.rotation.Get(FQuaternion(0,0,0,1), self->Level->totaltime + 1.0)); + scaling = DVector3(mod.scaling.Get(FVector3(0,0,0), self->Level->totaltime + 1.0)); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; +} + + +//================================================ +// +// Bone Info Getters +// +//================================================ + +static void GetRootBonesNative(AActor * self, TArray *out) +{ + if(out) + { + FModel * mdl = SetGetBoneShared(self, 0); + + mdl->GetRootJoints(*out); + } +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetRootBones, GetRootBonesNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_POINTER(out, TArray); + + GetRootBonesNative(self, out); + + return 0; +} + +static int GetBoneNameNative(AActor * self, int bone_index) +{ + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + return mdl->GetJointName(bone_index).GetIndex(); +} + +static int GetBoneIndexNative(AActor * self, int boneName_i) +{ + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + return bone_index; +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneName, GetBoneNameNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + + ACTION_RETURN_INT(GetBoneNameNative(self, boneindex)); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneIndex, GetBoneIndexNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + + ACTION_RETURN_INT(GetBoneIndexNative(self, bonename.GetIndex())); +} + +static int GetBoneParentNative(AActor * self, int bone_index) +{ + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + return mdl->GetJointParent(bone_index); +} + +static int GetNamedBoneParentNative(AActor * self, int boneName_i) +{ + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + return mdl->GetJointParent(bone_index); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneParent, GetBoneParentNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + + ACTION_RETURN_INT(GetBoneParentNative(self, boneindex)); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetNamedBoneParent, GetNamedBoneParentNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + + ACTION_RETURN_INT(GetNamedBoneParentNative(self, bonename.GetIndex())); +} + +static void GetBoneChildrenNative(AActor * self, int bone_index, TArray *out) +{ + if(out) + { + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + mdl->GetJointChildren(bone_index, *out); + } +} + +static void GetNamedBoneChildrenNative(AActor * self, int boneName_i, TArray *out) +{ + if(out) + { + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + mdl->GetJointChildren(bone_index, *out); + } +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneChildren, GetBoneChildrenNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + PARAM_POINTER(out, TArray); + + GetBoneChildrenNative(self, boneindex, out); + + return 0; +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetNamedBoneChildren, GetNamedBoneChildrenNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + PARAM_POINTER(out, TArray); + + GetNamedBoneChildrenNative(self, bonename.GetIndex(), out); + + return 0; +} + +static double GetBoneLengthNative(AActor * self, int bone_index) +{ + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + return mdl->GetJointLength(bone_index); +} + +static double GetNamedBoneLengthNative(AActor * self, int boneName_i) +{ + FName bone_name {ENamedName(boneName_i)}; + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + return mdl->GetJointLength(bone_index); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneLength, GetBoneLengthNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(boneindex); + + ACTION_RETURN_FLOAT(GetBoneLengthNative(self, boneindex)); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetNamedBoneLength, GetNamedBoneLengthNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bonename); + + ACTION_RETURN_FLOAT(GetNamedBoneLengthNative(self, bonename.GetIndex())); +} + +DEFINE_ACTION_FUNCTION(AActor, GetBoneDir) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + ACTION_RETURN_VEC3(DVector3(mdl->GetJointDir(bone_index))); +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneDir) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + ACTION_RETURN_VEC3(DVector3(mdl->GetJointDir(bone_index))); +} + +static int GetBoneCountNative(AActor * self) +{ + FModel * mdl = SetGetBoneShared(self, 0); + + return mdl->NumJoints(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneCount, GetBoneCountNative) +{ + PARAM_SELF_PROLOGUE(AActor); + + ACTION_RETURN_INT(GetBoneCountNative(self)); +} + + + + + +//================================================ +// SetAnimation +//================================================ + enum ESetAnimationFlags { SAF_INSTANT = 1 << 0, @@ -5449,6 +6070,7 @@ void ChangeModelNative( } surfaceSkins.Push(skindata); mobj->modelData->models.Push({queryModel, std::move(surfaceSkins)}); + mobj->modelData->modelFrameGenerators.Push(generatorindex); } else @@ -5456,6 +6078,11 @@ void ChangeModelNative( mobj->modelData->models.Push({queryModel, {}}); mobj->modelData->modelFrameGenerators.Push(generatorindex); } + + if(queryModel != -1 && mobj->modelData->modelBoneOverrides.Size() > modelindex) + { + mobj->modelData->modelBoneOverrides[modelindex].Clear(); + } } else { @@ -5475,7 +6102,15 @@ void ChangeModelNative( mobj->modelData->models[modelindex].surfaceSkinIDs[skinindex] = skindata; } } - if(queryModel != -1) mobj->modelData->models[modelindex].modelID = queryModel; + if(queryModel != -1) + { + mobj->modelData->models[modelindex].modelID = queryModel; + + if(mobj->modelData->modelBoneOverrides.Size() > modelindex) + { + mobj->modelData->modelBoneOverrides[modelindex].Clear(); + } + } if(generatorindex != -1) mobj->modelData->modelFrameGenerators[modelindex] = generatorindex; } diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 8c804d55b..00d20722b 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -1764,6 +1764,30 @@ void SerializeModelID(FSerializer &arc, const char *key, int &id) } } +template +static FSerializer &SerializeBoneOverrideComponent(FSerializer &arc, const char *key, T &comp) +{ + arc.BeginObject(key); + arc("mode", comp.mode); + arc("prev_mode", comp.prev_mode); + arc("switchtic", comp.switchtic); + arc("interplen", comp.interplen); + arc("prev", comp.prev); + arc("cur", comp.cur); + arc.EndObject(); + return arc; +} + +FSerializer &Serialize(FSerializer &arc, const char *key, BoneOverride &mod, BoneOverride *def) +{ + arc.BeginObject(key); + SerializeBoneOverrideComponent(arc, "translation", mod.translation); + SerializeBoneOverrideComponent(arc, "rotation", mod.rotation); + SerializeBoneOverrideComponent(arc, "scaling", mod.scaling); + arc.EndObject(); + return arc; +} + FSerializer &Serialize(FSerializer &arc, const char *key, ModelOverride &mo, ModelOverride *def) { arc.BeginObject(key); @@ -1869,6 +1893,7 @@ void DActorModelData::Serialize(FSerializer& arc) ("skinIDs", skinIDs) ("animationIDs", animationIDs) ("modelFrameGenerators", modelFrameGenerators) + ("modelBoneOverrides", modelBoneOverrides) ("flags", flags) ("overrideFlagsSet", overrideFlagsSet) ("overrideFlagsClear", overrideFlagsClear) diff --git a/src/r_data/models.cpp b/src/r_data/models.cpp index bc9473b07..8226cf411 100644 --- a/src/r_data/models.cpp +++ b/src/r_data/models.cpp @@ -313,19 +313,18 @@ void calcFrames(const ModelAnim &curAnim, double tic, ModelAnimFrameInterp &to, } } -void RenderFrameModels(FModelRenderer *renderer, FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, FTranslationID translation, AActor* actor) +CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, DActorModelData* data, AActor* actor, bool is_decoupled, double tic) { // [BB] Frame interpolation: Find the FSpriteModelFrame smfNext which follows after smf in the animation // and the scalar value inter ( element of [0,1) ), both necessary to determine the interpolated frame. - int smf_flags = smf->getFlags(actor->modelData); + int smf_flags = smf->getFlags(data); const FSpriteModelFrame * smfNext = nullptr; float inter = 0.; - bool is_decoupled = (actor->flags9 & MF9_DECOUPLEDANIMATIONS); - ModelAnimFrameInterp decoupled_frame; + ModelAnimFrame * decoupled_frame_prev = nullptr; // if prev_frame == -1: interpolate(main_frame, next_frame, inter), else: interpolate(interpolate(main_prev_frame, main_frame, inter_main), interpolate(next_prev_frame, next_frame, inter_next), inter) // 4-way interpolation is needed to interpolate animation switches between animations that aren't 35hz @@ -333,15 +332,10 @@ void RenderFrameModels(FModelRenderer *renderer, FLevelLocals *Level, const FSpr if(is_decoupled) { smfNext = smf = &BaseSpriteModelFrames[actor->GetClass()]; - if(actor->modelData && !(actor->modelData->curAnim.flags & MODELANIM_NONE)) + if(data && !(data->curAnim.flags & MODELANIM_NONE)) { - double tic = actor->Level->totaltime; - if ((ConsoleState == c_up || ConsoleState == c_rising) && (menuactive == MENU_Off || menuactive == MENU_OnNoPause) && !actor->isFrozen()) - { - tic += I_GetTimeFrac(); - } - - calcFrames(actor->modelData->curAnim, tic, decoupled_frame, inter); + calcFrames(data->curAnim, tic, decoupled_frame, inter); + decoupled_frame_prev = &data->prevAnim; } } else if (gl_interpolate_model_frames && !(smf_flags & MDL_NOINTERPOLATION)) @@ -388,173 +382,236 @@ void RenderFrameModels(FModelRenderer *renderer, FLevelLocals *Level, const FSpr unsigned modelsamount = smf->modelsAmount; //[SM] - if we added any models for the frame to also render, then we also need to update modelsAmount for this smf - if (actor->modelData != nullptr) + if (data != nullptr) { - if (actor->modelData->models.Size() > modelsamount) - modelsamount = actor->modelData->models.Size(); + if (data->models.Size() > modelsamount) + modelsamount = data->models.Size(); } - TArray surfaceskinids; + return + { + smf_flags, + smfNext, + inter, + is_decoupled, + decoupled_frame, + decoupled_frame_prev, + modelsamount + }; +} + +bool CalcModelOverrides(int i, const FSpriteModelFrame *smf, DActorModelData* data, const CalcModelFrameInfo &info, ModelDrawInfo &out, bool is_decoupled) +{ + //reset drawinfo + out.modelid = -1; + out.animationid = -1; + out.modelframe = -1; + out.modelframenext = -1; + out.skinid.SetNull(); + out.surfaceskinids.Clear(); + + if (data) + { + //modelID + if (data->models.Size() > i && data->models[i].modelID >= 0) + { + out.modelid = data->models[i].modelID; + } + else if(data->models.Size() > i && data->models[i].modelID == -2) + { + return false; + } + else if(smf->modelsAmount > i) + { + out.modelid = smf->modelIDs[i]; + } + + //animationID + if (data->animationIDs.Size() > i && data->animationIDs[i] >= 0) + { + out.animationid = data->animationIDs[i]; + } + else if(smf->modelsAmount > i) + { + out.animationid = smf->animationIDs[i]; + } + if(!is_decoupled) + { + //modelFrame + if (data->modelFrameGenerators.Size() > i + && (unsigned)data->modelFrameGenerators[i] < info.modelsamount + && smf->modelframes[data->modelFrameGenerators[i]] >= 0 + ) { + out.modelframe = smf->modelframes[data->modelFrameGenerators[i]]; + + if (info.smfNext) + { + if(info.smfNext->modelframes[data->modelFrameGenerators[i]] >= 0) + { + out.modelframenext = info.smfNext->modelframes[data->modelFrameGenerators[i]]; + } + else + { + out.modelframenext = info.smfNext->modelframes[i]; + } + } + } + else if(smf->modelsAmount > i) + { + out.modelframe = smf->modelframes[i]; + if (info.smfNext) out.modelframenext = info.smfNext->modelframes[i]; + } + } + + //skinID + if (data->skinIDs.Size() > i && data->skinIDs[i].isValid()) + { + out.skinid = data->skinIDs[i]; + } + else if(smf->modelsAmount > i) + { + out.skinid = smf->skinIDs[i]; + } + + //surfaceSkinIDs + if(data->models.Size() > i && data->models[i].surfaceSkinIDs.Size() > 0) + { + unsigned sz1 = smf->surfaceskinIDs.Size(); + unsigned sz2 = data->models[i].surfaceSkinIDs.Size(); + unsigned start = i * MD3_MAX_SURFACES; + + out.surfaceskinids = data->models[i].surfaceSkinIDs; + out.surfaceskinids.Resize(MD3_MAX_SURFACES); + + for (unsigned surface = 0; surface < MD3_MAX_SURFACES; surface++) + { + if (sz2 > surface && (data->models[i].surfaceSkinIDs[surface].isValid())) + { + continue; + } + if((surface + start) < sz1) + { + out.surfaceskinids[surface] = smf->surfaceskinIDs[surface + start]; + } + else + { + out.surfaceskinids[surface].SetNull(); + } + } + } + } + else + { + out.modelid = smf->modelIDs[i]; + out.animationid = smf->animationIDs[i]; + out.modelframe = smf->modelframes[i]; + if (info.smfNext) out.modelframenext = info.smfNext->modelframes[i]; + out.skinid = smf->skinIDs[i]; + } + + return (out.modelid >= 0 && out.modelid < Models.size()); +} + + +const TArray * ProcessModelFrame(FModel * animation, bool nextFrame, int i, const FSpriteModelFrame *smf, DActorModelData* modelData, const CalcModelFrameInfo &frameinfo, ModelDrawInfo &drawinfo, bool is_decoupled, double tic, BoneInfo *out) +{ + const TArray* animationData = nullptr; + + if (drawinfo.animationid >= 0) + { + animation = Models[drawinfo.animationid]; + animationData = animation->AttachAnimationData(); + } + + const TArray *boneData = nullptr; + + if(is_decoupled) + { + if(frameinfo.decoupled_frame.frame1 >= 0) + { + boneData = animation->CalculateBones( + frameinfo.decoupled_frame_prev ? *frameinfo.decoupled_frame_prev : nullptr, + frameinfo.decoupled_frame, + frameinfo.inter, + animationData, + modelData->modelBoneOverrides.Size() > i + ? &modelData->modelBoneOverrides[i] + : nullptr, + out, + tic); + } + } + else + { + boneData = animation->CalculateBones( + nullptr, + { + nextFrame ? frameinfo.inter : -1.0f, + drawinfo.modelframe, + drawinfo.modelframenext + }, + -1.0f, + animationData, + (modelData && modelData->modelBoneOverrides.Size() > i) + ? &modelData->modelBoneOverrides[i] + : nullptr, + out, + tic); + } + + return boneData; +} + +static inline void RenderModelFrame(FModelRenderer *renderer, int i, const FSpriteModelFrame *smf, DActorModelData* modelData, const CalcModelFrameInfo &frameinfo, ModelDrawInfo &drawinfo, bool is_decoupled, double tic, FTranslationID translation, int &boneStartingPosition, bool &evaluatedSingle) +{ + FModel * mdl = Models[drawinfo.modelid]; + auto tex = drawinfo.skinid.isValid() ? TexMan.GetGameTexture(drawinfo.skinid, true) : nullptr; + mdl->BuildVertexBuffer(renderer); + + auto ssidp = drawinfo.surfaceskinids.Size() > 0 + ? drawinfo.surfaceskinids.Data() + : (((i * MD3_MAX_SURFACES) < smf->surfaceskinIDs.Size()) ? &smf->surfaceskinIDs[i * MD3_MAX_SURFACES] : nullptr); + + bool nextFrame = frameinfo.smfNext && drawinfo.modelframe != drawinfo.modelframenext; + + // [Jay] while per-model animations aren't done, DECOUPLEDANIMATIONS does the same as MODELSAREATTACHMENTS + if(!evaluatedSingle) + { // [Jay] TODO per-model decoupled animations + const TArray *boneData = ProcessModelFrame(mdl, nextFrame, i, smf, modelData, frameinfo, drawinfo, is_decoupled, tic, nullptr); + + if(frameinfo.smf_flags & MDL_MODELSAREATTACHMENTS || is_decoupled) + { + boneStartingPosition = boneData ? screen->mBones->UploadBones(*boneData) : -1; + evaluatedSingle = true; + } + } + + mdl->RenderFrame(renderer, tex, drawinfo.modelframe, nextFrame ? drawinfo.modelframenext : drawinfo.modelframe, nextFrame ? frameinfo.inter : -1.f, translation, ssidp, boneStartingPosition); +} + +void RenderFrameModels(FModelRenderer *renderer, FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, FTranslationID translation, AActor* actor) +{ + double tic = actor->Level->totaltime; + if ((ConsoleState == c_up || ConsoleState == c_rising) && (menuactive == MENU_Off || menuactive == MENU_OnNoPause) && !actor->isFrozen()) + { + tic += I_GetTimeFrac(); + } + + bool is_decoupled = (actor->flags9 & MF9_DECOUPLEDANIMATIONS); + + DActorModelData* modelData = actor ? actor->modelData.ForceGet() : nullptr; + + CalcModelFrameInfo frameinfo = CalcModelFrame(Level, smf, curState, curTics, modelData, actor, is_decoupled, tic); + ModelDrawInfo drawinfo; int boneStartingPosition = -1; bool evaluatedSingle = false; - for (unsigned i = 0; i < modelsamount; i++) + for (unsigned i = 0; i < frameinfo.modelsamount; i++) { - int modelid = -1; - int animationid = -1; - int modelframe = -1; - int modelframenext = -1; - FTextureID skinid(nullptr); - - surfaceskinids.Clear(); - - if (actor->modelData != nullptr) + if (CalcModelOverrides(i, smf, modelData, frameinfo, drawinfo, is_decoupled)) { - //modelID - if (actor->modelData->models.Size() > i && actor->modelData->models[i].modelID >= 0) - { - modelid = actor->modelData->models[i].modelID; - } - else if(actor->modelData->models.Size() > i && actor->modelData->models[i].modelID == -2) - { - continue; - } - else if(smf->modelsAmount > i) - { - modelid = smf->modelIDs[i]; - } - - //animationID - if (actor->modelData->animationIDs.Size() > i && actor->modelData->animationIDs[i] >= 0) - { - animationid = actor->modelData->animationIDs[i]; - } - else if(smf->modelsAmount > i) - { - animationid = smf->animationIDs[i]; - } - if(!is_decoupled) - { - //modelFrame - if (actor->modelData->modelFrameGenerators.Size() > i - && (unsigned)actor->modelData->modelFrameGenerators[i] < modelsamount - && smf->modelframes[actor->modelData->modelFrameGenerators[i]] >= 0 - ) { - modelframe = smf->modelframes[actor->modelData->modelFrameGenerators[i]]; - - if (smfNext) - { - if(smfNext->modelframes[actor->modelData->modelFrameGenerators[i]] >= 0) - { - modelframenext = smfNext->modelframes[actor->modelData->modelFrameGenerators[i]]; - } - else - { - modelframenext = smfNext->modelframes[i]; - } - } - } - else if(smf->modelsAmount > i) - { - modelframe = smf->modelframes[i]; - if (smfNext) modelframenext = smfNext->modelframes[i]; - } - } - - //skinID - if (actor->modelData->skinIDs.Size() > i && actor->modelData->skinIDs[i].isValid()) - { - skinid = actor->modelData->skinIDs[i]; - } - else if(smf->modelsAmount > i) - { - skinid = smf->skinIDs[i]; - } - - //surfaceSkinIDs - if(actor->modelData->models.Size() > i && actor->modelData->models[i].surfaceSkinIDs.Size() > 0) - { - unsigned sz1 = smf->surfaceskinIDs.Size(); - unsigned sz2 = actor->modelData->models[i].surfaceSkinIDs.Size(); - unsigned start = i * MD3_MAX_SURFACES; - - surfaceskinids = actor->modelData->models[i].surfaceSkinIDs; - surfaceskinids.Resize(MD3_MAX_SURFACES); - - for (unsigned surface = 0; surface < MD3_MAX_SURFACES; surface++) - { - if (sz2 > surface && (actor->modelData->models[i].surfaceSkinIDs[surface].isValid())) - { - continue; - } - if((surface + start) < sz1) - { - surfaceskinids[surface] = smf->surfaceskinIDs[surface + start]; - } - else - { - surfaceskinids[surface].SetNull(); - } - } - } - } - else - { - modelid = smf->modelIDs[i]; - animationid = smf->animationIDs[i]; - modelframe = smf->modelframes[i]; - if (smfNext) modelframenext = smfNext->modelframes[i]; - skinid = smf->skinIDs[i]; - } - - if (modelid >= 0 && modelid < Models.size()) - { - FModel * mdl = Models[modelid]; - auto tex = skinid.isValid() ? TexMan.GetGameTexture(skinid, true) : nullptr; - mdl->BuildVertexBuffer(renderer); - - auto ssidp = surfaceskinids.Size() > 0 - ? surfaceskinids.Data() - : (((i * MD3_MAX_SURFACES) < smf->surfaceskinIDs.Size()) ? &smf->surfaceskinIDs[i * MD3_MAX_SURFACES] : nullptr); - - - bool nextFrame = smfNext && modelframe != modelframenext; - - - // [RL0] while per-model animations aren't done, DECOUPLEDANIMATIONS does the same as MODELSAREATTACHMENTS - if(!evaluatedSingle) - { - const TArray *boneData = nullptr; - FModel* animation = mdl; - const TArray* animationData = nullptr; - - if (animationid >= 0) - { - animation = Models[animationid]; - animationData = animation->AttachAnimationData(); - } - - if(is_decoupled) - { - if(decoupled_frame.frame1 >= 0) - { - boneData = animation->CalculateBones(actor->modelData->prevAnim, decoupled_frame, inter, animationData); - } - } - else - { - boneData = animation->CalculateBones(nullptr, {nextFrame ? inter : -1.0f, modelframe, modelframenext}, -1.0f, animationData); - } - - if(smf_flags & MDL_MODELSAREATTACHMENTS || is_decoupled) - { - boneStartingPosition = boneData ? screen->mBones->UploadBones(*boneData) : -1; - evaluatedSingle = true; - } - } - - mdl->RenderFrame(renderer, tex, modelframe, nextFrame ? modelframenext : modelframe, nextFrame ? inter : -1.f, translation, ssidp, boneStartingPosition); + RenderModelFrame(renderer, i, smf, modelData, frameinfo, drawinfo, is_decoupled, tic, translation, boneStartingPosition, evaluatedSingle); } } } @@ -1068,33 +1125,24 @@ void ParseModelDefLump(int Lump) // //=========================================================================== -FSpriteModelFrame * FindModelFrameRaw(const PClass * ti, int sprite, int frame, bool dropped) +FSpriteModelFrame * FindModelFrameRaw(const AActor * actorDefaults, const PClass * ti, int sprite, int frame, bool dropped) { - auto def = GetDefaultByType(ti); - if (def->hasmodel) + if(actorDefaults->hasmodel) { - if(def->flags9 & MF9_DECOUPLEDANIMATIONS) + FSpriteModelFrame smf; + + memset(&smf, 0, sizeof(smf)); + smf.type = ti; + smf.sprite = sprite; + smf.frame = frame; + + int hash = SpriteModelHash[ModelFrameHash(&smf) % SpriteModelFrames.Size()]; + + while (hash>=0) { - FSpriteModelFrame * smf = BaseSpriteModelFrames.CheckKey((void*)ti); - if(smf) return smf; - } - else - { - FSpriteModelFrame smf; - - memset(&smf, 0, sizeof(smf)); - smf.type=ti; - smf.sprite=sprite; - smf.frame=frame; - - int hash = SpriteModelHash[ModelFrameHash(&smf) % SpriteModelFrames.Size()]; - - while (hash>=0) - { - FSpriteModelFrame * smff = &SpriteModelFrames[hash]; - if (smff->type==ti && smff->sprite==sprite && smff->frame==frame) return smff; - hash=smff->hashnext; - } + FSpriteModelFrame * smff = &SpriteModelFrames[hash]; + if (smff->type == ti && smff->sprite == sprite && smff->frame == frame) return smff; + hash = smff->hashnext; } } @@ -1108,28 +1156,52 @@ FSpriteModelFrame * FindModelFrameRaw(const PClass * ti, int sprite, int frame, if (sprframe->Voxel != nullptr) { int index = sprframe->Voxel->VoxeldefIndex; - if (dropped && sprframe->Voxel->DroppedSpin !=sprframe->Voxel->PlacedSpin) index++; + if (dropped && sprframe->Voxel->DroppedSpin != sprframe->Voxel->PlacedSpin) index++; return &SpriteModelFrames[index]; } } } + return nullptr; } -FSpriteModelFrame * FindModelFrame(const AActor * thing, int sprite, int frame, bool dropped) +FSpriteModelFrame * FindModelFrame(const PClass * ti, int sprite, int frame, bool dropped) { - if(!thing) return nullptr; + auto def = GetDefaultByType(ti); - if(thing->flags9 & MF9_DECOUPLEDANIMATIONS) + if (def->hasmodel) { - return BaseSpriteModelFrames.CheckKey((thing->modelData != nullptr && thing->modelData->modelDef != nullptr) ? thing->modelData->modelDef : thing->GetClass()); + if(def->flags9 & MF9_DECOUPLEDANIMATIONS) + { + FSpriteModelFrame * smf = BaseSpriteModelFrames.CheckKey(ti); + if(smf) return smf; + } + } + + return FindModelFrameRaw(def, ti, sprite, frame, dropped); +} + +FSpriteModelFrame * FindModelFrame(const PClass * ti, bool is_decoupled, int sprite, int frame, bool dropped) +{ + if(!ti) return nullptr; + + if(is_decoupled) + { + return BaseSpriteModelFrames.CheckKey(ti); } else { - return FindModelFrameRaw((thing->modelData != nullptr && thing->modelData->modelDef != nullptr) ? thing->modelData->modelDef : thing->GetClass(), sprite, frame, dropped); + return FindModelFrameRaw(GetDefaultByType(ti), ti, sprite, frame, dropped); } } +FSpriteModelFrame * FindModelFrame(AActor * thing, int sprite, int frame, bool dropped) +{ + if(!thing) return nullptr; + + return FindModelFrame((thing->modelData != nullptr && thing->modelData->modelDef != nullptr) ? thing->modelData->modelDef : thing->GetClass(), (thing->flags9 & MF9_DECOUPLEDANIMATIONS), sprite, frame, dropped); +} + //=========================================================================== // // IsHUDModelForPlayerAvailable diff --git a/src/r_data/models.h b/src/r_data/models.h index 4a7154446..a62d59333 100644 --- a/src/r_data/models.h +++ b/src/r_data/models.h @@ -62,8 +62,11 @@ enum MDL_FORCECULLBACKFACES = 1<<14, }; -FSpriteModelFrame * FindModelFrame(const AActor * thing, int sprite, int frame, bool dropped); -FSpriteModelFrame * FindModelFrameRaw(const PClass * ti, int sprite, int frame, bool dropped); +FSpriteModelFrame * FindModelFrame(AActor * thing, int sprite, int frame, bool dropped); +FSpriteModelFrame * FindModelFrame(const PClass * ti, bool is_decoupled, int sprite, int frame, bool dropped); +FSpriteModelFrame * FindModelFrame(const PClass * ti, int sprite, int frame, bool dropped); +//FSpriteModelFrame * FindModelFrameRaw(const AActor * actorDefaults, const PClass * ti, int sprite, int frame, bool dropped); + bool IsHUDModelForPlayerAvailable(player_t * player); // Check if circle potentially intersects with node AABB @@ -114,6 +117,36 @@ void BSPWalkCircle(FLevelLocals *Level, float x, float y, float radiusSquared, c void RenderModel(FModelRenderer* renderer, float x, float y, float z, FSpriteModelFrame* smf, AActor* actor, double ticFrac); void RenderHUDModel(FModelRenderer* renderer, DPSprite* psp, FVector3 translation, FVector3 rotation, FVector3 rotation_pivot, FSpriteModelFrame *smf); +struct CalcModelFrameInfo +{ + int smf_flags; + const FSpriteModelFrame * smfNext; + float inter; + bool is_decoupled; + ModelAnimFrameInterp decoupled_frame; + ModelAnimFrame * decoupled_frame_prev; + unsigned modelsamount; +}; + +struct ModelDrawInfo +{ + TArray surfaceskinids; + int modelid; + int animationid; + int modelframe; + int modelframenext; + FTextureID skinid; +}; + +class DActorModelData; + +CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, DActorModelData* modelData, AActor* actor, bool is_decoupled, double tic); + +// returns true if the model isn't removed +bool CalcModelOverrides(int modelindex, const FSpriteModelFrame *smf, DActorModelData* modelData, const CalcModelFrameInfo &frameinfo, ModelDrawInfo &drawinfo, bool is_decoupled); + +const TArray * ProcessModelFrame(FModel * animation, bool nextFrame, int i, const FSpriteModelFrame *smf, DActorModelData* modelData, const CalcModelFrameInfo &frameinfo, ModelDrawInfo &drawinfo, bool is_decoupled, double tic, BoneInfo *out); + EXTERN_CVAR(Float, cl_scaleweaponfov) #endif diff --git a/src/rendering/hwrenderer/hw_precache.cpp b/src/rendering/hwrenderer/hw_precache.cpp index d6837252d..699faf706 100644 --- a/src/rendering/hwrenderer/hw_precache.cpp +++ b/src/rendering/hwrenderer/hw_precache.cpp @@ -159,7 +159,7 @@ void hw_PrecacheTexture(uint8_t *texhitlist, TMap &actorhitl { auto &state = cls->GetStates()[i]; spritelist[state.sprite].Insert(gltrans, true); - FSpriteModelFrame * smf = FindModelFrameRaw(cls, state.sprite, state.Frame, false); + FSpriteModelFrame * smf = FindModelFrame(cls, state.sprite, state.Frame, false); if (smf != NULL) { for (int i = 0; i < smf->modelsAmount; i++) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 1668b3a8c..1598ee697 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1359,6 +1359,108 @@ class Actor : Thinker native native bool A_AttachLight(Name lightid, int type, Color lightcolor, int radius1, int radius2, int flags = 0, Vector3 ofs = (0,0,0), double param = 0, double spoti = 10, double spoto = 25, double spotp = 0); native bool A_RemoveLight(Name lightid); + //================================================ + // + // Bone Offset Setters + // + //================================================ + + native version("4.15.1") void SetBoneRotation(int boneIndex, Quat rotation, int mode = SB_ADD, double interpolation_duration = 1.0); + native version("4.15.1") void SetNamedBoneRotation(Name boneName, Quat rotation, int mode = SB_ADD, double interpolation_duration = 1.0); + + version("4.15.1") void SetBoneRotationAngles(int boneIndex, double yaw, double pitch, double roll, int mode = SB_ADD, double interpolation_duration = 1.0) + { + SetBoneRotation(boneIndex, Quat.FromAngles(yaw, pitch, roll), mode, interpolation_duration); + } + + version("4.15.1") void SetNamedBoneRotationAngles(Name boneName, double yaw, double pitch, double roll, int mode = SB_ADD, double interpolation_duration = 1.0) + { + SetNamedBoneRotation(boneName, Quat.FromAngles(yaw, pitch, roll), mode, interpolation_duration); + } + + version("4.15.1") void ClearBoneRotation(int boneIndex, double interpolation_duration = 1.0) + { + SetBoneRotation(boneIndex, Quat(0, 0, 0, 1), 0, interpolation_duration); + } + + version("4.15.1") void ClearNamedBoneRotation(Name boneName, double interpolation_duration = 1.0) + { + SetNamedBoneRotation(boneName, Quat(0, 0, 0, 1), 0, interpolation_duration); + } + + native version("4.15.1") void SetBoneTranslation(int boneIndex, Vector3 translation, int mode = SB_ADD, double interpolation_duration = 1.0); + native version("4.15.1") void SetNamedBoneTranslation(Name boneName, Vector3 translation, int mode = SB_ADD, double interpolation_duration = 1.0); + + version("4.15.1") void ClearBoneTranslation(int boneIndex, double interpolation_duration = 1.0) + { + SetBoneTranslation(boneIndex, (0, 0, 0), 0, interpolation_duration); + } + + version("4.15.1") void ClearNamedBoneTranslation(Name boneName, double interpolation_duration = 1.0) + { + SetNamedBoneTranslation(boneName, (0, 0, 0), 0, interpolation_duration); + } + + native version("4.15.1") void SetBoneScaling(int boneIndex, Vector3 scaling, int mode = SB_ADD, double interpolation_duration = 1.0); + native version("4.15.1") void SetNamedBoneScaling(Name boneName, Vector3 scaling, int mode = SB_ADD, double interpolation_duration = 1.0); + + version("4.15.1") void ClearBoneScaling(int boneIndex, double interpolation_duration = 1.0) + { + SetBoneScaling(boneIndex, (0, 0, 0), 0, interpolation_duration); + } + + version("4.15.1") void ClearNamedBoneScaling(Name boneName, double interpolation_duration = 1.0) + { + SetNamedBoneScaling(boneName, (0, 0, 0), 0, interpolation_duration); + } + + native version("4.15.1") void ClearBoneOffsets(); + + //================================================ + // + // Bone Offset Getters + // + //================================================ + + /* rotation, translation, scaling */ + native version("4.15.1") Quat, Vector3, Vector3 GetBoneOffset(int boneIndex); + native version("4.15.1") Quat, Vector3, Vector3 GetNamedBoneOffset(Name boneName); + + //================================================ + // + // Bone Info Getters + // + //================================================ + + native version("4.15.1") void GetRootBones(out Array rootBones); + + native version("4.15.1") Name GetBoneName(int boneIndex); + native version("4.15.1") int GetBoneIndex(Name boneName); + + native version("4.15.1") int GetBoneParent(int boneIndex); + native version("4.15.1") int GetNamedBoneParent(Name boneName); // return value lower than 0 means it's a root bone, and as such has no parent + + native version("4.15.1") void GetBoneChildren(int boneIndex, out Array children); + native version("4.15.1") void GetNamedBoneChildren(Name boneName, out Array children); + + // this is the length in model units, not in world units, and does not take model scale in MODELDEF, world, etc, or current animation or offset into account at all + native version("4.15.1") double GetBoneLength(int boneIndex); + native version("4.15.1") double GetNamedBoneLength(Name boneName); + + // this is the direction of the bone in the armature, does not take the current animation or offset into account at all + native version("4.15.1") Vector3 GetBoneDir(int boneIndex); + native version("4.15.1") Vector3 GetNamedBoneDir(Name boneName); + + native version("4.15.1") int GetBoneCount(); + + + //================================================ + // + // + // + //================================================ + + native version("4.12") void SetAnimation(Name animName, double framerate = -1, int startFrame = -1, int loopFrame = -1, int endFrame = -1, int interpolateTics = -1, int flags = 0); native version("4.12") ui void SetAnimationUI(Name animName, double framerate = -1, int startFrame = -1, int loopFrame = -1, int endFrame = -1, int interpolateTics = -1, int flags = 0); diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index 249d031bb..5c1480e62 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1547,3 +1547,10 @@ enum EParticleStyle PT_ROUND = 1, PT_SMOOTH = 2, }; + +enum ESetBoneMode +{ + SB_CLEAR = 0, + SB_ADD = 1, + SB_REPLACE = 2, +}; From 0d612553a42178be8578cd642c4383a7e8673c3b Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Fri, 2 May 2025 09:36:05 -0400 Subject: [PATCH 141/384] - do not allow sv_gravity to be INF --- src/playsim/p_mobj.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 00d20722b..43d98ba25 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -149,6 +149,10 @@ FRandom pr_spawnmissile("SpawnMissile"); CUSTOM_CVAR (Float, sv_gravity, 800.f, CVAR_SERVERINFO|CVAR_NOSAVE|CVAR_NOINITCALL) { + // test NAN and INF + if (((double)self != (double)self) || isinf((double)self)) + sv_gravity = 800.f; + for (auto Level : AllLevels()) { Level->gravity = self; From 502af6adb9d00c6d9cc603bd9f5e11796f2eb7b8 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Thu, 1 May 2025 20:05:02 -0600 Subject: [PATCH 142/384] SSECMF_DRAWN was being skipped for some cases, affecting texture automap drawing. See bug #3066. --- src/rendering/hwrenderer/scene/hw_bsp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_bsp.cpp b/src/rendering/hwrenderer/scene/hw_bsp.cpp index a927872f6..4ff193e1d 100644 --- a/src/rendering/hwrenderer/scene/hw_bsp.cpp +++ b/src/rendering/hwrenderer/scene/hw_bsp.cpp @@ -317,7 +317,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) { if (!(currentsubsector->flags & SSECMF_DRAWN)) { - if (clipper.SafeCheckRange(startAngle, endAngle) && (!r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR))) + if (clipper.SafeCheckRange(startAngle, endAngle) && !Viewpoint.IsAllowedOoB()) { currentsubsector->flags |= SSECMF_DRAWN; } @@ -334,7 +334,7 @@ void HWDrawInfo::AddLine (seg_t *seg, bool portalclip) return; } - if (Viewpoint.IsAllowedOoB() && (!r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR) || clipperr.SafeCheckRange(startAngleR, endAngleR))) + if (!Viewpoint.IsAllowedOoB() || (!r_radarclipper || (Level->flags3 & LEVEL3_NOFOGOFWAR) || clipperr.SafeCheckRange(startAngleR, endAngleR))) currentsubsector->flags |= SSECMF_DRAWN; uint8_t ispoly = uint8_t(seg->sidedef->Flags & WALLF_POLYOBJ); From 9c383e93790a0165fcc8674ca9c5ae98b402e86e Mon Sep 17 00:00:00 2001 From: XLightningStormL Date: Sat, 3 May 2025 09:27:28 +1000 Subject: [PATCH 143/384] DepleteBy virtual * Update inventory_util.zs Added ExtraDepletionBehavior() functionality to TakeInventory and UseInventory * Update inventory.zs Added ExtraDepletionBehavior (int takeAmount) function * Update inventory_util.zs ExtraDepletionBehavior now requires at least 1 item in reserve to work * Replaced ExtraDepletion with DepleteBy Logic Shoutout to RicardoLuis0 * Replaced ExtraDepletion with DepleteBy Logic Shoutout to RicardoLuis0 * Update inventory_util.zs added sv_infiniteinventory checks for takeinventory for custominventory items, restored support for sv_infiniteinventory useinventory items * Update inventory.zs cleaned up DepleteBy - removing unnecessary "--Amount <= 0 && usedItem" check and usedItem bool * Update inventory_util.zs removed unnecessary sv_infiniteinventory check * Update inventory.zs amount is integer, depleteordestroy should occur when amount is less than 1 --- .../zscript/actors/inventory/inventory.zs | 19 +++++++++++++++++++ .../static/zscript/actors/inventory_util.zs | 16 +++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/wadsrc/static/zscript/actors/inventory/inventory.zs b/wadsrc/static/zscript/actors/inventory/inventory.zs index 40a89cb08..10ee3a5f2 100644 --- a/wadsrc/static/zscript/actors/inventory/inventory.zs +++ b/wadsrc/static/zscript/actors/inventory/inventory.zs @@ -923,6 +923,25 @@ class Inventory : Actor } } + //=========================================================================== + // + // Inventory :: DepleteBy + // + // Handles item depletion when using or taking items + // + //=========================================================================== + virtual void DepleteBy(int by) + { + if (amount < 1 || by >= amount) + { + DepleteOrDestroy(); + } + else + { + amount -= by; + } + } + //=========================================================================== // // Inventory :: DepleteOrDestroy diff --git a/wadsrc/static/zscript/actors/inventory_util.zs b/wadsrc/static/zscript/actors/inventory_util.zs index 011ca134f..6b35a2898 100644 --- a/wadsrc/static/zscript/actors/inventory_util.zs +++ b/wadsrc/static/zscript/actors/inventory_util.zs @@ -146,11 +146,7 @@ extend class Actor if (!fromdecorate) { - item.Amount -= amount; - if (item.Amount <= 0) - { - item.DepleteOrDestroy(); - } + item.DepleteBy(amount); // It won't be used in non-decorate context, so return false here return false; } @@ -169,11 +165,10 @@ extend class Actor // Nothing to do here, except maybe res = false;? Would it make sense? result = false; } - else if (!amount || amount >= item.Amount) + else { - item.DepleteOrDestroy(); + item.DepleteBy(amount); } - else item.Amount -= amount; return result; } @@ -271,10 +266,9 @@ extend class Actor { return true; } - - if (--item.Amount <= 0) + else { - item.DepleteOrDestroy (); + item.DepleteBy(1); //useinventory can only really use one item at a time } return true; } From 8447e1716c86868e7c8bde87eeb0e83e23019b93 Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Mon, 5 May 2025 17:49:57 +0400 Subject: [PATCH 144/384] Added a menu option for am_showlevelname --- wadsrc/static/menudef.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 05a83d98d..27d11edef 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -1363,6 +1363,7 @@ OptionMenu AutomapOptions protected Option "$AUTOMAPMNU_SHOWTIME", "am_showtime", "OnOff" Option "$AUTOMAPMNU_SHOWTOTALTIME", "am_showtotaltime", "OnOff" Option "$AUTOMAPMNU_SHOWMAPLABEL", "am_showmaplabel", "MaplabelTypes" + Option "$AUTOMAPMNU_SHOWLEVELNAME", "am_showlevelname", "OnOff" StaticText "" Option "$AUTOMAPMNU_SHOWKEYS", "am_showkeys", "OnOff" From 5168ce0e04f1cb2f80aecce4c6dcc2fa4914dfbc Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Sat, 26 Apr 2025 23:45:14 +0400 Subject: [PATCH 145/384] Exported DrawCrosshair --- src/g_statusbar/shared_sbar.cpp | 13 +++++++++++++ wadsrc/static/zscript/ui/statusbar/statusbar.zs | 1 + 2 files changed, 14 insertions(+) diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 5c4fa25d9..0626009b1 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -1019,6 +1019,19 @@ void DBaseStatusBar::DrawCrosshair (double ticFrac) ST_DrawCrosshair(health, viewwidth / 2 + viewwindowx, viewheight / 2 + viewwindowy, size); } +static void DrawCrosshair(DBaseStatusBar* self, double ticFrac) +{ + self->DrawCrosshair(ticFrac); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBaseStatusBar, DrawCrosshair, DrawCrosshair) +{ + PARAM_SELF_PROLOGUE(DBaseStatusBar); + PARAM_FLOAT(ticFrac); + self->DrawCrosshair(ticFrac); + return 0; +} + //--------------------------------------------------------------------------- // // FlashCrosshair diff --git a/wadsrc/static/zscript/ui/statusbar/statusbar.zs b/wadsrc/static/zscript/ui/statusbar/statusbar.zs index 1cbaeae87..f0b502ba6 100644 --- a/wadsrc/static/zscript/ui/statusbar/statusbar.zs +++ b/wadsrc/static/zscript/ui/statusbar/statusbar.zs @@ -238,6 +238,7 @@ class BaseStatusBar : StatusBarCore native // [MK] let the HUD handle drawing the pause graphics virtual bool DrawPaused(int player) { return false; } + protected native void DrawCrosshair(double TicFrac); native TextureID GetMugshot(int accuracy, int stateflags=MugShot.STANDARD, String default_face = "STF"); native int GetTopOfStatusBar(); From 3bb716c41454dc67e0c996b664b671b667de9b08 Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Sun, 27 Apr 2025 13:12:25 +0400 Subject: [PATCH 146/384] Exported RefreshBackground --- src/g_statusbar/shared_sbar.cpp | 12 ++++++++++++ wadsrc/static/zscript/ui/statusbar/statusbar.zs | 1 + 2 files changed, 13 insertions(+) diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 0626009b1..663ab8f96 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -987,6 +987,18 @@ void DBaseStatusBar::RefreshBackground () const } } +static void RefreshBackground(DBaseStatusBar* self) +{ + self->RefreshBackground(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBaseStatusBar, RefreshBackground, RefreshBackground) +{ + PARAM_SELF_PROLOGUE(DBaseStatusBar); + self->RefreshBackground(); + return 0; +} + //--------------------------------------------------------------------------- // // DrawCrosshair diff --git a/wadsrc/static/zscript/ui/statusbar/statusbar.zs b/wadsrc/static/zscript/ui/statusbar/statusbar.zs index f0b502ba6..8d42b2230 100644 --- a/wadsrc/static/zscript/ui/statusbar/statusbar.zs +++ b/wadsrc/static/zscript/ui/statusbar/statusbar.zs @@ -238,6 +238,7 @@ class BaseStatusBar : StatusBarCore native // [MK] let the HUD handle drawing the pause graphics virtual bool DrawPaused(int player) { return false; } + protected native void RefreshBackground(); protected native void DrawCrosshair(double TicFrac); native TextureID GetMugshot(int accuracy, int stateflags=MugShot.STANDARD, String default_face = "STF"); native int GetTopOfStatusBar(); From 31cd741cb052d01dbcf3796c20e49e8aee35d35d Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Mon, 5 May 2025 23:49:49 +0400 Subject: [PATCH 147/384] Scriptified DBaseStatusBar::Draw --- src/g_statusbar/sbar.h | 1 - src/g_statusbar/shared_sbar.cpp | 39 ------------------- src/scripting/vmthunks.cpp | 15 ------- .../static/zscript/ui/statusbar/statusbar.zs | 31 ++++++++++++++- 4 files changed, 30 insertions(+), 56 deletions(-) diff --git a/src/g_statusbar/sbar.h b/src/g_statusbar/sbar.h index 5d3f9794d..9141009c8 100644 --- a/src/g_statusbar/sbar.h +++ b/src/g_statusbar/sbar.h @@ -388,7 +388,6 @@ public: void SetScale(); virtual void Tick (); void CallTick(); - virtual void Draw (EHudState state, double ticFrac); void CallDraw(EHudState state, double ticFrac); void DrawBottomStuff (EHudState state); void DrawTopStuff (EHudState state); diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 663ab8f96..27575a5c6 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -1088,44 +1088,6 @@ void DBaseStatusBar::DrawMessages (int layer, int bottom) // //--------------------------------------------------------------------------- -void DBaseStatusBar::Draw (EHudState state, double ticFrac) -{ - // HUD_AltHud state is for popups only - if (state == HUD_AltHud) - return; - - if (state == HUD_StatusBar) - { - RefreshBackground (); - } - - if (idmypos) - { - // Draw current coordinates - IFVIRTUAL(DBaseStatusBar, DrawMyPos) - { - VMValue params[] = { (DObject*)this }; - VMCall(func, params, countof(params), nullptr, 0); - } - } - - if (viewactive) - { - if (CPlayer && CPlayer->camera && CPlayer->camera->player) - { - DrawCrosshair (ticFrac); - } - } - else if (automapactive) - { - IFVIRTUAL(DBaseStatusBar, DrawAutomapHUD) - { - VMValue params[] = { (DObject*)this, r_viewpoint.TicFrac }; - VMCall(func, params, countof(params), nullptr, 0); - } - } -} - void DBaseStatusBar::CallDraw(EHudState state, double ticFrac) { IFVIRTUAL(DBaseStatusBar, Draw) @@ -1133,7 +1095,6 @@ void DBaseStatusBar::CallDraw(EHudState state, double ticFrac) VMValue params[] = { (DObject*)this, state, ticFrac }; VMCall(func, params, countof(params), nullptr, 0); } - else Draw(state, ticFrac); twod->ClearClipRect(); // make sure the scripts don't leave a valid clipping rect behind. BeginStatusBar(BaseSBarHorizontalResolution, BaseSBarVerticalResolution, BaseRelTop, false); } diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 977d72de4..5d3aea03f 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2108,21 +2108,6 @@ DEFINE_ACTION_FUNCTION_NATIVE(DBaseStatusBar, DetachAllMessages, SBar_DetachAllM return 0; } -static void SBar_Draw(DBaseStatusBar *self, int state, double ticFrac) -{ - self->Draw((EHudState)state, ticFrac); -} - - -DEFINE_ACTION_FUNCTION_NATIVE(DBaseStatusBar, Draw, SBar_Draw) -{ - PARAM_SELF_PROLOGUE(DBaseStatusBar); - PARAM_INT(state); - PARAM_FLOAT(ticFrac); - self->Draw((EHudState)state, ticFrac); - return 0; -} - static void SetMugshotState(DBaseStatusBar *self, const FString &statename, bool wait, bool reset) { self->mugshot.SetState(statename.GetChars(), wait, reset); diff --git a/wadsrc/static/zscript/ui/statusbar/statusbar.zs b/wadsrc/static/zscript/ui/statusbar/statusbar.zs index 8d42b2230..fa2b66c6b 100644 --- a/wadsrc/static/zscript/ui/statusbar/statusbar.zs +++ b/wadsrc/static/zscript/ui/statusbar/statusbar.zs @@ -216,8 +216,37 @@ class BaseStatusBar : StatusBarCore native { } + virtual void Draw (int state, double TicFrac) + { + // HUD_AltHud state is for popups only + if (state == HUD_AltHud) + return; + + if (state == HUD_StatusBar) + { + RefreshBackground(); + } + + if (idmypos) + { + // Draw current coordinates + DrawMyPos(); + } + + if (viewactive) + { + if (CPlayer && CPlayer.camera && CPlayer.camera.player) + { + DrawCrosshair(TicFrac); + } + } + else if (automapactive) + { + DrawAutomapHUD(TicFrac); + } + } + native virtual void Tick (); - native virtual void Draw (int state, double TicFrac); native virtual void ScreenSizeChanged (); native virtual clearscope void ReceivedWeapon (Weapon weapn); native virtual clearscope void SetMugShotState (String state_name, bool wait_till_done=false, bool reset=false); From e9a067dd655fe4c17c0993a46ca6d4d1e9d892f1 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sat, 26 Apr 2025 09:11:58 -0600 Subject: [PATCH 148/384] Skymist is a third sky layer with transparency, and adopts the fade color and skyfog density. Size not connected to SKY1 or SKY2. Works with 6-sided skyboxes. Three template pngs (skymist1, 2, & 3) all 1x544 pixels, 8-bit grayscale with transparency are part of gzdoom.pk3 for general usage. Can supply custom lump through mapinfo. Console command 'skymisttoggle' shows the effect (make sure fade has a non-zero color in mapinfo and skyfog has non-zero density). Oh, and skyfog now works with 6-sided skyboxes. --- .../rendering/hwrenderer/data/hw_skydome.cpp | 2 +- src/console/c_cmds.cpp | 10 +++++++++ src/g_level.cpp | 2 ++ src/g_levellocals.h | 4 +++- src/gamedata/g_mapinfo.cpp | 21 +++++++++++++++++-- src/gamedata/g_mapinfo.h | 3 +++ src/p_saveg.cpp | 2 ++ src/p_setup.cpp | 4 ++++ src/playsim/p_acs.cpp | 1 + src/rendering/hwrenderer/scene/hw_portal.h | 5 ++--- src/rendering/hwrenderer/scene/hw_sky.cpp | 3 +++ .../hwrenderer/scene/hw_skyportal.cpp | 17 ++++++++++----- src/rendering/r_sky.cpp | 5 +++++ src/scripting/vmthunks.cpp | 13 ++++++++++++ 14 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/common/rendering/hwrenderer/data/hw_skydome.cpp b/src/common/rendering/hwrenderer/data/hw_skydome.cpp index 18108c8f2..d0f2e471d 100644 --- a/src/common/rendering/hwrenderer/data/hw_skydome.cpp +++ b/src/common/rendering/hwrenderer/data/hw_skydome.cpp @@ -488,7 +488,7 @@ void FSkyVertexBuffer::DoRenderDome(FRenderState& state, FGameTexture* tex, int RenderRow(state, DT_TriangleFan, rc, primStart); state.EnableTexture(true); } - state.SetObjectColor(0xffffffff); + state.SetObjectColor(color); for (int i = 1; i <= mRows; i++) { RenderRow(state, DT_TriangleStrip, i, primStart, i == 1); diff --git a/src/console/c_cmds.cpp b/src/console/c_cmds.cpp index 19c5c9d50..f92415477 100644 --- a/src/console/c_cmds.cpp +++ b/src/console/c_cmds.cpp @@ -942,6 +942,16 @@ CCMD(changesky) InitSkyMap (primaryLevel); } +//----------------------------------------------------------------------------- +// +// +// +//----------------------------------------------------------------------------- +CCMD(skymisttoggle) +{ + primaryLevel->flags3 ^= LEVEL3_SKYMIST; +} + //----------------------------------------------------------------------------- // // diff --git a/src/g_level.cpp b/src/g_level.cpp index b42d5885f..4a269c66b 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1835,8 +1835,10 @@ void FLevelLocals::Init() skyspeed1 = info->skyspeed1; skyspeed2 = info->skyspeed2; + skymistspeed = info->skymistspeed; skytexture1 = TexMan.GetTextureID(info->SkyPic1.GetChars(), ETextureType::Wall, FTextureManager::TEXMAN_Overridable | FTextureManager::TEXMAN_ReturnFirst); skytexture2 = TexMan.GetTextureID(info->SkyPic2.GetChars(), ETextureType::Wall, FTextureManager::TEXMAN_Overridable | FTextureManager::TEXMAN_ReturnFirst); + skymisttexture = TexMan.GetTextureID(info->SkyMistPic.GetChars(), ETextureType::Wall, FTextureManager::TEXMAN_Overridable | FTextureManager::TEXMAN_ReturnFirst); fadeto = info->fadeto; cdtrack = info->cdtrack; cdid = info->cdid; diff --git a/src/g_levellocals.h b/src/g_levellocals.h index fb553d984..01c95dac1 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -687,12 +687,14 @@ public: unsigned int cdid; FTextureID skytexture1; FTextureID skytexture2; + FTextureID skymisttexture; float skyspeed1; // Scrolling speed of sky textures, in pixels per ms float skyspeed2; + float skymistspeed; double sky1pos, sky2pos; - float hw_sky1pos, hw_sky2pos; + float hw_sky1pos, hw_sky2pos, hw_skymistpos; bool skystretch; uint32_t globalcolormap; diff --git a/src/gamedata/g_mapinfo.cpp b/src/gamedata/g_mapinfo.cpp index fee2347cb..3f72be352 100644 --- a/src/gamedata/g_mapinfo.cpp +++ b/src/gamedata/g_mapinfo.cpp @@ -97,6 +97,7 @@ level_info_t *FindLevelInfo (const char *mapname, bool allowdefault) if (TheDefaultLevelInfo.LevelName.IsEmpty()) { TheDefaultLevelInfo.SkyPic2 = TheDefaultLevelInfo.SkyPic1 = "SKY1"; + TheDefaultLevelInfo.SkyMistPic = "SKYMIST1"; TheDefaultLevelInfo.LevelName = "Unnamed"; } return &TheDefaultLevelInfo; @@ -255,6 +256,7 @@ void level_info_t::Reset() NextMap = ""; NextSecretMap = ""; SkyPic1 = SkyPic2 = "-NOFLAT-"; + SkyMistPic = "SKYMIST1"; cluster = 0; partime = 0; sucktime = 0; @@ -277,7 +279,7 @@ void level_info_t::Reset() musicorder = 0; Snapshot = { 0,0,0,0,0,nullptr }; deferred.Clear(); - skyspeed1 = skyspeed2 = 0.f; + skyspeed1 = skyspeed2 = skymistspeed = 0.f; fadeto = 0; outsidefog = 0xff000000; cdtrack = 0; @@ -1099,6 +1101,20 @@ DEFINE_MAP_OPTION(sky2, true) } } +DEFINE_MAP_OPTION(skymist, true) +{ + parse.ParseAssign(); + parse.ParseLumpOrTextureName(info->SkyMistPic); + if (parse.CheckFloat()) + { + if (parse.HexenHack) + { + parse.sc.Float /= 256; + } + info->skymistspeed = float(parse.sc.Float * (TICRATE / 1000.)); + } +} + // Vavoom compatibility DEFINE_MAP_OPTION(skybox, true) { @@ -1857,7 +1873,8 @@ MapFlagHandlers[] = { "disableskyboxao", MITYPE_CLRFLAG3, LEVEL3_SKYBOXAO, 0 }, { "avoidmelee", MITYPE_SETFLAG3, LEVEL3_AVOIDMELEE, 0 }, { "attenuatelights", MITYPE_SETFLAG3, LEVEL3_ATTENUATE, 0 }, - { "nofogofwar", MITYPE_SETFLAG3, LEVEL3_NOFOGOFWAR, 0 }, + { "nofogofwar", MITYPE_SETFLAG3, LEVEL3_NOFOGOFWAR, 0 }, + { "useskymist", MITYPE_SETFLAG3, LEVEL3_SKYMIST, 0 }, { "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes { "nopassover", MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 }, { "passover", MITYPE_CLRCOMPATFLAG, COMPATF_NO_PASSMOBJ, 0 }, diff --git a/src/gamedata/g_mapinfo.h b/src/gamedata/g_mapinfo.h index d6bd695df..4c3d6a18a 100644 --- a/src/gamedata/g_mapinfo.h +++ b/src/gamedata/g_mapinfo.h @@ -274,6 +274,7 @@ enum ELevelFlags : unsigned int LEVEL3_LIGHTCREATED = 0x00080000, // a light had been created in the last frame LEVEL3_NOFOGOFWAR = 0x00100000, // disables effect of r_radarclipper CVAR on this map LEVEL3_SECRET = 0x00200000, // level is a secret level + LEVEL3_SKYMIST = 0x00400000, // level skyfog uses the skymist texture }; @@ -333,6 +334,7 @@ struct level_info_t FString PName; FString SkyPic1; FString SkyPic2; + FString SkyMistPic; FString FadeTable; FString CustomColorMap; FString F1Pic; @@ -359,6 +361,7 @@ struct level_info_t TArray deferred; float skyspeed1; float skyspeed2; + float skymistspeed; uint32_t fadeto; uint32_t outsidefog; int cdtrack; diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 659e67664..6248f2329 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -964,6 +964,7 @@ void FLevelLocals::Serialize(FSerializer &arc, bool hubload) ("fadeto", fadeto) ("skyspeed1", skyspeed1) ("skyspeed2", skyspeed2) + ("skymistspeed", skymistspeed) ("found_secrets", found_secrets) ("found_items", found_items) ("killed_monsters", killed_monsters) @@ -977,6 +978,7 @@ void FLevelLocals::Serialize(FSerializer &arc, bool hubload) ("totaltime", i) ("skytexture1", skytexture1) ("skytexture2", skytexture2) + ("skymisttexture", skymisttexture) ("fogdensity", fogdensity) ("outsidefogdensity", outsidefogdensity) ("skyfog", skyfog) diff --git a/src/p_setup.cpp b/src/p_setup.cpp index c18cb1621..b948d4734 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -212,6 +212,10 @@ static void PrecacheLevel(FLevelLocals *Level) { AddToList(hitlist.Data(), Level->skytexture2, FTextureManager::HIT_Sky); } + if (Level->skymisttexture.isValid()) + { + AddToList(hitlist.Data(), Level->skymisttexture, FTextureManager::HIT_Sky); + } static const BITFIELD checkForTextureFlags = FTextureManager::TEXMAN_Overridable | FTextureManager::TEXMAN_TryAny | FTextureManager::TEXMAN_ReturnFirst | FTextureManager::TEXMAN_DontCreate; diff --git a/src/playsim/p_acs.cpp b/src/playsim/p_acs.cpp index aa26c0163..63432a9eb 100644 --- a/src/playsim/p_acs.cpp +++ b/src/playsim/p_acs.cpp @@ -5545,6 +5545,7 @@ int DLevelScript::CallFunction(int argCount, int funcIndex, int32_t *args, int & { if (args[0] == 1) Level->skyspeed1 = ACSToFloat(args[1]); else if (args[0] == 2) Level->skyspeed2 = ACSToFloat(args[1]); + else if (args[0] == 3) Level->skymistspeed = ACSToFloat(args[1]); return 1; } diff --git a/src/rendering/hwrenderer/scene/hw_portal.h b/src/rendering/hwrenderer/scene/hw_portal.h index 207cefc2f..64952b88b 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.h +++ b/src/rendering/hwrenderer/scene/hw_portal.h @@ -13,9 +13,9 @@ class FSkyBox; struct HWSkyInfo { - float x_offset[2]; + float x_offset[3]; float y_offset; // doubleskies don't have a y-offset - FGameTexture * texture[2]; + FGameTexture * texture[3]; FTextureID skytexno1; bool mirrored; bool doublesky; @@ -351,7 +351,6 @@ public: }; - struct HWSkyPortal : public HWPortal { HWSkyInfo * origin; diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index ce89b545a..a3fc89902 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -118,6 +118,9 @@ void HWSkyInfo::init(HWDrawInfo *di, sector_t* sec, int skypos, int sky1, PalEnt } else fadecolor = 0; + texture[2] = TexMan.GetGameTexture(di->Level->skymisttexture); + x_offset[2] = di->Level->hw_skymistpos; + } diff --git a/src/rendering/hwrenderer/scene/hw_skyportal.cpp b/src/rendering/hwrenderer/scene/hw_skyportal.cpp index 656b9dd16..6f1e0c1da 100644 --- a/src/rendering/hwrenderer/scene/hw_skyportal.cpp +++ b/src/rendering/hwrenderer/scene/hw_skyportal.cpp @@ -74,19 +74,26 @@ void HWSkyPortal::DrawContents(HWDrawInfo *di, FRenderState &state) vertexBuffer->RenderDome(state, origin->texture[0], origin->x_offset[0], origin->y_offset, origin->mirrored, FSkyVertexBuffer::SKYMODE_MAINLAYER, !!(di->Level->flags & LEVEL_FORCETILEDSKY)); state.SetTextureMode(TM_NORMAL); } - + state.AlphaFunc(Alpha_Greater, 0.f); if (origin->doublesky && origin->texture[1]) { vertexBuffer->RenderDome(state, origin->texture[1], origin->x_offset[1], origin->y_offset, false, FSkyVertexBuffer::SKYMODE_SECONDLAYER, !!(di->Level->flags & LEVEL_FORCETILEDSKY)); } + } - if (di->Level->skyfog>0 && !di->isFullbrightScene() && (origin->fadecolor & 0xffffff) != 0) + if (di->Level->skyfog>0 && (origin->fadecolor & 0xffffff) != 0) + { + PalEntry FadeColor = origin->fadecolor; + FadeColor.a = clamp(di->Level->skyfog, 0, 255); + + if (di->Level->flags3 & LEVEL3_SKYMIST && origin->texture[2]) + { + vertexBuffer->RenderDome(state, origin->texture[2], origin->x_offset[2], 0.f, false, FSkyVertexBuffer::SKYMODE_FOGLAYER, !!(di->Level->flags & LEVEL_FORCETILEDSKY), 0, 0, FadeColor); + } + else if (!di->isFullbrightScene()) { - PalEntry FadeColor = origin->fadecolor; - FadeColor.a = clamp(di->Level->skyfog, 0, 255); - state.EnableTexture(false); state.SetObjectColor(FadeColor); state.Draw(DT_Triangles, 0, 12); diff --git a/src/rendering/r_sky.cpp b/src/rendering/r_sky.cpp index 4d9e2a5cf..22e339e0d 100644 --- a/src/rendering/r_sky.cpp +++ b/src/rendering/r_sky.cpp @@ -79,6 +79,10 @@ void InitSkyMap(FLevelLocals *Level) { Level->skytexture1 = TexMan.GetFrontSkyLayer(Level->skytexture1); } + if (Level->skymisttexture.isNull()) + { + Level->skymisttexture = TexMan.CheckForTexture("skymist1", ETextureType::Any); + } skytex1 = TexMan.GetGameTexture(Level->skytexture1, false); skytex2 = TexMan.GetGameTexture(Level->skytexture2, false); @@ -143,6 +147,7 @@ void R_UpdateSky (uint64_t mstime) // The hardware renderer uses a different value range and clamps it to a single rotation Level->hw_sky1pos = (float)(fmod((double(mstime) * Level->skyspeed1), 1024.) * (90. / 256.)); Level->hw_sky2pos = (float)(fmod((double(mstime) * Level->skyspeed2), 1024.) * (90. / 256.)); + Level->hw_skymistpos = (float)(fmod((double(mstime) * Level->skymistspeed), 1024.) * (90. / 256.)); } } diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 5d3aea03f..8388b8f9c 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -1738,6 +1738,15 @@ DEFINE_ACTION_FUNCTION_NATIVE(_Sector, SetXOffset, SetXOffset) return 0; } + DEFINE_ACTION_FUNCTION(FLevelLocals, ChangeSkyMist) + { + PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); + PARAM_INT(skymist); + self->skymisttexture = FSetTextureID(skymist); + InitSkyMap(self); + return 0; + } + DEFINE_ACTION_FUNCTION(FLevelLocals, StartIntermission) { PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); @@ -2802,6 +2811,7 @@ DEFINE_FIELD_X(LevelInfo, level_info_t, NextMap) DEFINE_FIELD_X(LevelInfo, level_info_t, NextSecretMap) DEFINE_FIELD_X(LevelInfo, level_info_t, SkyPic1) DEFINE_FIELD_X(LevelInfo, level_info_t, SkyPic2) +DEFINE_FIELD_X(LevelInfo, level_info_t, SkyMistPic) DEFINE_FIELD_X(LevelInfo, level_info_t, F1Pic) DEFINE_FIELD_X(LevelInfo, level_info_t, cluster) DEFINE_FIELD_X(LevelInfo, level_info_t, partime) @@ -2817,6 +2827,7 @@ DEFINE_FIELD_X(LevelInfo, level_info_t, MapLabel) DEFINE_FIELD_X(LevelInfo, level_info_t, musicorder) DEFINE_FIELD_X(LevelInfo, level_info_t, skyspeed1) DEFINE_FIELD_X(LevelInfo, level_info_t, skyspeed2) +DEFINE_FIELD_X(LevelInfo, level_info_t, skymistspeed) DEFINE_FIELD_X(LevelInfo, level_info_t, cdtrack) DEFINE_FIELD_X(LevelInfo, level_info_t, gravity) DEFINE_FIELD_X(LevelInfo, level_info_t, aircontrol) @@ -2860,8 +2871,10 @@ DEFINE_FIELD(FLevelLocals, Music) DEFINE_FIELD(FLevelLocals, musicorder) DEFINE_FIELD(FLevelLocals, skytexture1) DEFINE_FIELD(FLevelLocals, skytexture2) +DEFINE_FIELD(FLevelLocals, skymisttexture) DEFINE_FIELD(FLevelLocals, skyspeed1) DEFINE_FIELD(FLevelLocals, skyspeed2) +DEFINE_FIELD(FLevelLocals, skymistspeed) DEFINE_FIELD(FLevelLocals, total_secrets) DEFINE_FIELD(FLevelLocals, found_secrets) DEFINE_FIELD(FLevelLocals, total_items) From b68f04fa7556457866f8cd9b4cdded0ad4fb9cdf Mon Sep 17 00:00:00 2001 From: dileepvr Date: Sat, 26 Apr 2025 10:57:13 -0600 Subject: [PATCH 149/384] Allow skymist to animate Allow animated skymist layer. --- src/rendering/hwrenderer/scene/hw_sky.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index a3fc89902..9ff480884 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -118,7 +118,7 @@ void HWSkyInfo::init(HWDrawInfo *di, sector_t* sec, int skypos, int sky1, PalEnt } else fadecolor = 0; - texture[2] = TexMan.GetGameTexture(di->Level->skymisttexture); + texture[2] = TexMan.GetGameTexture(di->Level->skymisttexture, true); x_offset[2] = di->Level->hw_skymistpos; } From cfae4be8fb9326957d51d89b6541ddc93d023183 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sat, 26 Apr 2025 11:30:35 -0600 Subject: [PATCH 150/384] Forgot to add the skymist lumps. --- wadsrc/static/textures/skymist1.png | Bin 0 -> 143 bytes wadsrc/static/textures/skymist2.png | Bin 0 -> 147 bytes wadsrc/static/textures/skymist3.png | Bin 0 -> 191 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 wadsrc/static/textures/skymist1.png create mode 100644 wadsrc/static/textures/skymist2.png create mode 100644 wadsrc/static/textures/skymist3.png diff --git a/wadsrc/static/textures/skymist1.png b/wadsrc/static/textures/skymist1.png new file mode 100644 index 0000000000000000000000000000000000000000..0a4a3db42d6f276957fb5af9c253e559beb58796 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU|?imU{c^&BxFe}m^gKH(aDSFFmzJwWEukJV>1F13~S7BWVyykf)R->{m2fq}u()z4*}Q$iB} D3PCqr literal 0 HcmV?d00001 diff --git a/wadsrc/static/textures/skymist3.png b/wadsrc/static/textures/skymist3.png new file mode 100644 index 0000000000000000000000000000000000000000..4eb8cf92548a5903626b4fb2b9c8a3e3d67eea9d GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0y~yU|?imU{c^S{*8DMTJKg{3l$?N$+xpY%8rda3Yw-CWYur{C8m3sf xGSoECcrQ!d!XIwO|F!bvH-F2||My&d*P%l_KR9!G85kHCJYD@<);T3K0RXv3O0EC^ literal 0 HcmV?d00001 From 54a100d97556011dd23a906a293ff24dd38108db Mon Sep 17 00:00:00 2001 From: Owlet7 Date: Sun, 6 Apr 2025 05:26:11 +0300 Subject: [PATCH 151/384] Add support for ID24 monsters and decorations --- .../static/filter/game-doomchex/sndinfo.txt | 47 ++++ wadsrc/static/mapinfo/doomitems.txt | 51 ++++ wadsrc/static/zscript.txt | 10 + .../actors/doom/id24/id24ambientsounds.zs | 65 +++++ .../zscript/actors/doom/id24/id24banshee.zs | 65 +++++ .../zscript/actors/doom/id24/id24ghoul.zs | 94 +++++++ .../zscript/actors/doom/id24/id24gore.zs | 248 ++++++++++++++++ .../actors/doom/id24/id24mindweaver.zs | 74 +++++ .../zscript/actors/doom/id24/id24nature.zs | 266 ++++++++++++++++++ .../zscript/actors/doom/id24/id24plasmaguy.zs | 128 +++++++++ .../zscript/actors/doom/id24/id24tech.zs | 101 +++++++ .../zscript/actors/doom/id24/id24tyrant.zs | 98 +++++++ .../zscript/actors/doom/id24/id24vassago.zs | 145 ++++++++++ 13 files changed, 1392 insertions(+) create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24ambientsounds.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24banshee.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24gore.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24nature.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24tech.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs create mode 100644 wadsrc/static/zscript/actors/doom/id24/id24vassago.zs diff --git a/wadsrc/static/filter/game-doomchex/sndinfo.txt b/wadsrc/static/filter/game-doomchex/sndinfo.txt index d571cfd01..84b6e68f7 100644 --- a/wadsrc/static/filter/game-doomchex/sndinfo.txt +++ b/wadsrc/static/filter/game-doomchex/sndinfo.txt @@ -460,6 +460,53 @@ $alias intermission/pastdmstats *gibbed // id24 sounds +world/officelamp = DSBREAK + +ambient/klaxon = DSKLAXON +ambient/portalopen = DSGATOPN +ambient/portalloop = DSGATLOP +ambient/portalclose = DSGATCLS + +monsters/ghoul/sight = DSGHLSIT +monsters/ghoul/pain = DSGHLPAI +monsters/ghoul/death = DSGHLDTH +monsters/ghoul/active = DSGHLACT +monsters/ghoul/attack = DSFIRSHT +monsters/ghoul/shotx = DSFIRXPL + +monsters/banshee/sight = DSBANACT +monsters/banshee/pain = DSBANPAI +monsters/banshee/death = DSBANDTH +monsters/banshee/active = DSBANACT + +monsters/mindweaver/sight = DSCSPSIT +monsters/mindweaver/pain = DSDMPAIN +monsters/mindweaver/death = DSCSPDTH +monsters/mindweaver/active = DSCSPACT +monsters/mindweaver/walk = DSCSPWLK + +monsters/plasmaguy/pain = DSPPOPAI +monsters/plasmaguy/death = DSPPODTH +monsters/plasmaguy/active = DSPPOACT +monsters/plasmaguy/head = DSPPOHED + +monsters/vassago/sight = DSVASSIT +monsters/vassago/pain = DSVASPAI +monsters/vassago/death = DSVASDTH +monsters/vassago/active = DSVASACT +monsters/vassago/attack = DSVASATK +monsters/vassago/burn = DSINCBRN +monsters/vassago/shotx = DSFLAME +monsters/vassago/hot1 = DSINCHT1 +monsters/vassago/hot2 = DSINCHT2 +monsters/vassago/hot3 = DSINCHT3 + +monsters/tyrant/sight = DSTYRSIT +monsters/tyrant/pain = DSTYRPAI +monsters/tyrant/death = DSTYRDTH +monsters/tyrant/active = DSTYRACT +monsters/tyrant/walk = DSTYRWLK + weapons/incinerator/fire1 DSINCFI1 weapons/incinerator/fire2 DSINCFI2 weapons/incinerator/burn DSINCBRN diff --git a/wadsrc/static/mapinfo/doomitems.txt b/wadsrc/static/mapinfo/doomitems.txt index f7db6f006..aa4fff87e 100644 --- a/wadsrc/static/mapinfo/doomitems.txt +++ b/wadsrc/static/mapinfo/doomitems.txt @@ -119,6 +119,57 @@ DoomEdNums 3004 = Zombieman 3005 = Cacodemon 3006 = LostSoul + 3007 = ID24Ghoul + 3008 = ID24Banshee + 3009 = ID24Mindweaver + 3010 = ID24PlasmaGuy + 3011 = ID24Vassago + 3012 = ID24Tyrant + 3013 = ID24TyrantBoss1 + 3014 = ID24TyrantBoss2 + 3100 = ID24GrayStalagmite + 3101 = ID24LargeCorpsePile + 3102 = ID24HumanBBQ1 + 3103 = ID24HumanBBQ2 + 3104 = ID24HangingBodyBothLegs + 3105 = ID24HangingBodyBothLegsSolid + 3106 = ID24HangingBodyCrucified + 3107 = ID24HangingBodyCrucifiedSolid + 3108 = ID24HangingBodyArmsBound + 3109 = ID24HangingBodyArmsBoundSolid + 3110 = ID24HangingBaronOfHell + 3111 = ID24HangingBaronOfHellSolid + 3112 = ID24HangingChainedBody + 3113 = ID24HangingChainedBodySolid + 3114 = ID24HangingChainedTorso + 3115 = ID24HangingChainedTorsoSolid + 3116 = ID24SkullPoleTrio + 3117 = ID24SkullGibs + 3118 = ID24BushShort + 3119 = ID24BushShortBurned1 + 3120 = ID24BushShortBurned2 + 3121 = ID24BushTall + 3122 = ID24BushTallBurned1 + 3123 = ID24BushTallBurned2 + 3124 = ID24CaveRockColumn + 3125 = ID24CaveStalagmiteLarge + 3126 = ID24CaveStalagmiteMedium + 3127 = ID24CaveStalagmiteSmall + 3128 = ID24CaveStalactiteLarge + 3129 = ID24CaveStalactiteLargeSolid + 3130 = ID24CaveStalactiteMedium + 3131 = ID24CaveStalactiteMediumSolid + 3132 = ID24CaveStalactiteSmall + 3133 = ID24CaveStalactiteSmallSolid + 3134 = ID24OfficeChair + 3135 = ID24OfficeLamp + 3136 = ID24LiveWire + 3137 = ID24CeilingLamp + 3138 = ID24CandelabraShort + 3139 = ID24AmbientKlaxon + 3140 = ID24AmbientPortalOpen + 3141 = ID24AmbientPortalLoop + 3142 = ID24AmbientPortalClose 4001 = "$Player5Start" 4002 = "$Player6Start" 4003 = "$Player7Start" diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt index 54939e70a..371063dab 100644 --- a/wadsrc/static/zscript.txt +++ b/wadsrc/static/zscript.txt @@ -124,6 +124,16 @@ version "4.15.1" #include "zscript/actors/doom/weaponbfg.zs" #include "zscript/actors/doom/dehacked.zs" +#include "zscript/actors/doom/id24/id24gore.zs" +#include "zscript/actors/doom/id24/id24nature.zs" +#include "zscript/actors/doom/id24/id24tech.zs" +#include "zscript/actors/doom/id24/id24ambientsounds.zs" +#include "zscript/actors/doom/id24/id24ghoul.zs" +#include "zscript/actors/doom/id24/id24banshee.zs" +#include "zscript/actors/doom/id24/id24mindweaver.zs" +#include "zscript/actors/doom/id24/id24plasmaguy.zs" +#include "zscript/actors/doom/id24/id24vassago.zs" +#include "zscript/actors/doom/id24/id24tyrant.zs" #include "zscript/actors/doom/id24/id24ammo.zs" #include "zscript/actors/doom/id24/id24incinerator.zs" #include "zscript/actors/doom/id24/id24calamityblade.zs" diff --git a/wadsrc/static/zscript/actors/doom/id24/id24ambientsounds.zs b/wadsrc/static/zscript/actors/doom/id24/id24ambientsounds.zs new file mode 100644 index 000000000..b82a0d712 --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24ambientsounds.zs @@ -0,0 +1,65 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - ambient sounds + ****************************************************************************/ + +class ID24AmbientKlaxon : Actor +{ + Default + { + Radius 8; + Height 16; + } + States + { + Spawn: + TNT1 A 1 A_Look; + Loop; + See: + TNT1 A 35 A_StartSound("ambient/klaxon"); + Loop; + } +} + +class ID24AmbientPortalOpen : ID24AmbientKlaxon +{ + States + { + See: + TNT1 A 250 A_StartSound("ambient/portalopen", 1); + Stop; + } +} + +class ID24AmbientPortalLoop : ID24AmbientKlaxon +{ + States + { + See: + TNT1 A 139 A_StartSound("ambient/portalloop"); + Loop; + } +} + +class ID24AmbientPortalClose : ID24AmbientKlaxon +{ + States + { + See: + TNT1 A 105 A_StartSound("ambient/portalclose", 1); + Stop; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs b/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs new file mode 100644 index 000000000..cf8083d8e --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs @@ -0,0 +1,65 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - banshee + ****************************************************************************/ + +//converted from DECOHACK + +class ID24Banshee : Actor +{ + Default + { + Health 100; + Speed 8; + Radius 20; + Height 56; + ReactionTime 8; + PainChance 64; + Mass 500; + Monster; + +FLOAT + +NOGRAVITY + SeeSound "monsters/banshee/sight"; + PainSound "monsters/banshee/pain"; + DeathSound "monsters/banshee/death"; + Tag "$FN_ID24BANSHEE"; + } + States + { + Spawn: + BSHE AB 10 BRIGHT A_Look; + Loop; + See: + BSHE AAABBBCCCAAABBBCCC 2 BRIGHT A_Chase; + BSHE A 0 BRIGHT A_StartSound("monsters/banshee/active"); + Loop; + Melee: + BSHE D 1 BRIGHT A_RadiusDamage(100, 8); + Wait; + Pain: + BSHE D 3 BRIGHT; + BSHE D 3 BRIGHT A_Pain; + Goto See; + Death: + BSHE D 4 BRIGHT A_Scream; + BSHE E 6 BRIGHT A_RadiusDamage(128, 128); + BSHE F 8 BRIGHT A_Fall; + BSHE G 6 BRIGHT; + BSHE H 4 BRIGHT; + TNT1 A 20; + Stop; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs b/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs new file mode 100644 index 000000000..fa07b84b9 --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs @@ -0,0 +1,94 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - ghoul + ****************************************************************************/ + +//converted from DECOHACK + +class ID24Ghoul : Actor +{ + Default + { + Health 50; + Speed 12; + Radius 16; + Height 40; + ReactionTime 8; + PainChance 128; + Mass 50; + Monster; + +FLOAT + +NOGRAVITY + SeeSound "monsters/ghoul/sight"; + PainSound "monsters/ghoul/pain"; + DeathSound "monsters/ghoul/death"; + ActiveSound "monsters/ghoul/active"; + Tag "$FN_ID24GHOUL"; + } + States + { + Spawn: + GHUL AB 10 A_Look; + Loop; + See: + GHUL AABBCCBB 3 A_Chase; + Loop; + Missile: + GHUL DE 4 BRIGHT A_FaceTarget; + GHUL F 4 BRIGHT MBF21_MonsterProjectile("ID24GhoulBall", 0.0, 0.0, 0.0, -8.0); + GHUL G 4 BRIGHT; + Goto See; + Pain: + GHUL I 3 BRIGHT; + GHUL K 3 BRIGHT A_Pain; + Goto See; + Death: + GHUL L 5 BRIGHT; + GHUL M 5 BRIGHT A_Scream; + GHUL NO 5 BRIGHT; + GHUL P 5 BRIGHT A_Fall; + GHUL QR 5 BRIGHT; + GHUL S -1; + Stop; + } +} + +class ID24GhoulBall : Actor +{ + Default + { + Damage 3; + Speed 15; + FastSpeed 20; + Radius 6; + Height 8; + Projectile; + +ZDOOMTRANS + RenderStyle "Add"; + SeeSound "monsters/ghoul/attack"; + DeathSound "monsters/ghoul/shotx"; + } + States + { + Spawn: + GBAL AB 4 BRIGHT; + Loop; + Death: + GBAL C 5 BRIGHT; + APBX BCDE 5 BRIGHT; + Stop; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24gore.zs b/wadsrc/static/zscript/actors/doom/id24/id24gore.zs new file mode 100644 index 000000000..b43721431 --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24gore.zs @@ -0,0 +1,248 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - gore + ****************************************************************************/ + +//converted from DECOHACK + +class ID24LargeCorpsePile : Actor +{ + Default + { + Radius 40; + Height 16; + +SOLID + } + States + { + Spawn: + POL7 A -1; + Stop; + } +} + +class ID24HumanBBQ1 : Actor +{ + Default + { + Radius 16; + Height 16; + +SOLID + } + States + { + Spawn: + HBBQ ABC 5 BRIGHT; + Loop; + } +} + +class ID24HumanBBQ2 : Actor +{ + Default + { + Radius 16; + Height 16; + +SOLID + } + States + { + Spawn: + HBB2 ABC 5 BRIGHT; + Loop; + } +} + +class ID24HangingBodyBothLegs : Actor +{ + Default + { + Radius 16; + Height 80; + +NOGRAVITY + +SPAWNCEILING + } + States + { + Spawn: + GOR6 A -1; + Stop; + } +} + +class ID24HangingBodyBothLegsSolid : ID24HangingBodyBothLegs +{ + Default + { + +SOLID + } +} + +class ID24HangingBodyCrucified : Actor +{ + Default + { + Radius 16; + Height 64; + +NOGRAVITY + +SPAWNCEILING + } + States + { + Spawn: + GOR7 A -1; + Stop; + } +} + +class ID24HangingBodyCrucifiedSolid : ID24HangingBodyCrucified +{ + Default + { + +SOLID + } +} + +class ID24HangingBodyArmsBound : Actor +{ + Default + { + Radius 16; + Height 72; + +NOGRAVITY + +SPAWNCEILING + } + States + { + Spawn: + GOR8 A -1; + Stop; + } +} + +class ID24HangingBodyArmsBoundSolid : ID24HangingBodyArmsBound +{ + Default + { + +SOLID + } +} + +class ID24HangingBaronOfHell : Actor +{ + Default + { + Radius 16; + Height 80; + +NOGRAVITY + +SPAWNCEILING + } + States + { + Spawn: + GORA A -1; + Stop; + } +} + +class ID24HangingBaronOfHellSolid : ID24HangingBaronOfHell +{ + Default + { + +SOLID + } +} + +class ID24HangingChainedBody : Actor +{ + Default + { + Radius 16; + Height 84; + +NOGRAVITY + +SPAWNCEILING + } + States + { + Spawn: + HDB7 A -1; + Stop; + } +} + +class ID24HangingChainedBodySolid : ID24HangingChainedBody +{ + Default + { + +SOLID + } +} + +class ID24HangingChainedTorso : Actor +{ + Default + { + Radius 16; + Height 52; + +NOGRAVITY + +SPAWNCEILING + } + States + { + Spawn: + HDB8 A -1; + Stop; + } +} + +class ID24HangingChainedTorsoSolid : ID24HangingChainedTorso +{ + Default + { + +SOLID + } +} + +class ID24SkullPoleTrio : Actor +{ + Default + { + Radius 16; + Height 16; + +SOLID + } + States + { + Spawn: + POLA A -1; + Stop; + } +} + +class ID24SkullGibs : Actor +{ + Default + { + Radius 16; + Height 16; + } + States + { + Spawn: + POB6 A -1; + Stop; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs b/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs new file mode 100644 index 000000000..caf74fcbc --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs @@ -0,0 +1,74 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - mindweaver + ****************************************************************************/ + +//converted from DECOHACK + +class ID24Mindweaver : Actor +{ + Default + { + Health 500; + Speed 12; + Radius 64; + Height 64; + ReactionTime 8; + PainChance 40; + Mass 600; + Monster; + SeeSound "monsters/mindweaver/sight"; + PainSound "monsters/mindweaver/pain"; + DeathSound "monsters/mindweaver/death"; + ActiveSound "monsters/mindweaver/active"; + Tag "$FN_ID24MINDWEAVER"; + } + States + { + Spawn: + CSPI AB 10 A_Look; + Loop; + See: + CSPI A 20; + Walk: + CSPI A 0 A_StartSound("monsters/mindweaver/walk"); + CSPI AABBCC 3 A_Chase; + CSPI D 0 A_StartSound("monsters/mindweaver/walk"); + CSPI DDEEFF 3 A_Chase; + Loop; + Missile: + CSPI A 20 BRIGHT A_FaceTarget; + Fighting: + CSPI G 4 BRIGHT A_SPosAttack; + CSPI H 4 BRIGHT A_SPosAttack; + CSPI H 1 BRIGHT A_SpidRefire; + Goto Fighting; + Pain: + CSPI I 3; + CSPI I 3 A_Pain; + Goto Walk; + Death: + CSPI J 20 A_Scream; + CSPI K 7 A_Fall; + CSPI LMNO 7; + CSPI P -1 A_BossDeath; + Stop; + Raise: + CSPI P 5; + CSPI ONMLKJ 5; + Goto Walk; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24nature.zs b/wadsrc/static/zscript/actors/doom/id24/id24nature.zs new file mode 100644 index 000000000..156c0f3a4 --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24nature.zs @@ -0,0 +1,266 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - nature + ****************************************************************************/ + +//converted from DECOHACK + +class ID24GrayStalagmite : Actor +{ + Default + { + Radius 16; + Height 16; + +SOLID + } + States + { + Spawn: + SMT2 A -1; + Stop; + } +} + +class ID24BushShort : Actor +{ + Default + { + Radius 12; + Height 16; + +SOLID + } + States + { + Spawn: + BSH1 A -1; + Stop; + } +} + +class ID24BushShortBurned1 : Actor +{ + Default + { + Radius 12; + Height 16; + } + States + { + Spawn: + BSH1 B -1; + Stop; + } +} + +class ID24BushShortBurned2 : Actor +{ + Default + { + Radius 12; + Height 16; + } + States + { + Spawn: + BSH1 C -1; + Stop; + } +} + +class ID24BushTall : Actor +{ + Default + { + Radius 12; + Height 16; + +SOLID + } + States + { + Spawn: + BSH2 A -1; + Stop; + } +} + +class ID24BushTallBurned1 : Actor +{ + Default + { + Radius 12; + Height 16; + } + States + { + Spawn: + BSH2 B -1; + Stop; + } +} + +class ID24BushTallBurned2 : Actor +{ + Default + { + Radius 12; + Height 16; + } + States + { + Spawn: + BSH2 C -1; + Stop; + } +} + +class ID24CaveRockColumn : Actor +{ + Default + { + Radius 24; + Height 16; + +SOLID + } + States + { + Spawn: + STMI A -1; + Stop; + } +} + +class ID24CaveStalagmiteLarge : Actor +{ + Default + { + Radius 16; + Height 16; + +SOLID + } + States + { + Spawn: + STG1 A -1; + Stop; + } +} + +class ID24CaveStalagmiteMedium : Actor +{ + Default + { + Radius 12; + Height 16; + +SOLID + } + States + { + Spawn: + STG2 A -1; + Stop; + } +} + +class ID24CaveStalagmiteSmall : Actor +{ + Default + { + Radius 8; + Height 16; + +SOLID + } + States + { + Spawn: + STG3 A -1; + Stop; + } +} + +class ID24CaveStalactiteLarge : Actor +{ + Default + { + Radius 16; + Height 112; + +SPAWNCEILING + +NOGRAVITY + } + States + { + Spawn: + STC1 A -1; + Stop; + } +} + +class ID24CaveStalactiteLargeSolid : ID24CaveStalactiteLarge +{ + Default + { + +SOLID + } +} + +class ID24CaveStalactiteMedium : Actor +{ + Default + { + Radius 12; + Height 64; + +SPAWNCEILING + +NOGRAVITY + } + States + { + Spawn: + STC2 A -1; + Stop; + } +} + +class ID24CaveStalactiteMediumSolid : ID24CaveStalactiteMedium +{ + Default + { + +SOLID + } +} + +class ID24CaveStalactiteSmall : Actor +{ + Default + { + Radius 8; + Height 32; + +SPAWNCEILING + +NOGRAVITY + } + States + { + Spawn: + STC3 A -1; + Stop; + } +} + +class ID24CaveStalactiteSmallSolid : ID24CaveStalactiteSmall +{ + Default + { + +SOLID + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs b/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs new file mode 100644 index 000000000..243ca0ccd --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs @@ -0,0 +1,128 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - former shocktrooper (a.k.a. "plasma guy" or "plasmadick") + ****************************************************************************/ + +//converted from DECOHACK + +class ID24PlasmaGuy : Actor +{ + Default + { + Health 100; + Speed 10; + Radius 20; + Height 56; + ReactionTime 8; + PainChance 30; + Mass 100; + Monster; + PainSound "monsters/plasmaguy/pain"; + DeathSound "monsters/plasmaguy/death"; + ActiveSound "monsters/plasmaguy/active"; + Tag "$FN_ID24PLASMAGUY"; + } + States + { + Spawn: + PPOS AB 10 A_Look; + Loop; + See: + PPOS AABBCCDD 2 Fast A_Chase; + Loop; + Missile: + PPOS E 10 A_FaceTarget; + PPOS F 2 Fast MBF21_MonsterProjectile("PlasmaBall", 0.0, 0.0, 0.0, 0.0); + PPOS E 4 Fast; + PPOS F 2 Fast MBF21_MonsterProjectile("PlasmaBall", 0.0, 0.0, 0.0, 0.0); + PPOS E 4 Fast; + PPOS F 2 Fast MBF21_MonsterProjectile("PlasmaBall", 0.0, 0.0, 0.0, 0.0); + PPOS E 4 Fast; + Goto See; + Pain: + PPOS G 5 Fast; + PPOS G 5 Fast A_Pain; + Goto See; + Death: + PPOS H 0 A_FaceTarget; + PPOS H 5 MBF21_SpawnObject("ID24PlasmaGuyHead", 175.0, 0.0, 0.0, 40.0, 2.0, 0.0, 1.5); + PPOS I 5 A_Scream; + PPOS J 5 A_Fall; + PPOS KL 5; + PPOS M -1; + Stop; + XDeath: + PPOS N 5; + PPOS O 5 A_XScream; + PPOS P 5 A_Fall; + PPOS Q 0 A_FaceTarget; + PPOS Q 5 MBF21_SpawnObject("ID24PlasmaGuyTorso", 170.0, 0.0, -8.0, 32.0, 4.0, 0.0, 2.0); + PPOS RST 5; + PPOS U -1; + Stop; + Raise: + PPOS MLKJIH 5; + Goto See; + } +} + +class ID24PlasmaGuyHead : Actor +{ + Default + { + Damage 0; + Speed 8; + Radius 6; + Height 16; + Gravity 0.125; + +MISSILE + +DROPOFF + DeathSound "monsters/plasmaguy/head"; + } + States + { + Spawn: + PHED ABCDEFGHI 3; + Loop; + Death: + PHED J -1; + Loop; + } +} + +class ID24PlasmaGuyTorso : Actor +{ + Default + { + Damage 0; + Speed 8; + Radius 6; + Height 16; + +NOBLOCKMAP + +MISSILE + +DROPOFF + } + States + { + Spawn: + PPOS V -1; + Loop; + Death: + PPOS W 5; + PPOS X -1; + Stop; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24tech.zs b/wadsrc/static/zscript/actors/doom/id24/id24tech.zs new file mode 100644 index 000000000..b886e097e --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24tech.zs @@ -0,0 +1,101 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - tech + ****************************************************************************/ + +//converted from DECOHACK + +class ID24OfficeChair : Actor +{ + Default + { + Radius 12; + Height 16; + +SOLID + } + States + { + Spawn: + CHR1 A -1; + Stop; + } +} + +class ID24OfficeLamp : Actor +{ + Default + { + Radius 12; + Height 52; + Health 20; + Mass 65536; + DeathSound "world/officelamp"; + +SOLID + +SHOOTABLE + +NOBLOOD + +DONTGIB + +NOICEDEATH + } + States + { + Spawn: + LAMP A -1 BRIGHT; + Stop; + Death: + LAMP B 3 BRIGHT A_Scream; + LAMP B 3 BRIGHT A_Fall; + DeathLoop: + LAMP CD 5 BRIGHT; + Loop; + } +} + +// this actor has an editor number in the decohack, +// but it doesn't actually exist in it +class ID24LiveWire : Actor { } + +class ID24CeilingLamp : Actor +{ + Default + { + Radius 12; + Height 32; + +SPAWNCEILING + +NOGRAVITY + } + States + { + Spawn: + TLP6 A -1 BRIGHT; + Stop; + } +} + +class ID24CandelabraShort : Actor +{ + Default + { + Radius 16; + Height 16; + +SOLID + } + States + { + Spawn: + CBR2 A -1 BRIGHT; + Stop; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs b/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs new file mode 100644 index 000000000..d7e30e181 --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs @@ -0,0 +1,98 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - tyrant + ****************************************************************************/ + +//converted from DECOHACK + +class ID24Tyrant : Cyberdemon +{ + Default + { + Health 1000; + ProjectileGroup 2; + SplashGroup 2; + Monster; + +BOSS + SeeSound "monsters/tyrant/sight"; + PainSound "monsters/tyrant/pain"; + DeathSound "monsters/tyrant/death"; + ActiveSound "monsters/tyrant/active"; + Tag "$FN_ID24TYRANT"; + } + States + { + Spawn: + CYB2 AB 10 A_Look; + Loop; + See: + CYB2 A 3 A_Hoof; + CYB2 ABBCC 3 A_Chase; + CYB2 D 0 A_StartSound("monsters/tyrant/walk"); + CYB2 DD 3 A_Chase; + Loop; + Missile: + CYB2 E 6 A_FaceTarget; + CYB2 F 12 BRIGHT A_CyberAttack; + CYB2 E 12 A_FaceTarget; + CYB2 F 12 BRIGHT A_CyberAttack; + CYB2 E 12 A_FaceTarget; + CYB2 F 12 BRIGHT A_CyberAttack; + Goto See; + Pain: + CYB2 G 10 A_Pain; + goto See; + Death: + CYB2 H 10; + CYB2 I 10 A_Scream; + CYB2 JKL 10; + CYB2 M 10 A_Fall; + CYB2 NO 10; + CYB2 P 30; + CYB2 P -1 A_BossDeath; + Stop; + } +} + +// "boss" variants -- these are simply alternative +// actors for use with UMAPINFO's "bossaction" +// field to make the final map's staged fights +// work nicely. they're functionally identical +// to the main actor in every other way. + +// [XA] well, now they also have a 60-second respawn +// minimum instead of the default 12, so Nightmare +// sucks a bit less ;) + +// (this id24 feature isn't implemented yet!) + +class ID24TyrantBoss1 : ID24Tyrant +{ + Default + { + // RespawnTics 2100; + // RespawnDice 64; + } +} + +class ID24TyrantBoss2 : ID24Tyrant +{ + Default + { + // RespawnTics 2100; + // RespawnDice 64; + } +} diff --git a/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs b/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs new file mode 100644 index 000000000..ad8ab9ec9 --- /dev/null +++ b/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs @@ -0,0 +1,145 @@ +/***************************************************************************** + * Copyright (C) 1993-2024 id Software LLC, a ZeniMax Media company. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ****************************************************************************/ + +/***************************************************************************** + * id1 - decohack - vassago + ****************************************************************************/ + +//converted from DECOHACK + +class ID24Vassago : Actor +{ + Default + { + Health 1000; + Speed 8; + Radius 24; + Height 64; + ReactionTime 8; + PainChance 100; + Mass 1000; + SplashGroup 1; + Monster; + SeeSound "monsters/vassago/sight"; + PainSound "monsters/vassago/pain"; + DeathSound "monsters/vassago/death"; + ActiveSound "monsters/vassago/active"; + Tag "$FN_ID24VASSAGO"; + } + States + { + Spawn: + VASS AB 10 A_Look; + Loop; + See: + VASS AABBCCDD 3 A_Chase; + Loop; + Melee: + Missile: + VASS E 0 BRIGHT A_StartSound("monsters/vassago/attack"); + VASS E 8 BRIGHT A_FaceTarget; + VASS FG 4 BRIGHT A_FaceTarget; + VASS H 8 BRIGHT MBF21_MonsterProjectile("ID24VassagoFlame", 0.0, 0.0, 0.0, 0.0); + Goto See; + Pain: + VASS I 2; + VASS I 2 A_Pain; + Goto See; + Death: + VASS J 8 BRIGHT; + VASS K 8 BRIGHT A_Scream; + VASS L 7 BRIGHT; + VASS M 6 BRIGHT A_Fall; + VASS NO 6 BRIGHT; + VASS P 7 BRIGHT; + VASS Q -1 A_BossDeath; + Stop; + Raise: + VASS P 8; + VASS ONMLKJ 8; + Goto See; + } +} + +class ID24VassagoFlame : Actor +{ + Default + { + Damage 5; + Speed 15; + FastSpeed 20; + SplashGroup 1; + Radius 6; + Height 16; + Projectile; + +ZDOOMTRANS + RenderStyle "Add"; + SeeSound "monsters/vassago/burn"; + DeathSound "monsters/vassago/shotx"; + } + States + { + Spawn: + VFLM AB 4 BRIGHT; + Loop; + Death: + VFLM C 0 BRIGHT { bNoGravity = false; } + VFLM C 0 BRIGHT A_ChangeLinkFlags(0, 0); + VFLM C 0 BRIGHT A_Jump(128, "DeathSound2"); + VFLM C 0 BRIGHT A_StartSound("monsters/vassago/hot1"); + goto Burninate; + DeathSound2: + VFLM C 0 BRIGHT A_StartSound("monsters/vassago/hot2"); + Goto Burninate; + Burninate: + VFLM C 4 BRIGHT A_RadiusDamage(10, 128); + VFLM D 4 BRIGHT; + VFLM E 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot3"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot2"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot3"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot1"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot2"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot1"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM F 0 BRIGHT A_StartSound("monsters/vassago/hot2"); + VFLM F 4 BRIGHT A_RadiusDamage(10, 128); + VFLM G 4 BRIGHT; + VFLM H 4 BRIGHT; + VFLM I 0 BRIGHT A_StartSound("monsters/vassago/hot3"); + VFLM I 4 BRIGHT A_RadiusDamage(10, 128); + VFLM J 4 BRIGHT; + VFLM K 4 BRIGHT; + VFLM L 4 BRIGHT A_RadiusDamage(10, 128); + VFLM MNOPQ 4 BRIGHT; + Stop; + } +} From fc7e4c768e4fbc3c81f50162dc8fa9d1a2aae193 Mon Sep 17 00:00:00 2001 From: Owlet7 Date: Sun, 6 Apr 2025 09:31:50 +0300 Subject: [PATCH 152/384] Use correct ID24 actors ednums --- wadsrc/static/mapinfo/doomitems.txt | 101 +++++++++--------- .../zscript/actors/doom/id24/id24tech.zs | 4 - 2 files changed, 50 insertions(+), 55 deletions(-) diff --git a/wadsrc/static/mapinfo/doomitems.txt b/wadsrc/static/mapinfo/doomitems.txt index aa4fff87e..c754ada77 100644 --- a/wadsrc/static/mapinfo/doomitems.txt +++ b/wadsrc/static/mapinfo/doomitems.txt @@ -119,57 +119,6 @@ DoomEdNums 3004 = Zombieman 3005 = Cacodemon 3006 = LostSoul - 3007 = ID24Ghoul - 3008 = ID24Banshee - 3009 = ID24Mindweaver - 3010 = ID24PlasmaGuy - 3011 = ID24Vassago - 3012 = ID24Tyrant - 3013 = ID24TyrantBoss1 - 3014 = ID24TyrantBoss2 - 3100 = ID24GrayStalagmite - 3101 = ID24LargeCorpsePile - 3102 = ID24HumanBBQ1 - 3103 = ID24HumanBBQ2 - 3104 = ID24HangingBodyBothLegs - 3105 = ID24HangingBodyBothLegsSolid - 3106 = ID24HangingBodyCrucified - 3107 = ID24HangingBodyCrucifiedSolid - 3108 = ID24HangingBodyArmsBound - 3109 = ID24HangingBodyArmsBoundSolid - 3110 = ID24HangingBaronOfHell - 3111 = ID24HangingBaronOfHellSolid - 3112 = ID24HangingChainedBody - 3113 = ID24HangingChainedBodySolid - 3114 = ID24HangingChainedTorso - 3115 = ID24HangingChainedTorsoSolid - 3116 = ID24SkullPoleTrio - 3117 = ID24SkullGibs - 3118 = ID24BushShort - 3119 = ID24BushShortBurned1 - 3120 = ID24BushShortBurned2 - 3121 = ID24BushTall - 3122 = ID24BushTallBurned1 - 3123 = ID24BushTallBurned2 - 3124 = ID24CaveRockColumn - 3125 = ID24CaveStalagmiteLarge - 3126 = ID24CaveStalagmiteMedium - 3127 = ID24CaveStalagmiteSmall - 3128 = ID24CaveStalactiteLarge - 3129 = ID24CaveStalactiteLargeSolid - 3130 = ID24CaveStalactiteMedium - 3131 = ID24CaveStalactiteMediumSolid - 3132 = ID24CaveStalactiteSmall - 3133 = ID24CaveStalactiteSmallSolid - 3134 = ID24OfficeChair - 3135 = ID24OfficeLamp - 3136 = ID24LiveWire - 3137 = ID24CeilingLamp - 3138 = ID24CandelabraShort - 3139 = ID24AmbientKlaxon - 3140 = ID24AmbientPortalOpen - 3141 = ID24AmbientPortalLoop - 3142 = ID24AmbientPortalClose 4001 = "$Player5Start" 4002 = "$Player6Start" 4003 = "$Player7Start" @@ -200,6 +149,56 @@ DoomEdNums 9109 = MarinePlasma 9110 = MarineRailgun 9111 = MarineBFG + -28672 = ID24Ghoul + -28671 = ID24Banshee + -28670 = ID24Mindweaver + -28669 = ID24PlasmaGuy + -28668 = ID24Vassago + -28667 = ID24Tyrant + -28666 = ID24TyrantBoss1 + -28665 = ID24TyrantBoss2 + -28664 = ID24GrayStalagmite + -28663 = ID24LargeCorpsePile + -28662 = ID24HumanBBQ1 + -28661 = ID24HumanBBQ2 + -28660 = ID24HangingBodyBothLegs + -28659 = ID24HangingBodyBothLegsSolid + -28658 = ID24HangingBodyCrucified + -28657 = ID24HangingBodyCrucifiedSolid + -28656 = ID24HangingBodyArmsBound + -28655 = ID24HangingBodyArmsBoundSolid + -28654 = ID24HangingBaronOfHell + -28653 = ID24HangingBaronOfHellSolid + -28652 = ID24HangingChainedBody + -28651 = ID24HangingChainedBodySolid + -28650 = ID24HangingChainedTorso + -28649 = ID24HangingChainedTorsoSolid + -28648 = ID24SkullPoleTrio + -28647 = ID24SkullGibs + -28646 = ID24BushShort + -28645 = ID24BushShortBurned1 + -28644 = ID24BushShortBurned2 + -28643 = ID24BushTall + -28642 = ID24BushTallBurned1 + -28641 = ID24BushTallBurned2 + -28640 = ID24CaveRockColumn + -28639 = ID24CaveStalagmiteLarge + -28638 = ID24CaveStalagmiteMedium + -28637 = ID24CaveStalagmiteSmall + -28636 = ID24CaveStalactiteLarge + -28635 = ID24CaveStalactiteLargeSolid + -28634 = ID24CaveStalactiteMedium + -28633 = ID24CaveStalactiteMediumSolid + -28632 = ID24CaveStalactiteSmall + -28631 = ID24CaveStalactiteSmallSolid + -28630 = ID24OfficeChair + -28629 = ID24OfficeLamp + -28628 = ID24CeilingLamp + -28627 = ID24CandelabraShort + -28626 = ID24AmbientKlaxon + -28625 = ID24AmbientPortalOpen + -28624 = ID24AmbientPortalLoop + -28623 = ID24AmbientPortalClose -28622 = ID24Fuel -28621 = ID24FuelTank -28620 = ID24CalamityBlade diff --git a/wadsrc/static/zscript/actors/doom/id24/id24tech.zs b/wadsrc/static/zscript/actors/doom/id24/id24tech.zs index b886e097e..c68fcc729 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24tech.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24tech.zs @@ -63,10 +63,6 @@ class ID24OfficeLamp : Actor } } -// this actor has an editor number in the decohack, -// but it doesn't actually exist in it -class ID24LiveWire : Actor { } - class ID24CeilingLamp : Actor { Default From eb22547fc9d2fbc652d68dd7a339072271ab3e4e Mon Sep 17 00:00:00 2001 From: TheSuperDave938 <71569097+superdave938@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:50:40 -0400 Subject: [PATCH 153/384] ID24 Weapon Lights and Decals --- wadsrc/static/decaldef.txt | 35 +++++++++++++ .../actors/doom/id24/id24calamityblade.zs | 1 + .../actors/doom/id24/id24incinerator.zs | 1 + .../static/filter/doom.id/gldefs.txt | 49 +++++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/wadsrc/static/decaldef.txt b/wadsrc/static/decaldef.txt index ea67cc86e..99618cd08 100644 --- a/wadsrc/static/decaldef.txt +++ b/wadsrc/static/decaldef.txt @@ -593,6 +593,41 @@ decalgroup RedPlasmaScorch RedPlasmaScorch2 1 } +//ID24 Weapons +object ID24IncineratorProjectile +{ + frame HETBA { light ROCKET } + frame HETBB { light ROCKET } + frame HETBC { light ROCKET } + + frame HETBD { light ROCKET_X1 } + frame HETBE { light ROCKET_X1 } + frame HETBF { light ROCKET_X2 } + frame HETBG { light ROCKET_X2 } + frame HETBH { light ROCKET_X3 } + frame HETBI { light ROCKET_X3 } +} + +object ID24IncineratorFlame +{ + frame IFLMA { light ARCHFIRE1 } + frame IFLMB { light ARCHFIRE2 } + frame IFLMC { light ARCHFIRE3 } + frame IFLMD { light ARCHFIRE4 } + frame IFLME { light ARCHFIRE5 } + frame IFLMF { light ARCHFIRE6 } + frame IFLMG { light ARCHFIRE7 } + frame IFLMH { light ARCHFIRE8 } + + frame IFLMI { light ARCHFIRE1 } + frame IFLMJ { light ARCHFIRE2 } + frame IFLMK { light ARCHFIRE3 } + frame IFLML { light ARCHFIRE4 } + frame IFLMM { light ARCHFIRE5 } + frame IFLMN { light ARCHFIRE6 } + frame IFLMO { light ARCHFIRE7 } + frame IFLMP { light ARCHFIRE8 } +} // Graf Zahl provided definitions for the other games. diff --git a/wadsrc/static/zscript/actors/doom/id24/id24calamityblade.zs b/wadsrc/static/zscript/actors/doom/id24/id24calamityblade.zs index 62e6a65f0..6dec94d8d 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24calamityblade.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24calamityblade.zs @@ -196,6 +196,7 @@ class ID24IncineratorProjectile : Actor // Heatwave Ripper +ZDOOMTRANS; +RIPPER; RenderStyle "Add"; + Decal "BladeScorch"; DeathSound "weapons/calamityblade/explode"; } diff --git a/wadsrc/static/zscript/actors/doom/id24/id24incinerator.zs b/wadsrc/static/zscript/actors/doom/id24/id24incinerator.zs index a55ffb83e..3d954a592 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24incinerator.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24incinerator.zs @@ -77,6 +77,7 @@ class ID24IncineratorFlame : Actor // Incinerator Flame +ZDOOMTRANS; +FORCERADIUSDMG; RenderStyle "Add"; + Decal "Scorch"; } States diff --git a/wadsrc_lights/static/filter/doom.id/gldefs.txt b/wadsrc_lights/static/filter/doom.id/gldefs.txt index 5bb91c32f..11b7eccd2 100644 --- a/wadsrc_lights/static/filter/doom.id/gldefs.txt +++ b/wadsrc_lights/static/filter/doom.id/gldefs.txt @@ -1382,3 +1382,52 @@ object TeleportFog frame TFOGI { light DTFOG4 } frame TFOGJ { light DTFOG3 } } + +// ---------- +// -- ID24 -- +// ---------- + +//Calamity Blade +decal BladeScorchLower +{ + pic SCORCH1 + shade "00 00 00" + x-scale 0.85 + y-scale 0.20 + randomflipx + randomflipy +} + +decal BladeScorch1 +{ + pic HETBA0 + add 1.0 + fullbright + animator GoAway + lowerdecal BladeScorchLower +} + +decal BladeScorch2 +{ + pic HETBB0 + add 1.0 + fullbright + animator GoAway + lowerdecal BladeScorchLower +} + +decal BladeScorch3 +{ + pic HETBC0 + add 1.0 + fullbright + animator GoAway + lowerdecal BladeScorchLower +} + +decalgroup BladeScorch +{ + BladeScorch1 1 + BladeScorch2 1 + BladeScorch3 1 +} \ No newline at end of file From d3ecb5b86ca5248ad5d596548c1315369990f86d Mon Sep 17 00:00:00 2001 From: Owlet7 Date: Wed, 7 May 2025 00:55:54 +0300 Subject: [PATCH 154/384] Use old SNDINFO syntax for ID24 sounds I made the mistake of mixing the two different syntaxes in my last PR. Here's a fix. --- .../static/filter/game-doomchex/sndinfo.txt | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/wadsrc/static/filter/game-doomchex/sndinfo.txt b/wadsrc/static/filter/game-doomchex/sndinfo.txt index 84b6e68f7..4ecddbe6b 100644 --- a/wadsrc/static/filter/game-doomchex/sndinfo.txt +++ b/wadsrc/static/filter/game-doomchex/sndinfo.txt @@ -460,60 +460,60 @@ $alias intermission/pastdmstats *gibbed // id24 sounds -world/officelamp = DSBREAK +world/officelamp dsbreak -ambient/klaxon = DSKLAXON -ambient/portalopen = DSGATOPN -ambient/portalloop = DSGATLOP -ambient/portalclose = DSGATCLS +ambient/klaxon dsklaxon +ambient/portalopen dsgatopn +ambient/portalloop dsgatlop +ambient/portalclose dsgatcls -monsters/ghoul/sight = DSGHLSIT -monsters/ghoul/pain = DSGHLPAI -monsters/ghoul/death = DSGHLDTH -monsters/ghoul/active = DSGHLACT -monsters/ghoul/attack = DSFIRSHT -monsters/ghoul/shotx = DSFIRXPL +monsters/ghoul/sight dsghlsit +monsters/ghoul/pain dsghlpai +monsters/ghoul/death dsghldth +monsters/ghoul/active dsghlact +monsters/ghoul/attack dsfirsht +monsters/ghoul/shotx dsfirxpl -monsters/banshee/sight = DSBANACT -monsters/banshee/pain = DSBANPAI -monsters/banshee/death = DSBANDTH -monsters/banshee/active = DSBANACT +monsters/banshee/sight dsbanact +monsters/banshee/pain dsbanpai +monsters/banshee/death dsbandth +monsters/banshee/active dsbanact -monsters/mindweaver/sight = DSCSPSIT -monsters/mindweaver/pain = DSDMPAIN -monsters/mindweaver/death = DSCSPDTH -monsters/mindweaver/active = DSCSPACT -monsters/mindweaver/walk = DSCSPWLK +monsters/mindweaver/sight dscspsit +monsters/mindweaver/pain dsdmpain +monsters/mindweaver/death dscspdth +monsters/mindweaver/active dscspact +monsters/mindweaver/walk dscspwlk -monsters/plasmaguy/pain = DSPPOPAI -monsters/plasmaguy/death = DSPPODTH -monsters/plasmaguy/active = DSPPOACT -monsters/plasmaguy/head = DSPPOHED +monsters/plasmaguy/pain dsppopai +monsters/plasmaguy/death dsppodth +monsters/plasmaguy/active dsppoact +monsters/plasmaguy/head dsppohed -monsters/vassago/sight = DSVASSIT -monsters/vassago/pain = DSVASPAI -monsters/vassago/death = DSVASDTH -monsters/vassago/active = DSVASACT -monsters/vassago/attack = DSVASATK -monsters/vassago/burn = DSINCBRN -monsters/vassago/shotx = DSFLAME -monsters/vassago/hot1 = DSINCHT1 -monsters/vassago/hot2 = DSINCHT2 -monsters/vassago/hot3 = DSINCHT3 +monsters/vassago/sight dsvassit +monsters/vassago/pain dsvaspai +monsters/vassago/death dsvasdth +monsters/vassago/active dsvasact +monsters/vassago/attack dsvasatk +monsters/vassago/burn dsincbrn +monsters/vassago/shotx dsflame +monsters/vassago/hot1 dsincht1 +monsters/vassago/hot2 dsincht2 +monsters/vassago/hot3 dsincht3 -monsters/tyrant/sight = DSTYRSIT -monsters/tyrant/pain = DSTYRPAI -monsters/tyrant/death = DSTYRDTH -monsters/tyrant/active = DSTYRACT -monsters/tyrant/walk = DSTYRWLK +monsters/tyrant/sight dstyrsit +monsters/tyrant/pain dstyrpai +monsters/tyrant/death dstyrdth +monsters/tyrant/active dstyract +monsters/tyrant/walk dstyrwlk -weapons/incinerator/fire1 DSINCFI1 -weapons/incinerator/fire2 DSINCFI2 -weapons/incinerator/burn DSINCBRN -weapons/incinerator/hot1 DSINCHT1 -weapons/incinerator/hot2 DSINCHT2 -weapons/incinerator/hot3 DSINCHT3 +weapons/incinerator/fire1 dsincfi1 +weapons/incinerator/fire2 dsincfi2 +weapons/incinerator/burn dsincbrn +weapons/incinerator/hot1 dsincht1 +weapons/incinerator/hot2 dsincht2 +weapons/incinerator/hot3 dsincht3 -weapons/calamityblade/charge DSHETCHG -weapons/calamityblade/shoot DSHETSHT -weapons/calamityblade/explode DSHETXPL +weapons/calamityblade/charge dshetchg +weapons/calamityblade/shoot dshetsht +weapons/calamityblade/explode dshetxpl From 6564fa04e1b59d3a19063fa30f1b13c4c7a9c350 Mon Sep 17 00:00:00 2001 From: Owlet7 Date: Tue, 6 May 2025 18:03:36 +0300 Subject: [PATCH 155/384] Added obituaries to ID24 monsters Renamed the language identifiers of the ID24 monsters to match the ones used by Legacy of Rust in DOOM + DOOM II. --- wadsrc/static/zscript/actors/doom/id24/id24banshee.zs | 3 ++- wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs | 3 ++- wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs | 3 ++- wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs | 3 ++- wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs | 3 ++- wadsrc/static/zscript/actors/doom/id24/id24vassago.zs | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs b/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs index cf8083d8e..494737243 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24banshee.zs @@ -35,7 +35,8 @@ class ID24Banshee : Actor SeeSound "monsters/banshee/sight"; PainSound "monsters/banshee/pain"; DeathSound "monsters/banshee/death"; - Tag "$FN_ID24BANSHEE"; + // SelfObituary "$ID24_OB_BANSHEE"; + Tag "$ID24_CC_BANSHEE"; } States { diff --git a/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs b/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs index fa07b84b9..01788b3fe 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24ghoul.zs @@ -36,7 +36,8 @@ class ID24Ghoul : Actor PainSound "monsters/ghoul/pain"; DeathSound "monsters/ghoul/death"; ActiveSound "monsters/ghoul/active"; - Tag "$FN_ID24GHOUL"; + Obituary "$ID24_OB_GHOUL"; + Tag "$ID24_CC_GHOUL"; } States { diff --git a/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs b/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs index caf74fcbc..71e2480ce 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24mindweaver.zs @@ -34,7 +34,8 @@ class ID24Mindweaver : Actor PainSound "monsters/mindweaver/pain"; DeathSound "monsters/mindweaver/death"; ActiveSound "monsters/mindweaver/active"; - Tag "$FN_ID24MINDWEAVER"; + Obituary "$ID24_OB_MINDWEAVER"; + Tag "$ID24_CC_MINDWEAVER"; } States { diff --git a/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs b/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs index 243ca0ccd..fa5553e86 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24plasmaguy.zs @@ -33,7 +33,8 @@ class ID24PlasmaGuy : Actor PainSound "monsters/plasmaguy/pain"; DeathSound "monsters/plasmaguy/death"; ActiveSound "monsters/plasmaguy/active"; - Tag "$FN_ID24PLASMAGUY"; + Obituary "$ID24_OB_SHOCKTROOPER"; + Tag "$ID24_CC_SHOCKTROOPER"; } States { diff --git a/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs b/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs index d7e30e181..ea8bb9ca1 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24tyrant.zs @@ -31,7 +31,8 @@ class ID24Tyrant : Cyberdemon PainSound "monsters/tyrant/pain"; DeathSound "monsters/tyrant/death"; ActiveSound "monsters/tyrant/active"; - Tag "$FN_ID24TYRANT"; + Obituary "$ID24_OB_TYRANT"; + Tag "$ID24_CC_TYRANT"; } States { diff --git a/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs b/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs index ad8ab9ec9..a6fe03507 100644 --- a/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs +++ b/wadsrc/static/zscript/actors/doom/id24/id24vassago.zs @@ -35,7 +35,8 @@ class ID24Vassago : Actor PainSound "monsters/vassago/pain"; DeathSound "monsters/vassago/death"; ActiveSound "monsters/vassago/active"; - Tag "$FN_ID24VASSAGO"; + Obituary "$ID24_OB_VASSAGO"; + Tag "$ID24_CC_VASSAGO"; } States { From 9eaf47207198d375e1719f6d093dadf3f7281c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Tue, 6 May 2025 22:08:26 -0300 Subject: [PATCH 156/384] fix gldefs and decaldef being inverted --- wadsrc/static/decaldef.txt | 81 +++++++++++-------- .../static/filter/doom.id/gldefs.txt | 68 +++++++--------- 2 files changed, 75 insertions(+), 74 deletions(-) diff --git a/wadsrc/static/decaldef.txt b/wadsrc/static/decaldef.txt index 99618cd08..0efbd1e5d 100644 --- a/wadsrc/static/decaldef.txt +++ b/wadsrc/static/decaldef.txt @@ -593,41 +593,6 @@ decalgroup RedPlasmaScorch RedPlasmaScorch2 1 } -//ID24 Weapons -object ID24IncineratorProjectile -{ - frame HETBA { light ROCKET } - frame HETBB { light ROCKET } - frame HETBC { light ROCKET } - - frame HETBD { light ROCKET_X1 } - frame HETBE { light ROCKET_X1 } - frame HETBF { light ROCKET_X2 } - frame HETBG { light ROCKET_X2 } - frame HETBH { light ROCKET_X3 } - frame HETBI { light ROCKET_X3 } -} - -object ID24IncineratorFlame -{ - frame IFLMA { light ARCHFIRE1 } - frame IFLMB { light ARCHFIRE2 } - frame IFLMC { light ARCHFIRE3 } - frame IFLMD { light ARCHFIRE4 } - frame IFLME { light ARCHFIRE5 } - frame IFLMF { light ARCHFIRE6 } - frame IFLMG { light ARCHFIRE7 } - frame IFLMH { light ARCHFIRE8 } - - frame IFLMI { light ARCHFIRE1 } - frame IFLMJ { light ARCHFIRE2 } - frame IFLMK { light ARCHFIRE3 } - frame IFLML { light ARCHFIRE4 } - frame IFLMM { light ARCHFIRE5 } - frame IFLMN { light ARCHFIRE6 } - frame IFLMO { light ARCHFIRE7 } - frame IFLMP { light ARCHFIRE8 } -} // Graf Zahl provided definitions for the other games. @@ -1070,6 +1035,52 @@ decalgroup SwordLightning SwordLightning2 1 } +/***** ID24 ****************************************************************/ + +//Calamity Blade +decal BladeScorchLower +{ + pic SCORCH1 + shade "00 00 00" + x-scale 0.85 + y-scale 0.20 + randomflipx + randomflipy +} + +decal BladeScorch1 +{ + pic HETBA0 + add 1.0 + fullbright + animator GoAway + lowerdecal BladeScorchLower +} + +decal BladeScorch2 +{ + pic HETBB0 + add 1.0 + fullbright + animator GoAway + lowerdecal BladeScorchLower +} + +decal BladeScorch3 +{ + pic HETBC0 + add 1.0 + fullbright + animator GoAway + lowerdecal BladeScorchLower +} + +decalgroup BladeScorch +{ + BladeScorch1 1 + BladeScorch2 1 + BladeScorch3 1 +} /***** Generators **********************************************************/ diff --git a/wadsrc_lights/static/filter/doom.id/gldefs.txt b/wadsrc_lights/static/filter/doom.id/gldefs.txt index 11b7eccd2..253d4ebf8 100644 --- a/wadsrc_lights/static/filter/doom.id/gldefs.txt +++ b/wadsrc_lights/static/filter/doom.id/gldefs.txt @@ -1387,47 +1387,37 @@ object TeleportFog // -- ID24 -- // ---------- -//Calamity Blade -decal BladeScorchLower +object ID24IncineratorProjectile { - pic SCORCH1 - shade "00 00 00" - x-scale 0.85 - y-scale 0.20 - randomflipx - randomflipy + frame HETBA { light ROCKET } + frame HETBB { light ROCKET } + frame HETBC { light ROCKET } + + frame HETBD { light ROCKET_X1 } + frame HETBE { light ROCKET_X1 } + frame HETBF { light ROCKET_X2 } + frame HETBG { light ROCKET_X2 } + frame HETBH { light ROCKET_X3 } + frame HETBI { light ROCKET_X3 } } -decal BladeScorch1 +object ID24IncineratorFlame { - pic HETBA0 - add 1.0 - fullbright - animator GoAway - lowerdecal BladeScorchLower + frame IFLMA { light ARCHFIRE1 } + frame IFLMB { light ARCHFIRE2 } + frame IFLMC { light ARCHFIRE3 } + frame IFLMD { light ARCHFIRE4 } + frame IFLME { light ARCHFIRE5 } + frame IFLMF { light ARCHFIRE6 } + frame IFLMG { light ARCHFIRE7 } + frame IFLMH { light ARCHFIRE8 } + + frame IFLMI { light ARCHFIRE1 } + frame IFLMJ { light ARCHFIRE2 } + frame IFLMK { light ARCHFIRE3 } + frame IFLML { light ARCHFIRE4 } + frame IFLMM { light ARCHFIRE5 } + frame IFLMN { light ARCHFIRE6 } + frame IFLMO { light ARCHFIRE7 } + frame IFLMP { light ARCHFIRE8 } } - -decal BladeScorch2 -{ - pic HETBB0 - add 1.0 - fullbright - animator GoAway - lowerdecal BladeScorchLower -} - -decal BladeScorch3 -{ - pic HETBC0 - add 1.0 - fullbright - animator GoAway - lowerdecal BladeScorchLower -} - -decalgroup BladeScorch -{ - BladeScorch1 1 - BladeScorch2 1 - BladeScorch3 1 -} \ No newline at end of file From 9e2b1f9c4ce3cce2fbbf39708542f224519016d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 9 May 2025 17:06:16 -0300 Subject: [PATCH 157/384] Bone Getters Part 2/3, plus fixed warnings for MSVC * add getters for frame poses * fix missing joint in GetJointPose * clean up models_iqm.cpp * clean up usage of I_GetTimeFrac, split out matrix calculation into its own function * clean up SetModelBoneRotationInternal * clean up a few float <-> double and unsigned <-> signed warnings * fix more warnings * further clean up warnings * split mode ObjectToWorldMatrix stuff * initial work on bone getters, matrix hell (the matrix/vec3 multiplications are probably wrong af, just gotta add more stuff 'till i can test it) * clean up matrix math * GetBone/TransformByBone * fix GetBoneFramePose * fix ObjectToWorldMatrix * fix missing array resize * raw matrix getters (for use with gutamatics/etc) * reverse matrix mult order * replace GetBoneLength/GetBoneDir with GetBoneBaseTRS * fix GetBonePosition, remove GetBoneWorldMatrix as it's useless * GetBonePosition * deduplicate code * rename GetBonePosition to GetBoneBasePosition to avoid confusion * GetBoneBaseRotation * GetBonePosition helper function * forgot include_offsets --- src/common/2d/wipe.cpp | 2 +- src/common/engine/i_net.cpp | 62 +- src/common/engine/i_net.h | 2 +- src/common/filesystem/include/resourcefile.h | 1 + src/common/models/bonecomponents.h | 7 +- src/common/models/model.h | 12 +- src/common/models/model_iqm.h | 33 +- src/common/models/models_iqm.cpp | 63 +- src/common/objects/dobjtype.cpp | 6 +- src/common/rendering/r_videoscale.cpp | 6 +- src/common/textures/m_png.cpp | 50 +- src/common/utility/matrix.h | 2 +- src/common/utility/quaternion.h | 25 +- src/common/utility/vectors.h | 5 + src/common/widgets/netstartwindow.cpp | 2 +- src/d_main.cpp | 7 +- src/d_net.cpp | 26 +- src/events.cpp | 15 +- src/g_level.cpp | 2 +- src/gamedata/r_defs.h | 2 +- src/gamedata/textures/animations.cpp | 2 +- src/gamedata/textures/buildloader.cpp | 7 +- src/launcher/settingspage.cpp | 2 +- src/p_openmap.cpp | 2 +- src/playsim/actor.h | 25 +- src/playsim/p_actionfunctions.cpp | 637 ++++++++++++++++--- src/playsim/p_enemy.cpp | 2 - src/playsim/p_mobj.cpp | 127 ++++ src/playsim/p_user.cpp | 2 +- src/r_data/models.cpp | 182 +++--- src/r_data/models.h | 4 +- src/rendering/hwrenderer/scene/hw_weapon.cpp | 2 +- src/rendering/r_utility.cpp | 2 +- src/win32/i_steam.cpp | 2 +- tools/lemon/lemon.c | 6 +- wadsrc/static/zscript/actors/actor.zs | 73 ++- wadsrc/static/zscript/constants.zs | 7 + 37 files changed, 1131 insertions(+), 283 deletions(-) diff --git a/src/common/2d/wipe.cpp b/src/common/2d/wipe.cpp index c829f7a8d..a729a933c 100644 --- a/src/common/2d/wipe.cpp +++ b/src/common/2d/wipe.cpp @@ -289,7 +289,7 @@ bool Wiper_Crossfade::Run(int ticks) bool Wiper_Crossfade::RunInterpolated(double ticks) { - Clock += ticks; + Clock += float(ticks); DrawTexture(twod, startScreen, 0, 0, DTA_FlipY, screen->RenderTextureIsFlipped(), DTA_Masked, false, TAG_DONE); DrawTexture(twod, endScreen, 0, 0, DTA_FlipY, screen->RenderTextureIsFlipped(), DTA_Masked, false, DTA_Alpha, clamp(Clock / 32.f, 0.f, 1.f), TAG_DONE); return Clock >= 32.; diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 3bddcbeff..7d9a9674f 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -164,7 +164,7 @@ FClientStack NetworkClients = {}; uint32_t GameID = DEFAULT_GAME_ID; uint8_t TicDup = 1u; -uint8_t MaxClients = 1u; +int MaxClients = 1; int RemoteClient = -1; size_t NetBufferLength = 0u; uint8_t NetBuffer[MAX_MSGLEN] = {}; @@ -187,10 +187,10 @@ CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE) void Net_SetupUserInfo(); const char* Net_GetClientName(int client, unsigned int charLimit); -int Net_SetUserInfo(int client, uint8_t*& stream); -int Net_ReadUserInfo(int client, uint8_t*& stream); -int Net_ReadGameInfo(uint8_t*& stream); -int Net_SetGameInfo(uint8_t*& stream); +size_t Net_SetUserInfo(int client, uint8_t*& stream); +size_t Net_ReadUserInfo(int client, uint8_t*& stream); +size_t Net_ReadGameInfo(uint8_t*& stream); +size_t Net_SetGameInfo(uint8_t*& stream); static SOCKET CreateUDPSocket() { @@ -320,7 +320,7 @@ static int PrivateNetOf(const in_addr& in) // private network, since that means we (probably) are too. static bool ClientsOnSameNetwork() { - size_t start = 1u; + int start = 1; for (; start < MaxClients; ++start) { if (Connected[start].Status != CSTAT_NONE) @@ -334,7 +334,7 @@ static bool ClientsOnSameNetwork() if (!firstClient) return false; - for (size_t i = 1u; i < MaxClients; ++i) + for (int i = 1; i < MaxClients; ++i) { if (i == start) continue; @@ -395,7 +395,7 @@ static bool I_NetLoop(bool (*loopCallback)(void*), void* data) } // A new client has just entered the game, so add them to the player list. -static void I_NetClientConnected(size_t client, unsigned int charLimit = 0u) +static void I_NetClientConnected(int client, unsigned int charLimit = 0u) { const char* name = Net_GetClientName(client, charLimit); unsigned int flags = CFL_NONE; @@ -408,17 +408,17 @@ static void I_NetClientConnected(size_t client, unsigned int charLimit = 0u) } // A client changed ready state. -static void I_NetClientUpdated(size_t client) +static void I_NetClientUpdated(int client) { StartWindow->NetUpdate(client, Connected[client].Status); } -static void I_NetClientDisconnected(size_t client) +static void I_NetClientDisconnected(int client) { StartWindow->NetDisconnect(client); } -static void I_NetUpdatePlayers(size_t current, size_t limit) +static void I_NetUpdatePlayers(int current, int limit) { StartWindow->NetProgress(current, limit); } @@ -488,16 +488,17 @@ static void SendPacket(const sockaddr_in& to) if (NetBufferLength >= MinCompressionSize) { TransmitBuffer[0] = NetBuffer[0] | NCMD_COMPRESSED; - res = compress2(TransmitBuffer + 1, &size, NetBuffer + 1, NetBufferLength - 1u, 9); + + res = compress2(TransmitBuffer + 1, &size, NetBuffer + 1, uint32_t(NetBufferLength - 1u), 9); ++size; } if (res == Z_OK && size < static_cast(NetBufferLength)) - res = sendto(MySocket, (const char*)TransmitBuffer, size, 0, (const sockaddr*)&to, sizeof(to)); + res = sendto(MySocket, (const char*)TransmitBuffer, (int)size, 0, (const sockaddr*)&to, sizeof(to)); else if (NetBufferLength > MaxTransmitSize) I_Error("Net compression failed (zlib error %d)", res); else - res = sendto(MySocket, (const char*)NetBuffer, NetBufferLength, 0, (const sockaddr*)&to, sizeof(to)); + res = sendto(MySocket, (const char*)NetBuffer, (int)NetBufferLength, 0, (const sockaddr*)&to, sizeof(to)); } static void GetPacket(sockaddr_in* const from = nullptr) @@ -632,7 +633,7 @@ static void RejectConnection(const sockaddr_in& to, ENetConnectType reason) SendPacket(to); } -static void AddClientConnection(const sockaddr_in& from, size_t client) +static void AddClientConnection(const sockaddr_in& from, int client) { Connected[client].Status = CSTAT_CONNECTING; Connected[client].Address = from; @@ -641,7 +642,7 @@ static void AddClientConnection(const sockaddr_in& from, size_t client) I_NetClientUpdated(client); // Make sure any ready clients are marked as needing the new client's info. - for (size_t i = 1u; i < MaxClients; ++i) + for (int i = 1; i < MaxClients; ++i) { if (Connected[i].Status == CSTAT_READY) { @@ -651,7 +652,7 @@ static void AddClientConnection(const sockaddr_in& from, size_t client) } } -static void RemoveClientConnection(size_t client) +static void RemoveClientConnection(int client) { I_NetClientDisconnected(client); I_ClearClient(client); @@ -664,7 +665,7 @@ static void RemoveClientConnection(size_t client) NetBuffer[2] = client; NetBufferLength = 3u; - for (size_t i = 1u; i < MaxClients; ++i) + for (int i = 1; i < MaxClients; ++i) { if (Connected[i].Status == CSTAT_NONE) continue; @@ -694,7 +695,7 @@ static bool Host_CheckForConnections(void* connected) { const bool forceStarting = I_ShouldStartNetGame(); const bool hasPassword = strlen(net_password) > 0; - size_t* connectedPlayers = (size_t*)connected; + int* connectedPlayers = (int*)connected; TArray toBoot = {}; I_GetKickClients(toBoot); @@ -780,7 +781,7 @@ static bool Host_CheckForConnections(void* connected) } else { - size_t free = 1u; + int free = 1; for (; free < MaxClients; ++free) { if (Connected[free].Status == CSTAT_NONE) @@ -815,7 +816,7 @@ static bool Host_CheckForConnections(void* connected) const size_t addrSize = sizeof(sockaddr_in); bool ready = true; NetBuffer[0] = NCMD_SETUP; - for (size_t client = 1u; client < MaxClients; ++client) + for (int client = 1; client < MaxClients; ++client) { auto& con = Connected[client]; // If we're starting before the lobby is full, only check against connected clients. @@ -855,7 +856,7 @@ static bool Host_CheckForConnections(void* connected) } NetBuffer[1] = PRE_USER_INFO; - for (size_t i = 0u; i < MaxClients; ++i) + for (int i = 0; i < MaxClients; ++i) { if (i == client || Connected[i].Status == CSTAT_NONE) continue; @@ -864,7 +865,7 @@ static bool Host_CheckForConnections(void* connected) { if (Connected[i].Status >= CSTAT_WAITING) { - NetBuffer[2] = i; + NetBuffer[2] = uint8_t(i); NetBufferLength = 3u; // Client will already have the host connection information. if (i > 0) @@ -908,7 +909,7 @@ static void SendAbort() if (consoleplayer == 0) { - for (size_t client = 1u; client < MaxClients; ++client) + for (int client = 1; client < MaxClients; ++client) { if (Connected[client].Status != CSTAT_NONE) SendPacket(Connected[client].Address); @@ -935,7 +936,7 @@ static bool HostGame(int arg, bool forcedNetMode) Net_SetupUserInfo(); // If only 1 player, don't bother starting the network - if (MaxClients == 1u) + if (MaxClients == 1) { TicDup = 1u; multiplayer = true; @@ -944,11 +945,11 @@ static bool HostGame(int arg, bool forcedNetMode) StartNetwork(false); I_NetInit("Waiting for other players...", true); - I_NetUpdatePlayers(1u, MaxClients); + I_NetUpdatePlayers(1, MaxClients); I_NetClientConnected(0u, 16u); // Wait for the lobby to be full. - size_t connectedPlayers = 1u; + int connectedPlayers = 1; if (!I_NetLoop(Host_CheckForConnections, (void*)&connectedPlayers)) { SendAbort(); @@ -961,10 +962,11 @@ static bool HostGame(int arg, bool forcedNetMode) // If the player force started with only themselves in the lobby, start the game // immediately. - if (connectedPlayers == 1u) + if (connectedPlayers == 1) { CloseNetwork(); - MaxClients = TicDup = 1u; + MaxClients = 1; + TicDup = 1u; return true; } @@ -1102,7 +1104,7 @@ static bool Guest_ContactHost(void* unused) } else if (NetBuffer[1] == PRE_USER_INFO) { - const size_t c = NetBuffer[2]; + const int c = NetBuffer[2]; if (!ClientGotAck(consoleplayer, c)) { NetworkClients += c; diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h index 7d9f40af0..64c781309 100644 --- a/src/common/engine/i_net.h +++ b/src/common/engine/i_net.h @@ -66,7 +66,7 @@ extern uint8_t NetBuffer[MAX_MSGLEN]; extern size_t NetBufferLength; extern uint8_t TicDup; extern int RemoteClient; -extern uint8_t MaxClients; +extern int MaxClients; extern uint32_t GameID; bool I_InitNetwork(); diff --git a/src/common/filesystem/include/resourcefile.h b/src/common/filesystem/include/resourcefile.h index a23914444..a51433eda 100644 --- a/src/common/filesystem/include/resourcefile.h +++ b/src/common/filesystem/include/resourcefile.h @@ -156,6 +156,7 @@ public: const char* GetHash() const { return Hash; } int EntryCount() const { return NumLumps; } + uint32_t EntryCountU() const { return NumLumps; } int FindEntry(const char* name); size_t Length(uint32_t entry) diff --git a/src/common/models/bonecomponents.h b/src/common/models/bonecomponents.h index db8957f8a..de0f74856 100644 --- a/src/common/models/bonecomponents.h +++ b/src/common/models/bonecomponents.h @@ -67,13 +67,13 @@ struct BoneOverrideComponent void Modify(T &value, double tic) const { - double lerp_amt = interplen > 0.0 ? std::clamp(((tic - switchtic) / interplen), 0.0, 1.0) : 1.0; + float lerp_amt = interplen > 0.0f ? std::clamp(float((tic - switchtic) / interplen), 0.0f, 1.0f) : 1.0f; if(mode > 0 || (prev_mode > 0 && lerp_amt < 1.0)) { T from = ModifyValue(value, prev, prev_mode); T to = ModifyValue(value, cur, mode); - value = Lerp(from, to, lerp_amt, 1.0 - lerp_amt); + value = Lerp(from, to, lerp_amt, 1.0f - lerp_amt); } } @@ -126,9 +126,10 @@ struct BoneOverride struct BoneInfo { - TArray bones_anim_only; + TArray bones; TArray bones_with_override; TArray positions; + TArray positions_with_override; }; struct ModelAnim diff --git a/src/common/models/model.h b/src/common/models/model.h index 23381bed1..3fa293073 100644 --- a/src/common/models/model.h +++ b/src/common/models/model.h @@ -17,7 +17,9 @@ class FGameTexture; class IModelVertexBuffer; class FModel; class PClass; +class AActor; struct FSpriteModelFrame; +struct FLevelLocals; FTextureID LoadSkin(const char* path, const char* fn); void FlushModels(); @@ -59,6 +61,9 @@ public: unsigned int getFlags(class DActorModelData * defs) const; friend void InitModels(); friend void ParseModelDefLump(int Lump); + + VSMatrix ObjectToWorldMatrix(AActor * actor, float x, float y, float z, double ticFrac); + VSMatrix ObjectToWorldMatrix(FLevelLocals *Level, DVector3 translation, DRotator rotation, DVector2 scaling, unsigned int flags, double tic); }; @@ -92,9 +97,12 @@ public: virtual int FindJoint(FName name) { return -1; } virtual int GetJointParent(int joint) { return -1; } - virtual double GetJointLength(int joint) { return 0.0; } virtual FName GetJointName(int joint) { return NAME_None; } - virtual FVector3 GetJointDir(int joint) { return FVector3(0.0f,0.0f,0.0f); } + virtual FQuaternion GetJointRotation(int joint) { return FQuaternion(0.0f,0.0f,0.0f,1.0f); } + virtual FVector3 GetJointPosition(int joint) { return FVector3(0.0f,0.0f,0.0f); } + virtual TRS GetJointBaseTRS(int joint) { return {}; } + virtual TRS GetJointPose(int joint, int frame) { return {}; } + virtual int NumFrames() { return -1; } virtual void GetJointChildren(int joint, TArray &out) {} diff --git a/src/common/models/model_iqm.h b/src/common/models/model_iqm.h index 9374e5f85..cddd9e70b 100644 --- a/src/common/models/model_iqm.h +++ b/src/common/models/model_iqm.h @@ -73,6 +73,9 @@ struct IQMJoint FVector3 Translate; FQuaternion Quaternion; FVector3 Scale; + FVector3 Position; + FQuaternion Rotation; + FVector3 Scaling; }; struct IQMPose @@ -164,7 +167,7 @@ private: TArray inversebaseframe; TArray TRSData; public: - int NumJoints() override { return Joints.Size(); } + int NumJoints() override { return Joints.SSize(); } int FindJoint(FName name) override { int *j = NamedJoints.CheckKey(name); @@ -174,12 +177,12 @@ public: int GetJointParent(int joint) override { - return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Parent : -1; + return (joint >= 0 && joint < Joints.SSize()) ? Joints[joint].Parent : -1; } FName GetJointName(int joint) override { - return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Name : FName(NAME_None); + return (joint >= 0 && joint < Joints.SSize()) ? Joints[joint].Name : FName(NAME_None); } void GetRootJoints(TArray &out) override @@ -189,20 +192,34 @@ public: void GetJointChildren(int joint, TArray &out) override { - if(joint >= 0 && joint < Joints.Size()) + if(joint >= 0 && joint < Joints.SSize()) { out = Joints[joint].Children; } } - double GetJointLength(int joint) override + FQuaternion GetJointRotation(int joint) override { - return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Translate.Length() : 0.0; + return (joint >= 0 && joint < Joints.SSize()) ? Joints[joint].Rotation : FQuaternion(0.0f,0.0f,0.0f,1.0f); } - FVector3 GetJointDir(int joint) override + FVector3 GetJointPosition(int joint) override { - return (joint >= 0 && joint < Joints.Size()) ? Joints[joint].Translate.Unit() : FVector3(0.0f,0.0f,0.0f); + return (joint >= 0 && joint < Joints.SSize()) ? Joints[joint].Position : FVector3(0.0f,0.0f,0.0f); + } + + TRS GetJointBaseTRS(int joint) override + { + return (joint >= 0 && joint < Joints.SSize()) ? TRS{Joints[joint].Translate, Joints[joint].Quaternion, Joints[joint].Scale} : TRS{}; + } + + TRS GetJointPose(int joint, int frame) override + { + return (joint >= 0 && joint < Joints.SSize() && frame >= 0 && ((frame * Joints.SSize()) + joint) < TRSData.SSize()) ? TRSData[(frame * Joints.SSize()) + joint] : TRS{} ; + } + virtual int NumFrames() + { + return Joints.SSize() > 0 ? (TRSData.SSize() / Joints.SSize()) : 0; } }; diff --git a/src/common/models/models_iqm.cpp b/src/common/models/models_iqm.cpp index 719f15062..488085590 100644 --- a/src/common/models/models_iqm.cpp +++ b/src/common/models/models_iqm.cpp @@ -109,7 +109,7 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt } reader.SeekTo(ofs_joints); - for (int i = 0; i < Joints.Size(); i++) + for (int i = 0; i < Joints.SSize(); i++) { IQMJoint& joint = Joints[i]; @@ -127,8 +127,6 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt joint.Translate.Y = reader.ReadFloat(); joint.Translate.Z = reader.ReadFloat(); - int len = joint.Translate.Length(); - joint.Quaternion.X = reader.ReadFloat(); joint.Quaternion.Y = reader.ReadFloat(); joint.Quaternion.Z = reader.ReadFloat(); @@ -140,14 +138,24 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt if(joint.Parent < 0) { + joint.Rotation = joint.Quaternion; + joint.Scaling = joint.Scale; + joint.Position = joint.Translate.ScaleXYZ(joint.Scaling); RootJoints.Push(i); } - else if(joint.Parent >= Joints.Size()) + else if(joint.Parent >= i) + { + I_FatalError("Joint child comes before parent in IQM Model"); + } + else if(joint.Parent >= Joints.SSize()) { I_FatalError("Joint parent index out of bounds in IQM Model"); } else { + joint.Rotation = (Joints[joint.Parent].Rotation * joint.Quaternion).Unit(); + joint.Scaling = joint.Scale.ScaleXYZ(Joints[joint.Parent].Scaling); + joint.Position = (Joints[joint.Parent].Rotation * joint.Translate.ScaleXYZ(joint.Scaling)) + Joints[joint.Parent].Position; Joints[joint.Parent].Children.Push(i); } } @@ -619,11 +627,6 @@ const TArray* IQMModel::CalculateBones(const ModelAnimFrame &from, con } } -inline void ModifyBone(const BoneOverride& mod, TRS &bone, double time) -{ - mod.Modify(bone, time); -} - // explicitly don't pass modelBoneOverrides when precalculating animation for interpolation, as it's applied _after_ animation ModelAnimFramePrecalculatedIQM IQMModel::CalculateFrameIQM(int frame1, int frame2, float inter, int frame1_prev, float inter1_prev, int frame2_prev, float inter2_prev, const ModelAnimFramePrecalculatedIQM* precalculated, const TArray* animationData) { @@ -683,15 +686,16 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa { const TArray& animationFrames = animationData ? *animationData : TRSData; - TArray* outMatrix = out ? &out->positions : &boneData; + TArray* outMatrix = out ? &out->positions_with_override : &boneData; int numbones = Joints.SSize(); outMatrix->Resize(numbones); if(out) { - out->bones_anim_only.Resize(numbones); + out->bones.Resize(numbones); out->bones_with_override.Resize(numbones); + out->positions.Resize(numbones); } if(in && in->size() != Joints.Size()) in = nullptr; @@ -702,11 +706,11 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa frame1 = clamp(frame1, 0, (animationFrames.SSize() - 1) / numbones); frame2 = clamp(frame2, 0, (animationFrames.SSize() - 1) / numbones); - int offset1 = frame1 * numbones; - int offset2 = frame2 * numbones; + unsigned int offset1 = frame1 * numbones; + unsigned int offset2 = frame2 * numbones; - int offset1_1 = frame1_prev * numbones; - int offset2_1 = frame2_prev * numbones; + unsigned int offset1_1 = frame1_prev * numbones; + unsigned int offset2_1 = frame2_prev * numbones; float invt = 1.0f - inter; float invt1 = 1.0f - inter1_prev; @@ -752,7 +756,7 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa if(out) { - out->bones_anim_only[i] = bone; + out->bones[i] = bone; if(in) { @@ -775,7 +779,6 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa VSMatrix& result = (*outMatrix)[i]; if (Joints[i].Parent >= 0) { - assert(Joints[i].Parent < i); result = (*outMatrix)[Joints[i].Parent]; result.multMatrix(swapYZ); result.multMatrix(baseframe[Joints[i].Parent]); @@ -789,6 +792,32 @@ const TArray* IQMModel::CalculateBonesIQM(int frame1, int frame2, floa result.multMatrix(inversebaseframe[i]); } result.multMatrix(swapYZ); + + if(out) + { + VSMatrix m; + m.loadIdentity(); + m.translate(out->bones[i].translation.X, out->bones[i].translation.Y, out->bones[i].translation.Z); + m.multQuaternion(out->bones[i].rotation); + m.scale(out->bones[i].scaling.X, out->bones[i].scaling.Y, out->bones[i].scaling.Z); + + VSMatrix& result = out->positions[i]; + if (Joints[i].Parent >= 0) + { + result = out->positions[Joints[i].Parent]; + result.multMatrix(swapYZ); + result.multMatrix(baseframe[Joints[i].Parent]); + result.multMatrix(m); + result.multMatrix(inversebaseframe[i]); + } + else + { + result.loadMatrix(swapYZ); + result.multMatrix(m); + result.multMatrix(inversebaseframe[i]); + } + result.multMatrix(swapYZ); + } } return &boneData; diff --git a/src/common/objects/dobjtype.cpp b/src/common/objects/dobjtype.cpp index a7c3dfb0f..d08947903 100644 --- a/src/common/objects/dobjtype.cpp +++ b/src/common/objects/dobjtype.cpp @@ -680,14 +680,14 @@ int PClass::FindVirtualIndex(FName name, PFunction::Variant *variant, PFunction auto vproto = Virtuals[i]->Proto; auto &vflags = Virtuals[i]->ArgFlags; - int n = flags.size(); + int n = flags.SSize(); bool flagsOk = true; for(int i = 0; i < n; i++) { - int argA = i >= vflags.size() ? 0 : vflags[i]; - int argB = i >= flags.size() ? 0 : flags[i]; + int argA = i >= vflags.SSize() ? 0 : vflags[i]; + int argB = i >= flags.SSize() ? 0 : flags[i]; bool AisRef = argA & (VARF_Out | VARF_Ref); bool BisRef = argB & (VARF_Out | VARF_Ref); diff --git a/src/common/rendering/r_videoscale.cpp b/src/common/rendering/r_videoscale.cpp index 51f2844d1..837886be9 100644 --- a/src/common/rendering/r_videoscale.cpp +++ b/src/common/rendering/r_videoscale.cpp @@ -102,14 +102,14 @@ namespace float v_MinimumToFill2(uint32_t inwidth, uint32_t inheight) { // sx = screen x dimension, sy = same for y - float sx = (float)inwidth * 1.2, sy = (float)inheight; + float sx = (float)inwidth * 1.2f, sy = (float)inheight; static float lastsx = 0., lastsy = 0., result = 0.; if (lastsx != sx || lastsy != sy) { if (sx <= 0. || sy <= 0.) return 1.; // prevent x/0 error // set absolute minimum scale to fill the entire screen but get as close to 640x400 as possible - float ssx = (float)(VID_MIN_UI_WIDTH) / 1.2 / sx, ssy = (float)(VID_MIN_UI_HEIGHT) / sy; + float ssx = (float)(VID_MIN_UI_WIDTH) / 1.2f / sx, ssy = (float)(VID_MIN_UI_HEIGHT) / sy; result = (ssx < ssy) ? ssy : ssx; lastsx = sx; lastsy = sy; @@ -165,7 +165,7 @@ namespace { true, [](uint32_t Width, uint32_t Height)->uint32_t { return 1280; }, [](uint32_t Width, uint32_t Height)->uint32_t { return 800; }, 1.2f, false }, // 4 - 1280x800 { true, [](uint32_t Width, uint32_t Height)->uint32_t { return vid_scale_customwidth; }, [](uint32_t Width, uint32_t Height)->uint32_t { return vid_scale_customheight; }, 1.0f, true }, // 5 - Custom { true, [](uint32_t Width, uint32_t Height)->uint32_t { return 320; }, [](uint32_t Width, uint32_t Height)->uint32_t { return 200; }, 1.2f, false }, // 6 - 320x200 - { true, [](uint32_t Width, uint32_t Height)->uint32_t { return v_mfillX2(Width, Height) * 1.2; }, [](uint32_t Width, uint32_t Height)->uint32_t { return v_mfillY2(Width, Height); }, 1.2f, false }, // 7 - Minimum Scale to Fill Entire Screen (1.2) + { true, [](uint32_t Width, uint32_t Height)->uint32_t { return uint32_t(v_mfillX2(Width, Height) * 1.2); }, [](uint32_t Width, uint32_t Height)->uint32_t { return v_mfillY2(Width, Height); }, 1.2f, false }, // 7 - Minimum Scale to Fill Entire Screen (1.2) }; bool isOutOfBounds(int x) { diff --git a/src/common/textures/m_png.cpp b/src/common/textures/m_png.cpp index 83714a6ac..d32083bb4 100644 --- a/src/common/textures/m_png.cpp +++ b/src/common/textures/m_png.cpp @@ -49,6 +49,7 @@ #endif #include "m_png.h" #include "basics.h" +#include "printf.h" // MACROS ------------------------------------------------------------------ @@ -944,7 +945,13 @@ bool M_SaveBitmap(const uint8_t *from, ESSType color_type, int width, int height y = height; stream.next_out = buffer.data(); - stream.avail_out = buffer.size(); + + if(buffer.size() > UINT_MAX) + { + I_Error("save png buffer too large"); + } + + stream.avail_out = (unsigned int) buffer.size(); temprow[0][0] = 0; #if USE_FILTER_HEURISTIC @@ -1006,12 +1013,24 @@ bool M_SaveBitmap(const uint8_t *from, ESSType color_type, int width, int height } while (stream.avail_out == 0) { - if (!WriteIDAT (file, buffer.data(), buffer.size())) + + if(buffer.size() > INT_MAX) + { + I_Error("save png buffer too large"); + } + int sz = (int) buffer.size(); + if (!WriteIDAT (file, buffer.data(), sz)) { return false; } stream.next_out = buffer.data(); - stream.avail_out = buffer.size(); + + if(buffer.size() > UINT_MAX) + { + I_Error("save png buffer too large"); + } + + stream.avail_out = (unsigned int) buffer.size(); if (stream.avail_in != 0) { err = deflate (&stream, (y == 0) ? Z_FINISH : 0); @@ -1032,12 +1051,23 @@ bool M_SaveBitmap(const uint8_t *from, ESSType color_type, int width, int height } if (stream.avail_out == 0) { - if (!WriteIDAT (file, buffer.data(), buffer.size())) + if(buffer.size() > INT_MAX) + { + I_Error("save png buffer too large"); + } + int sz = (int) buffer.size(); + if (!WriteIDAT (file, buffer.data(), sz)) { return false; } stream.next_out = buffer.data(); - stream.avail_out = buffer.size(); + + if(buffer.size() > UINT_MAX) + { + I_Error("save png buffer too large"); + } + + stream.avail_out = (unsigned int) buffer.size(); } } @@ -1047,7 +1077,15 @@ bool M_SaveBitmap(const uint8_t *from, ESSType color_type, int width, int height { return false; } - return WriteIDAT (file, buffer.data(), buffer.size() - stream.avail_out); + + + if((buffer.size() - stream.avail_out) > INT_MAX) + { + I_Error("save png buffer too large"); + } + int sz = (int) (buffer.size() - stream.avail_out); + + return WriteIDAT (file, buffer.data(), sz); } //========================================================================== diff --git a/src/common/utility/matrix.h b/src/common/utility/matrix.h index 74d764bb2..a8996a67c 100644 --- a/src/common/utility/matrix.h +++ b/src/common/utility/matrix.h @@ -107,7 +107,7 @@ class VSMatrix { static void multMatrix(FLOATTYPE *resMatrix, const FLOATTYPE *aMatrix); static void setIdentityMatrix(FLOATTYPE *mat, int size = 4); - + public: /// The storage for matrices FLOATTYPE mMatrix[16]; diff --git a/src/common/utility/quaternion.h b/src/common/utility/quaternion.h index a1c506e6e..9f2af2619 100644 --- a/src/common/utility/quaternion.h +++ b/src/common/utility/quaternion.h @@ -82,25 +82,15 @@ public: } // returns the XY fields as a 2D-vector. - const Vector2& XY() const + Vector2 XY() const { - return *reinterpret_cast(this); + return Vector2(X, Y); } - Vector2& XY() + // returns the XYZ fields as a 3D-vector. + Vector3 XYZ() const { - return *reinterpret_cast(this); - } - - // returns the XY fields as a 2D-vector. - const Vector3& XYZ() const - { - return *reinterpret_cast(this); - } - - Vector3& XYZ() - { - return *reinterpret_cast(this); + return Vector3(X, Y, Z); } @@ -320,7 +310,10 @@ public: auto factor = sinTheta / g_sqrt(lengthSquared); TQuaternion ret; ret.W = cosTheta; - ret.XYZ() = factor * axis; + auto xyz = vec_t(factor) * axis; + ret.X = vec_t(xyz.X); + ret.Y = vec_t(xyz.Y); + ret.Z = vec_t(xyz.Z); return ret; } static TQuaternion FromAngles(TAngle yaw, TAngle pitch, TAngle roll) diff --git a/src/common/utility/vectors.h b/src/common/utility/vectors.h index 299a08985..a2dfae389 100644 --- a/src/common/utility/vectors.h +++ b/src/common/utility/vectors.h @@ -704,6 +704,11 @@ struct TVector3 *this = *this ^ other; return *this; } + + constexpr TVector3 ScaleXYZ (const TVector3 &scaling) + { + return TVector3(X * scaling.X, Y * scaling.Y, Z * scaling.Z); + } }; template diff --git a/src/common/widgets/netstartwindow.cpp b/src/common/widgets/netstartwindow.cpp index 674a119ee..c7ec55f33 100644 --- a/src/common/widgets/netstartwindow.cpp +++ b/src/common/widgets/netstartwindow.cpp @@ -78,7 +78,7 @@ void NetStartWindow::NetDisconnect(int client) if (Instance) { for (size_t i = 1u; i < Instance->LobbyWindow->GetColumnAmount(); ++i) - Instance->LobbyWindow->UpdateItem("", client, i); + Instance->LobbyWindow->UpdateItem("", client, int(i)); } } diff --git a/src/d_main.cpp b/src/d_main.cpp index 97bb47ef7..d0df8d06f 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -1785,12 +1785,15 @@ FExecList *D_MultiExec (FArgs *list, FExecList *exec) static void GetCmdLineFiles(std::vector& wadfiles) { FString *args; - int i, argc; + int i; + int argc; argc = Args->CheckParmList("-file", &args); + assert(wadfiles.size() < INT_MAX); + // [RL0] Check for array size to only add new wads - for (i = wadfiles.size(); i < argc; ++i) + for (i = int(wadfiles.size()); i < argc; ++i) { D_AddWildFile(wadfiles, args[i].GetChars(), ".wad", GameConfig); } diff --git a/src/d_net.cpp b/src/d_net.cpp index 68959425d..31e489c2b 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -418,7 +418,7 @@ void Net_SetWaiting() // [RH] Rewritten to properly calculate the packet size // with our variable length Command. -static int GetNetBufferSize() +static size_t GetNetBufferSize() { if (NetBuffer[0] & NCMD_EXIT) return 1 + (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator); @@ -528,7 +528,7 @@ static bool HGetPacket() if (RemoteClient == -1) return false; - int sizeCheck = GetNetBufferSize(); + size_t sizeCheck = GetNetBufferSize(); if (NetBufferLength != sizeCheck) { Printf("Incorrect packet size %d (expected %d)\n", NetBufferLength, sizeCheck); @@ -826,7 +826,7 @@ static void GetPackets() for (size_t i = 0u; i < consistencies.Size(); ++i) { - const int cTic = baseConsistency + i; + const int cTic = baseConsistency + int(i); if (cTic <= pState.CurrentNetConsistency) continue; @@ -859,7 +859,7 @@ static void GetPackets() for (size_t i = 0u; i < data.Size(); ++i) { - const int seq = baseSequence + i; + const int seq = baseSequence + int(i); // Duplicate command, ignore it. if (seq <= pState.CurrentSequence) continue; @@ -1346,13 +1346,13 @@ void NetUpdate(int tics) return; } - constexpr size_t MaxPlayersPerPacket = 16u; + constexpr int MaxPlayersPerPacket = 16; int startSequence = startTic / TicDup; int endSequence = newestTic; int quitters = 0; int quitNums[MAXPLAYERS]; - size_t players = 1u; + int players = 1u; int maxCommands = MAXSENDTICS; if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { @@ -1656,7 +1656,7 @@ const char* Net_GetClientName(int client, unsigned int charLimit = 0u) return players[client].userinfo.GetName(charLimit); } -int Net_SetUserInfo(int client, uint8_t*& stream) +size_t Net_SetUserInfo(int client, uint8_t*& stream) { auto str = D_GetUserInfoStrings(client, true); const size_t userSize = str.Len() + 1; @@ -1664,29 +1664,29 @@ int Net_SetUserInfo(int client, uint8_t*& stream) return userSize; } -int Net_ReadUserInfo(int client, uint8_t*& stream) +size_t Net_ReadUserInfo(int client, uint8_t*& stream) { const uint8_t* start = stream; D_ReadUserInfoStrings(client, &stream, false); - return int(stream - start); + return stream - start; } -int Net_SetGameInfo(uint8_t*& stream) +size_t Net_SetGameInfo(uint8_t*& stream) { const uint8_t* start = stream; WriteString(startmap.GetChars(), &stream); WriteInt32(rngseed, &stream); C_WriteCVars(&stream, CVAR_SERVERINFO, true); - return int(stream - start); + return stream - start; } -int Net_ReadGameInfo(uint8_t*& stream) +size_t Net_ReadGameInfo(uint8_t*& stream) { const uint8_t* start = stream; startmap = ReadStringConst(&stream); rngseed = ReadInt32(&stream); C_ReadCVars(&stream); - return int(stream - start); + return stream - start; } // Connects players to each other if needed. diff --git a/src/events.cpp b/src/events.cpp index a45bacd8b..80059b5e7 100755 --- a/src/events.cpp +++ b/src/events.cpp @@ -123,7 +123,12 @@ void DNetworkBuffer::AddDouble(double msg) void DNetworkBuffer::AddString(const FString& msg) { - _size += msg.Len() + 1u; + if(msg.Len() >= UINT_MAX) + { + I_Error("network buffer string too large"); + } + + _size += ((unsigned int)msg.Len()) + 1u; _buffer.Push({ NET_STRING, msg }); } @@ -427,8 +432,14 @@ bool EventManager::SendNetworkCommand(const FName& cmd, VMVa_List& args) { ++bytes; // Strings will always consume at least one byte. const FString* str = ListGetString(args); + + if(str->Len() >= UINT_MAX || (bytes + (unsigned int)str->Len()) >= UINT_MAX) + { + I_Error("network buffer string too large"); + } + if (str != nullptr) - bytes += str->Len(); + bytes += (unsigned int)str->Len(); break; } } diff --git a/src/g_level.cpp b/src/g_level.cpp index 4a269c66b..69a4c9af2 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -2034,7 +2034,7 @@ void G_ReadSnapshots(FResourceFile *resf) G_ClearSnapshots(); - for (unsigned j = 0; j < resf->EntryCount(); j++) + for (unsigned j = 0; j < resf->EntryCountU(); j++) { auto name = resf->getName(j); auto ptr = strstr(name, ".map.json"); diff --git a/src/gamedata/r_defs.h b/src/gamedata/r_defs.h index 93531b555..25608df36 100644 --- a/src/gamedata/r_defs.h +++ b/src/gamedata/r_defs.h @@ -1033,7 +1033,7 @@ public: void SetPlaneReflectivity(int pos, double val) { - reflect[pos] = val; + reflect[pos] = float(val); } double GetPlaneReflectivity(int pos) diff --git a/src/gamedata/textures/animations.cpp b/src/gamedata/textures/animations.cpp index 619375c46..d79e2bab7 100644 --- a/src/gamedata/textures/animations.cpp +++ b/src/gamedata/textures/animations.cpp @@ -210,7 +210,7 @@ void FTextureAnimator::InitAnimated (void) if (lumpnum != -1) { auto animatedlump = fileSystem.ReadFile (lumpnum); - int animatedlen = fileSystem.FileLength(lumpnum); + ptrdiff_t animatedlen = fileSystem.FileLength(lumpnum); auto animdefs = animatedlump.bytes(); const uint8_t *anim_p; FTextureID pic1, pic2; diff --git a/src/gamedata/textures/buildloader.cpp b/src/gamedata/textures/buildloader.cpp index 83c547b50..a29cbb1e9 100644 --- a/src/gamedata/textures/buildloader.cpp +++ b/src/gamedata/textures/buildloader.cpp @@ -283,7 +283,12 @@ void InitBuildTiles() } auto& artdata = TexMan.GetNewBuildTileData(); - artdata.Resize(fileSystem.FileLength(lumpnum)); + + ptrdiff_t len = fileSystem.FileLength(lumpnum); + + assert(len >= 0 && len < UINT_MAX); + + artdata.Resize((unsigned int)len); fileSystem.ReadFile(lumpnum, &artdata[0]); if ((numtiles = CountTiles(&artdata[0])) > 0) diff --git a/src/launcher/settingspage.cpp b/src/launcher/settingspage.cpp index 1bfe54d22..01b6817df 100644 --- a/src/launcher/settingspage.cpp +++ b/src/launcher/settingspage.cpp @@ -93,7 +93,7 @@ SettingsPage::SettingsPage(LauncherWindow* launcher, int* autoloadflags) : Widge } } } - catch (const std::exception& ex) + catch (const std::exception&) { hideLanguage = true; } diff --git a/src/p_openmap.cpp b/src/p_openmap.cpp index ef688cda6..0906f65ea 100644 --- a/src/p_openmap.cpp +++ b/src/p_openmap.cpp @@ -283,7 +283,7 @@ MapData *P_OpenMapData(const char * mapname, bool justcheck) map->MapLumps[0].Reader = map->resource->GetEntryReader(0, FileSys::READER_SHARED); uppercopy(map->MapLumps[0].Name, map->resource->getName(0)); - for(uint32_t i = 1; i < map->resource->EntryCount(); i++) + for(uint32_t i = 1; i < map->resource->EntryCountU(); i++) { const char* lumpname = map->resource->getName(i); diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 66b85ea79..c4ec8a8b8 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -725,8 +725,19 @@ struct AnimModelOverride enum EModelDataFlags { - MODELDATA_HADMODEL = 1 << 0, - MODELDATA_OVERRIDE_FLAGS = 1 << 1, + MODELDATA_HADMODEL = 1 << 0, + MODELDATA_OVERRIDE_FLAGS = 1 << 1, + MODELDATA_GET_BONE_INFO = 1 << 2, + MODELDATA_GET_BONE_INFO_RECALC = 1 << 3, // RECALCULATE BONE INFO INSTANTLY WHEN STATE/ANIMATION CHANGES, MIGHT GET EXPENSIVE + + + + + + + + + MODELDATA_IQMFLAGS = MODELDATA_GET_BONE_INFO | MODELDATA_GET_BONE_INFO_RECALC, }; class DActorModelData : public DObject @@ -739,6 +750,7 @@ public: TArray animationIDs; TArray modelFrameGenerators; TArray> modelBoneOverrides; + TArray modelBoneInfo; int flags; int overrideFlagsSet; int overrideFlagsClear; @@ -813,6 +825,15 @@ public: virtual void Tick() override; void EnableNetworking(const bool enable) override; + void CalcBones(bool recalc); + TRS GetBoneTRS(int model_index, int bone_index, bool with_override); + + //outmat must be double[16] + void GetBoneMatrix(int model_index, int bone_index, bool with_override, double *outMat); + + void GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &normal); + void GetObjectToWorldMatrix(double *outMat); + static AActor *StaticSpawn (FLevelLocals *Level, PClassActor *type, const DVector3 &pos, replace_t allowreplacement, bool SpawningMapThing = false); inline AActor *GetDefault () const diff --git a/src/playsim/p_actionfunctions.cpp b/src/playsim/p_actionfunctions.cpp index 98897d873..be5463bae 100644 --- a/src/playsim/p_actionfunctions.cpp +++ b/src/playsim/p_actionfunctions.cpp @@ -5125,15 +5125,6 @@ static void CleanupModelData(AActor * mobj) FQuaternion InterpolateQuat(const FQuaternion &from, const FQuaternion &to, float t, float invt); -static void SetModelBoneRotationInternal(AActor * self, FModel * mdl, int model_index, int index, FQuaternion rotation, int mode, double interpolation_duration, double switchTic) -{ - if(self->modelData->modelBoneOverrides.Size() <= model_index) self->modelData->modelBoneOverrides.Resize(model_index + 1); - - self->modelData->modelBoneOverrides[model_index].Resize(mdl->NumJoints()); - - self->modelData->modelBoneOverrides[model_index][index].rotation.Set(rotation, switchTic, interpolation_duration, mode); -} - template FModel * SetGetBoneShared(AActor * self, int model_index) { @@ -5149,11 +5140,11 @@ FModel * SetGetBoneShared(AActor * self, int model_index) EnsureModelData(self); - if(self->modelData->models.Size() > model_index && self->modelData->models[model_index].modelID >= 0 && self->modelData->models[model_index].modelID < Models.Size()) + if(self->modelData->models.SSize() > model_index && self->modelData->models[model_index].modelID >= 0 && self->modelData->models[model_index].modelID < Models.SSize()) { return Models[self->modelData->models[model_index].modelID]; } - else if(BaseSpriteModelFrames[self->GetClass()].modelIDs.Size() > model_index) + else if(BaseSpriteModelFrames[self->GetClass()].modelIDs.SSize() > model_index) { return Models[BaseSpriteModelFrames[self->GetClass()].modelIDs[model_index]]; } @@ -5182,7 +5173,7 @@ FModel * SetGetBoneSharedIndex(AActor * self, int model_index, int &bone_index, ThrowAbortException(X_OTHER, "bone index out of range"); } - if(self->modelData->modelBoneOverrides.Size() <= model_index) self->modelData->modelBoneOverrides.Resize(model_index + 1); + if(self->modelData->modelBoneOverrides.SSize() <= model_index) self->modelData->modelBoneOverrides.Resize(model_index + 1); self->modelData->modelBoneOverrides[model_index].Resize(mdl->NumJoints()); @@ -5224,6 +5215,8 @@ static void SetModelBoneRotationNative(AActor * self, int model_index, int bone_ if(!mdl) return; self->modelData->modelBoneOverrides[model_index][bone_index].rotation.Set(FQuaternion(rot_x, rot_y, rot_z, rot_w), self->Level->totaltime + ticFrac, interpolation_duration, mode); + + self->CalcBones(true); } static void SetBoneRotationNative(AActor * self, int bone_index, double rot_x, double rot_y, double rot_z, double rot_w, int mode, double interpolation_duration, double ticFrac) @@ -5242,6 +5235,8 @@ static void SetModelNamedBoneRotationNative(AActor * self, int model_index, int if(!mdl) return; self->modelData->modelBoneOverrides[model_index][bone_index].rotation.Set(FQuaternion(rot_x, rot_y, rot_z, rot_w), self->Level->totaltime + ticFrac, interpolation_duration, mode); + + self->CalcBones(true); } static void SetNamedBoneRotationNative(AActor * self, int boneName_i, double rot_x, double rot_y, double rot_z, double rot_w, int mode, double interpolation_duration, double ticFrac) @@ -5294,6 +5289,8 @@ static void SetModelBoneTranslationNative(AActor * self, int model_index, int bo if(!mdl) return; self->modelData->modelBoneOverrides[model_index][bone_index].translation.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); + + self->CalcBones(true); } static void SetBoneTranslationNative(AActor * self, int bone_index, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) @@ -5312,6 +5309,8 @@ static void SetModelNamedBoneTranslationNative(AActor * self, int model_index, i if(!mdl) return; self->modelData->modelBoneOverrides[model_index][bone_index].translation.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); + + self->CalcBones(true); } static void SetNamedBoneTranslationNative(AActor * self, int boneName_i, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) @@ -5362,6 +5361,8 @@ static void SetModelBoneScalingNative(AActor * self, int model_index, int bone_i if(!mdl) return; self->modelData->modelBoneOverrides[model_index][bone_index].scaling.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); + + self->CalcBones(true); } static void SetBoneScalingNative(AActor * self, int bone_index, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) @@ -5380,6 +5381,8 @@ static void SetModelNamedBoneScalingNative(AActor * self, int model_index, int b if(!mdl) return; self->modelData->modelBoneOverrides[model_index][bone_index].scaling.Set(FVector3(rot_x, rot_y, rot_z), self->Level->totaltime + ticFrac, interpolation_duration, mode); + + self->CalcBones(true); } static void SetNamedBoneScalingNative(AActor * self, int boneName_i, double rot_x, double rot_y, double rot_z, int mode, double interpolation_duration, double ticFrac) @@ -5665,51 +5668,46 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetNamedBoneChildren, GetNamedBoneChildren return 0; } -static double GetBoneLengthNative(AActor * self, int bone_index) -{ - FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); - - return mdl->GetJointLength(bone_index); -} - -static double GetNamedBoneLengthNative(AActor * self, int boneName_i) -{ - FName bone_name {ENamedName(boneName_i)}; - - int bone_index; - - FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); - - return mdl->GetJointLength(bone_index); -} - -DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneLength, GetBoneLengthNative) -{ - PARAM_SELF_PROLOGUE(AActor); - PARAM_INT(boneindex); - - ACTION_RETURN_FLOAT(GetBoneLengthNative(self, boneindex)); -} - -DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetNamedBoneLength, GetNamedBoneLengthNative) -{ - PARAM_SELF_PROLOGUE(AActor); - PARAM_NAME(bonename); - - ACTION_RETURN_FLOAT(GetNamedBoneLengthNative(self, bonename.GetIndex())); -} - -DEFINE_ACTION_FUNCTION(AActor, GetBoneDir) +DEFINE_ACTION_FUNCTION(AActor, GetBoneBaseTRS) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(bone_index); FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); - ACTION_RETURN_VEC3(DVector3(mdl->GetJointDir(bone_index))); + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl) + { + TRS pose = mdl->GetJointBaseTRS(bone_index); + + translation = DVector3(pose.translation); + rotation = DVector4(pose.rotation); + scaling = DVector3(pose.scaling); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; } -DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneDir) +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneBaseTRS) { PARAM_SELF_PROLOGUE(AActor); PARAM_NAME(bone_name); @@ -5718,7 +5716,36 @@ DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneDir) FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); - ACTION_RETURN_VEC3(DVector3(mdl->GetJointDir(bone_index))); + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl) + { + TRS pose = mdl->GetJointBaseTRS(bone_index); + + translation = DVector3(pose.translation); + rotation = DVector4(pose.rotation); + scaling = DVector3(pose.scaling); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; } static int GetBoneCountNative(AActor * self) @@ -5735,9 +5762,415 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetBoneCount, GetBoneCountNative) ACTION_RETURN_INT(GetBoneCountNative(self)); } +//================================================ +// +// Bone Pose Getters +// +//================================================ + +static int GetAnimStartFrameNative(AActor * self, int animName_i) +{ + FName anim_name {ENamedName(animName_i)}; + FModel * mdl = SetGetBoneShared(self, 0); + return mdl->FindFirstFrame(anim_name); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetAnimStartFrame, GetAnimStartFrameNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(animName); + + ACTION_RETURN_INT(GetAnimStartFrameNative(self, animName.GetIndex())); +} + +static int GetAnimEndFrameNative(AActor * self, int animName_i) +{ + FName anim_name {ENamedName(animName_i)}; + FModel * mdl = SetGetBoneShared(self, 0); + return mdl->FindLastFrame(anim_name); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetAnimEndFrame, GetAnimEndFrameNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(animName); + + ACTION_RETURN_INT(GetAnimEndFrameNative(self, animName.GetIndex())); +} + +static double GetAnimFramerateNative(AActor * self, int animName_i) +{ + FName anim_name {ENamedName(animName_i)}; + FModel * mdl = SetGetBoneShared(self, 0); + return mdl->FindFramerate(anim_name); +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetAnimFramerate, GetAnimFramerateNative) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(animName); + + ACTION_RETURN_FLOAT(GetAnimFramerateNative(self, animName.GetIndex())); +} + +DEFINE_ACTION_FUNCTION(AActor, GetBoneFramePose) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + PARAM_INT(frame_index); + + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl && frame_index < mdl->NumFrames()) + { + TRS pose = mdl->GetJointPose(bone_index, frame_index); + + translation = DVector3(pose.translation); + rotation = DVector4(pose.rotation); + scaling = DVector3(pose.scaling); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneFramePose) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + PARAM_INT(frame_index); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl && frame_index < mdl->NumFrames()) + { + TRS pose = mdl->GetJointPose(bone_index, frame_index); + + translation = DVector3(pose.translation); + rotation = DVector4(pose.rotation); + scaling = DVector3(pose.scaling); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; +} + +DEFINE_ACTION_FUNCTION(AActor, GetBoneBasePosition) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + ACTION_RETURN_VEC3(DVector3(mdl->GetJointPosition(bone_index))); +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneBasePosition) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + ACTION_RETURN_VEC3(DVector3(mdl->GetJointPosition(bone_index))); +} + +DEFINE_ACTION_FUNCTION(AActor, GetBoneBaseRotation) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + ACTION_RETURN_VEC4(DVector4(mdl->GetJointRotation(bone_index))); +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneBaseRotation) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + ACTION_RETURN_VEC4(DVector4(mdl->GetJointRotation(bone_index))); +} + +//================================================ +// +// Bone TRS Getters +// +//================================================ + +DEFINE_ACTION_FUNCTION(AActor, GetBone) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + PARAM_BOOL(with_override); + + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl) + { + TRS trs = self->GetBoneTRS(0, bone_index, with_override); + + translation = DVector3(trs.translation); + rotation = DVector4(trs.rotation); + scaling = DVector3(trs.scaling); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBone) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + PARAM_BOOL(with_override); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + DVector3 translation(0,0,0); + DVector4 rotation(0,0,0,1); + DVector3 scaling(0,0,0); + + if(mdl) + { + TRS trs = self->GetBoneTRS(0, bone_index, with_override); + + translation = DVector3(trs.translation); + rotation = DVector4(trs.rotation); + scaling = DVector3(trs.scaling); + } + + if(numret > 2) + { + ret[2].SetVector(scaling); + numret = 3; + } + + if(numret > 1) + { + ret[1].SetVector(translation); + } + + if(numret > 0) + { + ret[0].SetVector4(rotation); + } + + return numret; +} +DEFINE_ACTION_FUNCTION(AActor, TransformByBone) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + PARAM_FLOAT(pos_x); + PARAM_FLOAT(pos_y); + PARAM_FLOAT(pos_z); + PARAM_FLOAT(dir_x); + PARAM_FLOAT(dir_y); + PARAM_FLOAT(dir_z); + PARAM_BOOL(with_override); + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + DVector3 position(pos_x, pos_y, pos_z); + DVector3 direction(dir_x, dir_y, dir_z); + + if(mdl) + { + self->GetBonePosition(0, bone_index, with_override, position, direction); + } + + if(numret > 1) + { + ret[1].SetVector(direction); + } + + if(numret > 0) + { + ret[0].SetVector(position); + } + + return numret; +} + +DEFINE_ACTION_FUNCTION(AActor, TransformByNamedBone) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + PARAM_FLOAT(pos_x); + PARAM_FLOAT(pos_y); + PARAM_FLOAT(pos_z); + PARAM_FLOAT(dir_x); + PARAM_FLOAT(dir_y); + PARAM_FLOAT(dir_z); + PARAM_BOOL(with_override); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + DVector3 position(pos_x, pos_y, pos_z); + DVector3 direction(dir_x, dir_y, dir_z); + + if(mdl) + { + self->GetBonePosition(0, bone_index, with_override, position, direction); + } + + if(numret > 1) + { + ret[1].SetVector(direction); + } + + if(numret > 0) + { + ret[0].SetVector(position); + } + + return numret; +} + +//================================================ +// +// Bone Matrix Getters +// +//================================================ + +DEFINE_ACTION_FUNCTION(AActor, GetBoneMatrixRaw) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + PARAM_POINTER(outMatrix, TArray); + PARAM_BOOL(with_override); + + if(outMatrix) + { + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + outMatrix->Clear(); + outMatrix->Resize(16); + + if(mdl) + { + self->GetBoneMatrix(0, bone_index, with_override, outMatrix->Data()); + } + } + return 0; +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneMatrixRaw) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + PARAM_POINTER(outMatrix, TArray); + PARAM_BOOL(with_override); + + if(outMatrix) + { + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + outMatrix->Clear(); + outMatrix->Resize(16); + + if(mdl) + { + self->GetBoneMatrix(0, bone_index, with_override, outMatrix->Data()); + } + } + return 0; +} + +DEFINE_ACTION_FUNCTION(AActor, GetObjectToWorldMatrixRaw) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_POINTER(outMatrix, TArray); + + if(outMatrix) + { + FModel * mdl = SetGetBoneShared(self, 0); + + outMatrix->Clear(); + outMatrix->Resize(16); + + if(mdl) + { + self->GetObjectToWorldMatrix(outMatrix->Data()); + } + } + return 0; +} //================================================ // SetAnimation @@ -5771,7 +6204,11 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s if(animName == NAME_None) { + if(self->modelData->curAnim.flags & MODELANIM_NONE) return; + self->modelData->curAnim.flags = MODELANIM_NONE; + self->CalcBones(true); + return; } @@ -5794,11 +6231,11 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s } FModel * animation = nullptr; - if (animID >= 0 && animID < Models.Size()) + if (animID >= 0 && animID < Models.SSize()) { animation = Models[animID]; } - else if(self->modelData->models.Size() && self->modelData->models[0].modelID >= 0 && self->modelData->models[0].modelID < Models.Size()) + else if(self->modelData->models.Size() > 0 && self->modelData->models[0].modelID >= 0 && self->modelData->models[0].modelID < Models.SSize()) { animation = Models[self->modelData->models[0].modelID]; } @@ -5811,8 +6248,12 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s if(animStart == FErr_NotFound) { - self->modelData->curAnim.flags = MODELANIM_NONE; Printf("Could not find animation %s\n", animName.GetChars()); + if(self->modelData->curAnim.flags & MODELANIM_NONE) return; + + self->modelData->curAnim.flags = MODELANIM_NONE; + self->CalcBones(true); + return; } @@ -5861,20 +6302,32 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s if(startFrame >= len) { - self->modelData->curAnim.flags = MODELANIM_NONE; Printf("frame %d (startFrame) is past the end of animation %s\n", startFrame, animName.GetChars()); + if(self->modelData->curAnim.flags & MODELANIM_NONE) return; + + self->modelData->curAnim.flags = MODELANIM_NONE; + self->CalcBones(true); + return; } else if(loopFrame >= len) { - self->modelData->curAnim.flags = MODELANIM_NONE; Printf("frame %d (loopFrame) is past the end of animation %s\n", startFrame, animName.GetChars()); + if(self->modelData->curAnim.flags & MODELANIM_NONE) return; + + self->modelData->curAnim.flags = MODELANIM_NONE; + self->CalcBones(true); + return; } else if(endFrame >= len) { - self->modelData->curAnim.flags = MODELANIM_NONE; Printf("frame %d (endFrame) is past the end of animation %s\n", endFrame, animName.GetChars()); + if(self->modelData->curAnim.flags & MODELANIM_NONE) return; + + self->modelData->curAnim.flags = MODELANIM_NONE; + self->CalcBones(true); + return; } @@ -5896,6 +6349,8 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s self->modelData->curAnim.startTic = tic; self->modelData->curAnim.switchOffset = 0; } + + self->CalcBones(true); } void SetAnimationNative(AActor * self, int i_animName, double framerate, int startFrame, int loopFrame, int endFrame, int interpolateTics, int flags) @@ -5957,29 +6412,60 @@ void SetAnimationFrameRateUINative(AActor * self, double framerate) SetAnimationFrameRateInternal(self, framerate, I_GetTimeFrac()); } -void SetModelFlag(AActor * self, int flag) +void SetModelFlag(AActor * self, int flag, int iqmFlag) { EnsureModelData(self); - self->modelData->flags |= MODELDATA_OVERRIDE_FLAGS; - self->modelData->overrideFlagsSet |= flag; - self->modelData->overrideFlagsClear &= ~flag; + + iqmFlag &= MODELDATA_IQMFLAGS; + + if(flag) + { + self->modelData->flags |= MODELDATA_OVERRIDE_FLAGS; + self->modelData->overrideFlagsSet |= flag; + self->modelData->overrideFlagsClear &= ~flag; + } + + if(iqmFlag) + { + self->modelData->flags |= iqmFlag; + } } -void ClearModelFlag(AActor * self, int flag) +void ClearModelFlag(AActor * self, int flag, int iqmFlag) { EnsureModelData(self); - self->modelData->flags |= MODELDATA_OVERRIDE_FLAGS; - self->modelData->overrideFlagsClear |= flag; - self->modelData->overrideFlagsSet &= ~flag; + + iqmFlag &= MODELDATA_IQMFLAGS; + + if(flag) + { + self->modelData->flags |= MODELDATA_OVERRIDE_FLAGS; + self->modelData->overrideFlagsClear |= flag; + self->modelData->overrideFlagsSet &= ~flag; + } + + if(iqmFlag) + { + self->modelData->flags &= ~iqmFlag; + } + } -void ResetModelFlags(AActor * self) +void ResetModelFlags(AActor * self, int resetModel, int resetIqm) { if(self->modelData) { - self->modelData->overrideFlagsClear = 0; - self->modelData->overrideFlagsSet = 0; - self->modelData->flags &= ~MODELDATA_OVERRIDE_FLAGS; + if(resetModel) + { + self->modelData->overrideFlagsClear = 0; + self->modelData->overrideFlagsSet = 0; + self->modelData->flags &= ~MODELDATA_OVERRIDE_FLAGS; + } + + if(resetIqm) + { + self->modelData->flags &= ~MODELDATA_IQMFLAGS; + } } } @@ -6148,6 +6634,11 @@ void ChangeModelNative( CleanupModelData(mobj); + if(animation != NAME_None || modeldef != nullptr) + { + mobj->CalcBones(true); + } + return; } @@ -6228,8 +6719,9 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, SetModelFlag, SetModelFlag) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(flag); + PARAM_INT(flagIqm); - SetModelFlag(self, flag); + SetModelFlag(self, flag, flagIqm); return 0; } @@ -6238,8 +6730,9 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, ClearModelFlag, ClearModelFlag) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(flag); + PARAM_INT(flagIqm); - ClearModelFlag(self, flag); + ClearModelFlag(self, flag, flagIqm); return 0; } @@ -6247,8 +6740,10 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, ClearModelFlag, ClearModelFlag) DEFINE_ACTION_FUNCTION_NATIVE(AActor, ResetModelFlags, ResetModelFlags) { PARAM_SELF_PROLOGUE(AActor); + PARAM_BOOL(resetModel); + PARAM_BOOL(resetIqm); - ResetModelFlags(self); + ResetModelFlags(self, resetModel, resetIqm); return 0; } diff --git a/src/playsim/p_enemy.cpp b/src/playsim/p_enemy.cpp index 16a8918a4..0f69a57e0 100644 --- a/src/playsim/p_enemy.cpp +++ b/src/playsim/p_enemy.cpp @@ -1701,8 +1701,6 @@ int P_LookForTID (AActor *actor, INTBOOL allaround, FLookExParams *params) AActor *LookForEnemiesInBlock (AActor *lookee, int index, void *extparam) { FBlockNode *block; - AActor *link; - AActor *other; FLookExParams *params = (FLookExParams *)extparam; for (block = lookee->Level->blockmap.blocklinks[index]; block != NULL; block = block->NextActor) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 43d98ba25..08182167b 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -101,6 +101,7 @@ #include "fragglescript/t_fs.h" #include "shadowinlines.h" #include "model.h" +#include "models.h" #include "d_net.h" // MACROS ------------------------------------------------------------------ @@ -4244,6 +4245,122 @@ DEFINE_ACTION_FUNCTION(AActor, CheckPortalTransition) return 0; } +void AActor::CalcBones(bool recalc) +{ + if(modelData && (!recalc || (modelData->flags & MODELDATA_GET_BONE_INFO_RECALC)) && modelData->flags & MODELDATA_GET_BONE_INFO) + { + if(picnum.isValid()) return; // picnum overrides don't render models + + FSpriteModelFrame *smf = FindModelFrame(this, sprite, frame, false); // dropped flag is for voxels + + if(!smf) return; + + bool is_decoupled = flags9 & MF9_DECOUPLEDANIMATIONS; + double tic = Level->totaltime + 1; + + CalcModelFrameInfo frameinfo = CalcModelFrame(Level, smf, state, tics, modelData, this, is_decoupled, tic, 1.0); + + ModelDrawInfo drawinfo; + + int boneStartingPosition = -1; + bool evaluatedSingle = false; + + modelData->modelBoneInfo.Resize(frameinfo.modelsamount); + + for (unsigned i = 0; i < frameinfo.modelsamount; i++) + { + if (CalcModelOverrides(i, smf, modelData, frameinfo, drawinfo, is_decoupled)) + { + if(!evaluatedSingle) + { // [Jay] TODO per-model decoupled animations + FModel * mdl = Models[drawinfo.modelid]; + bool nextFrame = frameinfo.smfNext && drawinfo.modelframe != drawinfo.modelframenext; + ProcessModelFrame(mdl, nextFrame, i, smf, modelData, frameinfo, drawinfo, is_decoupled, tic, &modelData->modelBoneInfo[i]); + + if(frameinfo.smf_flags & MDL_MODELSAREATTACHMENTS || is_decoupled) + { + evaluatedSingle = true; + //if(!is_decoupled) break; + + break; // TODO remove this break when per-model decoupled animations are in + } + } + } + } + } +} + +TRS AActor::GetBoneTRS(int model_index, int bone_index, bool with_override) +{ + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index > 0 && bone_index > 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].bones.SSize() < bone_index) + { + return with_override ? modelData->modelBoneInfo[model_index].bones_with_override[bone_index] : modelData->modelBoneInfo[model_index].bones[bone_index]; + } + return {}; +} + +void AActor::GetBoneMatrix(int model_index, int bone_index, bool with_override, double *outMat) +{ + VSMatrix boneMatrix = (with_override ? modelData->modelBoneInfo[model_index].positions_with_override : modelData->modelBoneInfo[model_index].positions)[bone_index]; + + for(int i = 0; i < 16; i++) + { + outMat[i] = boneMatrix.mMatrix[i]; + } +} + +void AActor::GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &normal) +{ + if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) + { + if(picnum.isValid()) return; // picnum overrides don't render models + + FSpriteModelFrame *smf = FindModelFrame(this, sprite, frame, false); // dropped flag is for voxels + + FVector3 objPos = FVector3(Pos() + WorldOffset); + + VSMatrix boneMatrix = (with_override ? modelData->modelBoneInfo[model_index].positions_with_override : modelData->modelBoneInfo[model_index].positions)[bone_index]; + VSMatrix worldMatrix = smf->ObjectToWorldMatrix(this, objPos.X, objPos.Y, objPos.Z, 1.0); + + FVector4 oldPos(pos.X, pos.Z, pos.Y, 1.0); + FVector4 newPos; + FVector4 oldNormal(normal.X, normal.Z, normal.Y, 0.0); + FVector4 newNormal; + + boneMatrix.multMatrixPoint(&oldPos.X, &newPos.X); + boneMatrix.multMatrixPoint(&oldNormal.X, &newNormal.X); + + oldPos = FVector4(FVector3(newPos.X, newPos.Y, newPos.Z) / newPos.W, 1.0); + oldNormal = FVector4(FVector3(newNormal.X, newNormal.Y, newNormal.Z) / newNormal.W, 0.0); + + worldMatrix.multMatrixPoint(&oldPos.X, &newPos.X); + worldMatrix.multMatrixPoint(&oldNormal.X, &newNormal.X); + + pos = DVector3(newPos.X, newPos.Z, newPos.Y); + normal = DVector3(newNormal.X, newNormal.Z, newNormal.Y); + } +} + +void AActor::GetObjectToWorldMatrix(double *outMat) +{ + if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) + { + if(picnum.isValid()) return; // picnum overrides don't render models + + FSpriteModelFrame *smf = FindModelFrame(this, sprite, frame, false); // dropped flag is for voxels + + FVector3 pos = FVector3(Pos() + WorldOffset); + + VSMatrix outMatrix = smf->ObjectToWorldMatrix(this, pos.X, pos.Y, pos.Z, 1.0); + + for(int i = 0; i < 16; i++) + { + outMat[i] = outMatrix.mMatrix[i]; + } + } +} + + // // P_MobjThinker // @@ -4291,6 +4408,11 @@ void AActor::Tick () return; } + if(flags9 & MF9_DECOUPLEDANIMATIONS) + { + CalcBones(false); + } + AActor *onmo; //assert (state != NULL); @@ -4852,6 +4974,11 @@ void AActor::Tick () } } + if(!(flags9 & MF9_DECOUPLEDANIMATIONS) && modelData && !(modelData->flags & MODELDATA_GET_BONE_INFO_RECALC)) + { + CalcBones(false); + } + if (tics == -1 || state->GetCanRaise()) { int respawn_monsters = G_SkillProperty(SKILLP_Respawn); diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 2580cb41e..28dd271b7 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -792,7 +792,7 @@ DEFINE_ACTION_FUNCTION(_PlayerInfo, GetSkin) DEFINE_ACTION_FUNCTION(_PlayerInfo, GetSkinCount) { PARAM_SELF_STRUCT_PROLOGUE(player_t); - ACTION_RETURN_INT(Skins.size()); + ACTION_RETURN_INT(Skins.SSize()); } DEFINE_ACTION_FUNCTION(_PlayerInfo, GetGender) diff --git a/src/r_data/models.cpp b/src/r_data/models.cpp index 8226cf411..34693c20a 100644 --- a/src/r_data/models.cpp +++ b/src/r_data/models.cpp @@ -59,32 +59,42 @@ EXTERN_CVAR (Bool, r_drawvoxels) extern TDeletingArray Voxels; extern TDeletingArray VoxelDefs; -void RenderFrameModels(FModelRenderer* renderer, FLevelLocals* Level, const FSpriteModelFrame *smf, const FState* curState, const int curTics, FTranslationID translation, AActor* actor); - +void RenderFrameModels(FModelRenderer* renderer, FLevelLocals* Level, const FSpriteModelFrame *smf, const FState* curState, int curTics, double tic, FTranslationID translation, AActor* actor); void RenderModel(FModelRenderer *renderer, float x, float y, float z, FSpriteModelFrame *smf, AActor *actor, double ticFrac) { - // Setup transformation. - int smf_flags = smf->getFlags(actor->modelData); - FTranslationID translation = NO_TRANSLATION; if (!(smf_flags & MDL_IGNORETRANSLATION)) translation = actor->Translation; - // y scale for a sprite means height, i.e. z in the world! + VSMatrix objectToWorldMatrix = smf->ObjectToWorldMatrix(actor, x, y, z, ticFrac); + float scaleFactorX = actor->Scale.X * smf->xscale; float scaleFactorY = actor->Scale.X * smf->yscale; float scaleFactorZ = actor->Scale.Y * smf->zscale; - float pitch = 0; - float roll = 0; - double rotateOffset = 0; + float orientation = scaleFactorX * scaleFactorY * scaleFactorZ; + + renderer->BeginDrawModel(actor->RenderStyle, smf_flags, objectToWorldMatrix, orientation < 0); + RenderFrameModels(renderer, actor->Level, smf, actor->state, actor->tics, ticFrac, translation, actor); + renderer->EndDrawModel(actor->RenderStyle, smf_flags); +} + +VSMatrix FSpriteModelFrame::ObjectToWorldMatrix(AActor * actor, float x, float y, float z, double ticFrac) +{ + int smf_flags = getFlags(actor->modelData); + + // Setup transformation. DRotator angles; + if (actor->renderflags & RF_INTERPOLATEANGLES) // [Nash] use interpolated angles angles = actor->InterpolatedAngles(ticFrac); else angles = actor->Angles; + float angle = angles.Yaw.Degrees(); + float pitch = 0; + float roll = 0; // [BB] Workaround for the missing pitch information. if ((smf_flags & MDL_PITCHFROMMOMENTUM)) @@ -107,20 +117,6 @@ void RenderModel(FModelRenderer *renderer, float x, float y, float z, FSpriteMod } } - if (smf_flags & MDL_ROTATING) - { - if (smf->rotationSpeed > 0.0000000001 || smf->rotationSpeed < -0.0000000001) - { - double turns = (I_GetTime() + I_GetTimeFrac()) / (200.0 / smf->rotationSpeed); - turns -= floor(turns); - rotateOffset = turns * 360.0; - } - else - { - rotateOffset = 0.0; - } - } - // Added MDL_USEACTORPITCH and MDL_USEACTORROLL flags processing. // If both flags MDL_USEACTORPITCH and MDL_PITCHFROMMOMENTUM are set, the pitch sums up the actor pitch and the velocity vector pitch. if (smf_flags & MDL_USEACTORPITCH) @@ -131,75 +127,116 @@ void RenderModel(FModelRenderer *renderer, float x, float y, float z, FSpriteMod } if (smf_flags & MDL_USEACTORROLL) roll += angles.Roll.Degrees(); + // [Nash] take SpriteRotation into account + angle += actor->SpriteRotation.Degrees(); + + double tic = actor->Level->totaltime; + + if ((ConsoleState == c_up || ConsoleState == c_rising) && (menuactive == MENU_Off || menuactive == MENU_OnNoPause) && !actor->isFrozen()) + { + tic += ticFrac; + } + + return ObjectToWorldMatrix(actor->Level, DVector3(x, y, z), DRotator(DAngle::fromDeg(pitch), DAngle::fromDeg(angle), DAngle::fromDeg(roll)), actor->Scale, smf_flags, tic); +} + +VSMatrix FSpriteModelFrame::ObjectToWorldMatrix(FLevelLocals *Level, DVector3 translation, DRotator rotation, DVector2 scaling, unsigned int flags, double tic) +{ + double rotateOffset = 0; + + if (flags & MDL_ROTATING) + { + if (rotationSpeed > 0.0000000001 || rotationSpeed < -0.0000000001) + { + double turns = (tic) / (200.0 / rotationSpeed); + turns -= floor(turns); + rotateOffset = turns * 360.0; + } + else + { + rotateOffset = 0.0; + } + } + + // y scale for a sprite means height, i.e. z in the world! + float scaleFactorX = scaling.X * xscale; + float scaleFactorY = scaling.X * yscale; + float scaleFactorZ = scaling.Y * zscale; + VSMatrix objectToWorldMatrix; objectToWorldMatrix.loadIdentity(); // Model space => World space - objectToWorldMatrix.translate(x, z, y); - - // [Nash] take SpriteRotation into account - angle += actor->SpriteRotation.Degrees(); + objectToWorldMatrix.translate(translation.X, translation.Z, translation.Y); // consider the pixel stretching. For non-voxels this must be factored out here float stretch = 1.f; // [MK] distortions might happen depending on when the pixel stretch is compensated for // so we make the "undistorted" behavior opt-in - if ((smf_flags & MDL_CORRECTPIXELSTRETCH) && smf->modelIDs.Size() > 0) + if ((flags & MDL_CORRECTPIXELSTRETCH) && modelIDs.Size() > 0) { - stretch = (smf->modelIDs[0] >= 0 ? Models[smf->modelIDs[0]]->getAspectFactor(actor->Level->info->pixelstretch) : 1.f) / actor->Level->info->pixelstretch; + stretch = (modelIDs[0] >= 0 ? Models[modelIDs[0]]->getAspectFactor(Level->info->pixelstretch) : 1.f) / Level->info->pixelstretch; objectToWorldMatrix.scale(1, stretch, 1); } // Applying model transformations: // 1) Applying actor angle, pitch and roll to the model - if (smf_flags & MDL_USEROTATIONCENTER) + if (flags & MDL_USEROTATIONCENTER) { - objectToWorldMatrix.translate(smf->rotationCenterX, smf->rotationCenterZ/stretch, smf->rotationCenterY); - } - objectToWorldMatrix.rotate(-angle, 0, 1, 0); - objectToWorldMatrix.rotate(pitch, 0, 0, 1); - objectToWorldMatrix.rotate(-roll, 1, 0, 0); - if (smf_flags & MDL_USEROTATIONCENTER) - { - objectToWorldMatrix.translate(-smf->rotationCenterX, -smf->rotationCenterZ/stretch, -smf->rotationCenterY); - } + objectToWorldMatrix.translate(rotationCenterX, rotationCenterY/stretch, rotationCenterZ); - // 2) Applying Doomsday like rotation of the weapon pickup models - // The rotation angle is based on the elapsed time. + objectToWorldMatrix.rotate(-rotation.Yaw.Degrees(), 0, 1, 0); + objectToWorldMatrix.rotate(rotation.Pitch.Degrees(), 0, 0, 1); + objectToWorldMatrix.rotate(-rotation.Roll.Degrees(), 1, 0, 0); - if (smf_flags & MDL_ROTATING) + // 2) Applying Doomsday like rotation of the weapon pickup models + // The rotation angle is based on the elapsed time. + if(flags & MDL_ROTATING) + { + objectToWorldMatrix.rotate(rotateOffset, xrotate, yrotate, zrotate); + } + + objectToWorldMatrix.translate(-rotationCenterX, -rotationCenterY/stretch, -rotationCenterZ); + } + else { - objectToWorldMatrix.translate(smf->rotationCenterX, smf->rotationCenterY/stretch, smf->rotationCenterZ); - objectToWorldMatrix.rotate(rotateOffset, smf->xrotate, smf->yrotate, smf->zrotate); - objectToWorldMatrix.translate(-smf->rotationCenterX, -smf->rotationCenterY/stretch, -smf->rotationCenterZ); + objectToWorldMatrix.rotate(-rotation.Yaw.Degrees(), 0, 1, 0); + objectToWorldMatrix.rotate(rotation.Pitch.Degrees(), 0, 0, 1); + objectToWorldMatrix.rotate(-rotation.Roll.Degrees(), 1, 0, 0); + + + // 2) Applying Doomsday like rotation of the weapon pickup models + // The rotation angle is based on the elapsed time. + if(flags & MDL_ROTATING) + { + objectToWorldMatrix.translate(rotationCenterX, rotationCenterY/stretch, rotationCenterZ); + objectToWorldMatrix.rotate(rotateOffset, xrotate, yrotate, zrotate); + objectToWorldMatrix.translate(-rotationCenterX, -rotationCenterY/stretch, -rotationCenterZ); + } } // 3) Scaling model. objectToWorldMatrix.scale(scaleFactorX, scaleFactorZ, scaleFactorY); // 4) Aplying model offsets (model offsets do not depend on model scalings). - objectToWorldMatrix.translate(smf->xoffset / smf->xscale, smf->zoffset / (smf->zscale*stretch), smf->yoffset / smf->yscale); + objectToWorldMatrix.translate(xoffset / xscale, zoffset / (zscale*stretch), yoffset / yscale); // 5) Applying model rotations. - objectToWorldMatrix.rotate(-smf->angleoffset, 0, 1, 0); - objectToWorldMatrix.rotate(smf->pitchoffset, 0, 0, 1); - objectToWorldMatrix.rotate(-smf->rolloffset, 1, 0, 0); + objectToWorldMatrix.rotate(-angleoffset, 0, 1, 0); + objectToWorldMatrix.rotate(pitchoffset, 0, 0, 1); + objectToWorldMatrix.rotate(-rolloffset, 1, 0, 0); - if (!(smf_flags & MDL_CORRECTPIXELSTRETCH) && smf->modelIDs.Size() > 0) + if (!(flags & MDL_CORRECTPIXELSTRETCH) && modelIDs.Size() > 0) { - stretch = (smf->modelIDs[0] >= 0 ? Models[smf->modelIDs[0]]->getAspectFactor(actor->Level->info->pixelstretch) : 1.f) / actor->Level->info->pixelstretch; + stretch = (modelIDs[0] >= 0 ? Models[modelIDs[0]]->getAspectFactor(Level->info->pixelstretch) : 1.f) / Level->info->pixelstretch; objectToWorldMatrix.scale(1, stretch, 1); } - float orientation = scaleFactorX * scaleFactorY * scaleFactorZ; - - renderer->BeginDrawModel(actor->RenderStyle, smf_flags, objectToWorldMatrix, orientation < 0); - RenderFrameModels(renderer, actor->Level, smf, actor->state, actor->tics, translation, actor); - renderer->EndDrawModel(actor->RenderStyle, smf_flags); + return objectToWorldMatrix; } -void RenderHUDModel(FModelRenderer *renderer, DPSprite *psp, FVector3 translation, FVector3 rotation, FVector3 rotation_pivot, FSpriteModelFrame *smf) +void RenderHUDModel(FModelRenderer *renderer, DPSprite *psp, FVector3 translation, FVector3 rotation, FVector3 rotation_pivot, FSpriteModelFrame *smf, double ticFrac) { AActor * playermo = players[consoleplayer].camera; @@ -256,7 +293,7 @@ void RenderHUDModel(FModelRenderer *renderer, DPSprite *psp, FVector3 translatio auto trans = psp->GetTranslation(); if ((psp->Flags & PSPF_PLAYERTRANSLATED)) trans = psp->Owner->mo->Translation; - RenderFrameModels(renderer, playermo->Level, smf, psp->GetState(), psp->GetTics(), trans, psp->Caller); + RenderFrameModels(renderer, playermo->Level, smf, psp->GetState(), psp->GetTics(), ticFrac, trans, psp->Caller); renderer->EndDrawHUDModel(playermo->RenderStyle, smf_flags); } @@ -313,7 +350,7 @@ void calcFrames(const ModelAnim &curAnim, double tic, ModelAnimFrameInterp &to, } } -CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, DActorModelData* data, AActor* actor, bool is_decoupled, double tic) +CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, DActorModelData* data, AActor* actor, bool is_decoupled, double tic, double ticFrac) { // [BB] Frame interpolation: Find the FSpriteModelFrame smfNext which follows after smf in the animation // and the scalar value inter ( element of [0,1) ), both necessary to determine the interpolated frame. @@ -348,8 +385,9 @@ CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame * // [BB] In case the tic counter is frozen we have to leave ticFraction at zero. if ((ConsoleState == c_up || ConsoleState == c_rising) && (menuactive == MENU_Off || menuactive == MENU_OnNoPause) && !Level->isFrozen()) { - ticFraction = I_GetTimeFrac(); + ticFraction = ticFrac; } + inter = static_cast(curState->Tics - curTics + ticFraction) / static_cast(curState->Tics); // [BB] For some actors (e.g. ZPoisonShroom) spr->actor->tics can be bigger than curState->Tics. @@ -413,11 +451,11 @@ bool CalcModelOverrides(int i, const FSpriteModelFrame *smf, DActorModelData* da if (data) { //modelID - if (data->models.Size() > i && data->models[i].modelID >= 0) + if (data->models.SSize() > i && data->models[i].modelID >= 0) { out.modelid = data->models[i].modelID; } - else if(data->models.Size() > i && data->models[i].modelID == -2) + else if(data->models.SSize() > i && data->models[i].modelID == -2) { return false; } @@ -427,7 +465,7 @@ bool CalcModelOverrides(int i, const FSpriteModelFrame *smf, DActorModelData* da } //animationID - if (data->animationIDs.Size() > i && data->animationIDs[i] >= 0) + if (data->animationIDs.SSize() > i && data->animationIDs[i] >= 0) { out.animationid = data->animationIDs[i]; } @@ -438,7 +476,7 @@ bool CalcModelOverrides(int i, const FSpriteModelFrame *smf, DActorModelData* da if(!is_decoupled) { //modelFrame - if (data->modelFrameGenerators.Size() > i + if (data->modelFrameGenerators.SSize() > i && (unsigned)data->modelFrameGenerators[i] < info.modelsamount && smf->modelframes[data->modelFrameGenerators[i]] >= 0 ) { @@ -464,7 +502,7 @@ bool CalcModelOverrides(int i, const FSpriteModelFrame *smf, DActorModelData* da } //skinID - if (data->skinIDs.Size() > i && data->skinIDs[i].isValid()) + if (data->skinIDs.SSize() > i && data->skinIDs[i].isValid()) { out.skinid = data->skinIDs[i]; } @@ -474,7 +512,7 @@ bool CalcModelOverrides(int i, const FSpriteModelFrame *smf, DActorModelData* da } //surfaceSkinIDs - if(data->models.Size() > i && data->models[i].surfaceSkinIDs.Size() > 0) + if(data->models.SSize() > i && data->models[i].surfaceSkinIDs.SSize() > 0) { unsigned sz1 = smf->surfaceskinIDs.Size(); unsigned sz2 = data->models[i].surfaceSkinIDs.Size(); @@ -534,7 +572,7 @@ const TArray * ProcessModelFrame(FModel * animation, bool nextFrame, i frameinfo.decoupled_frame, frameinfo.inter, animationData, - modelData->modelBoneOverrides.Size() > i + modelData->modelBoneOverrides.SSize() > i ? &modelData->modelBoneOverrides[i] : nullptr, out, @@ -552,7 +590,7 @@ const TArray * ProcessModelFrame(FModel * animation, bool nextFrame, i }, -1.0f, animationData, - (modelData && modelData->modelBoneOverrides.Size() > i) + (modelData && modelData->modelBoneOverrides.SSize() > i) ? &modelData->modelBoneOverrides[i] : nullptr, out, @@ -570,7 +608,7 @@ static inline void RenderModelFrame(FModelRenderer *renderer, int i, const FSpri auto ssidp = drawinfo.surfaceskinids.Size() > 0 ? drawinfo.surfaceskinids.Data() - : (((i * MD3_MAX_SURFACES) < smf->surfaceskinIDs.Size()) ? &smf->surfaceskinIDs[i * MD3_MAX_SURFACES] : nullptr); + : (((i * MD3_MAX_SURFACES) < smf->surfaceskinIDs.SSize()) ? &smf->surfaceskinIDs[i * MD3_MAX_SURFACES] : nullptr); bool nextFrame = frameinfo.smfNext && drawinfo.modelframe != drawinfo.modelframenext; @@ -589,19 +627,19 @@ static inline void RenderModelFrame(FModelRenderer *renderer, int i, const FSpri mdl->RenderFrame(renderer, tex, drawinfo.modelframe, nextFrame ? drawinfo.modelframenext : drawinfo.modelframe, nextFrame ? frameinfo.inter : -1.f, translation, ssidp, boneStartingPosition); } -void RenderFrameModels(FModelRenderer *renderer, FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, FTranslationID translation, AActor* actor) +void RenderFrameModels(FModelRenderer *renderer, FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, int curTics, double ticFrac, FTranslationID translation, AActor* actor) { double tic = actor->Level->totaltime; if ((ConsoleState == c_up || ConsoleState == c_rising) && (menuactive == MENU_Off || menuactive == MENU_OnNoPause) && !actor->isFrozen()) { - tic += I_GetTimeFrac(); + tic += ticFrac; } bool is_decoupled = (actor->flags9 & MF9_DECOUPLEDANIMATIONS); DActorModelData* modelData = actor ? actor->modelData.ForceGet() : nullptr; - CalcModelFrameInfo frameinfo = CalcModelFrame(Level, smf, curState, curTics, modelData, actor, is_decoupled, tic); + CalcModelFrameInfo frameinfo = CalcModelFrame(Level, smf, curState, curTics, modelData, actor, is_decoupled, tic, ticFrac); ModelDrawInfo drawinfo; int boneStartingPosition = -1; diff --git a/src/r_data/models.h b/src/r_data/models.h index a62d59333..d421194e3 100644 --- a/src/r_data/models.h +++ b/src/r_data/models.h @@ -115,7 +115,7 @@ void BSPWalkCircle(FLevelLocals *Level, float x, float y, float radiusSquared, c } void RenderModel(FModelRenderer* renderer, float x, float y, float z, FSpriteModelFrame* smf, AActor* actor, double ticFrac); -void RenderHUDModel(FModelRenderer* renderer, DPSprite* psp, FVector3 translation, FVector3 rotation, FVector3 rotation_pivot, FSpriteModelFrame *smf); +void RenderHUDModel(FModelRenderer* renderer, DPSprite* psp, FVector3 translation, FVector3 rotation, FVector3 rotation_pivot, FSpriteModelFrame *smf, double ticFrac); struct CalcModelFrameInfo { @@ -140,7 +140,7 @@ struct ModelDrawInfo class DActorModelData; -CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, const int curTics, DActorModelData* modelData, AActor* actor, bool is_decoupled, double tic); +CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame *smf, const FState *curState, int curTics, DActorModelData* modelData, AActor* actor, bool is_decoupled, double tic, double ticFrac); // returns true if the model isn't removed bool CalcModelOverrides(int modelindex, const FSpriteModelFrame *smf, DActorModelData* modelData, const CalcModelFrameInfo &frameinfo, ModelDrawInfo &drawinfo, bool is_decoupled); diff --git a/src/rendering/hwrenderer/scene/hw_weapon.cpp b/src/rendering/hwrenderer/scene/hw_weapon.cpp index 207f9fb69..8067e6524 100644 --- a/src/rendering/hwrenderer/scene/hw_weapon.cpp +++ b/src/rendering/hwrenderer/scene/hw_weapon.cpp @@ -91,7 +91,7 @@ void HWDrawInfo::DrawPSprite(HUDSprite *huds, FRenderState &state) state.AlphaFunc(Alpha_GEqual, 0); FHWModelRenderer renderer(this, state, huds->lightindex); - RenderHUDModel(&renderer, huds->weapon, huds->translation, huds->rotation + FVector3(huds->mx / 4., (huds->my - WEAPONTOP) / -4., 0), huds->pivot, huds->mframe); + RenderHUDModel(&renderer, huds->weapon, huds->translation, huds->rotation + FVector3(huds->mx / 4., (huds->my - WEAPONTOP) / -4., 0), huds->pivot, huds->mframe, Viewpoint.TicFrac); state.SetVertexBuffer(screen->mVertexData); } else diff --git a/src/rendering/r_utility.cpp b/src/rendering/r_utility.cpp index 035ac6de7..adc3df2f1 100644 --- a/src/rendering/r_utility.cpp +++ b/src/rendering/r_utility.cpp @@ -68,7 +68,7 @@ #include "i_interface.h" #include "d_main.h" -const float MY_SQRT2 = 1.41421356237309504880; // sqrt(2) +const float MY_SQRT2 = float(1.41421356237309504880); // sqrt(2) // EXTERNAL DATA DECLARATIONS ---------------------------------------------- extern bool DrawFSHUD; // [RH] Defined in d_main.cpp diff --git a/src/win32/i_steam.cpp b/src/win32/i_steam.cpp index 2cc76ab66..0c0512b07 100644 --- a/src/win32/i_steam.cpp +++ b/src/win32/i_steam.cpp @@ -339,7 +339,7 @@ TArray I_GetSteamPath() } } } - catch (const CRecoverableError& err) + catch (const CRecoverableError&) { // don't abort on errors in here. Just return an empty path. } diff --git a/tools/lemon/lemon.c b/tools/lemon/lemon.c index 45f458d72..2c229ba08 100644 --- a/tools/lemon/lemon.c +++ b/tools/lemon/lemon.c @@ -416,7 +416,7 @@ static int actioncmp(void *_ap1,void *_ap2) rc = ap1->x.rp->index - ap2->x.rp->index; } if( rc==0 ){ - rc = ap2 - ap1; + rc = (int)(ap2 - ap1); } return rc; } @@ -1957,7 +1957,7 @@ static int handleswitch(int i, FILE *err) if( err ){ fprintf(err, "%sillegal character in floating-point argument.\n",emsg); - errline(i,((size_t)end)-(size_t)argv[i],err); + errline(i,(int)(((size_t)end)-(size_t)argv[i]),err); } errcnt++; } @@ -1968,7 +1968,7 @@ static int handleswitch(int i, FILE *err) if( *end ){ if( err ){ fprintf(err,"%sillegal character in integer argument.\n",emsg); - errline(i,((size_t)end)-(size_t)argv[i],err); + errline(i,(int)(((size_t)end)-(size_t)argv[i]),err); } errcnt++; } diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 1598ee697..4ce112424 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1442,17 +1442,67 @@ class Actor : Thinker native native version("4.15.1") void GetBoneChildren(int boneIndex, out Array children); native version("4.15.1") void GetNamedBoneChildren(Name boneName, out Array children); - - // this is the length in model units, not in world units, and does not take model scale in MODELDEF, world, etc, or current animation or offset into account at all - native version("4.15.1") double GetBoneLength(int boneIndex); - native version("4.15.1") double GetNamedBoneLength(Name boneName); - - // this is the direction of the bone in the armature, does not take the current animation or offset into account at all - native version("4.15.1") Vector3 GetBoneDir(int boneIndex); - native version("4.15.1") Vector3 GetNamedBoneDir(Name boneName); + /* rotation, translation, scaling */ + native version("4.15.1") Quat, Vector3, Vector3 GetBoneBaseTRS(int boneIndex); + native version("4.15.1") Quat, Vector3, Vector3 GetNamedBoneBaseTRS(Name boneName); + + native version("4.15.1") Vector3 GetBoneBasePosition(int boneIndex); + native version("4.15.1") Vector3 GetNamedBoneBasePosition(Name boneName); + + native version("4.15.1") Quat GetBoneBaseRotation(int boneIndex); + native version("4.15.1") Quat GetNamedBoneBaseRotation(Name boneName); + native version("4.15.1") int GetBoneCount(); + //================================================ + // + // Bone Pose Getters + // + //================================================ + + native version("4.15.1") int GetAnimStartFrame(Name animName); + native version("4.15.1") int GetAnimEndFrame(Name animName); + native version("4.15.1") double GetAnimFramerate(Name animName); + + /* rotation, translation, scaling */ + native version("4.15.1") Quat, Vector3, Vector3 GetBoneFramePose(int boneIndex, int frame); + native version("4.15.1") Quat, Vector3, Vector3 GetNamedBoneFramePose(Name boneName, int frame); + + //================================================ + // + // Bone TRS Getters + // + //================================================ + + /* rotation, translation, scaling */ + native version("4.15.1") Quat, Vector3, Vector3 GetBone(int boneIndex, bool include_offsets = true); + native version("4.15.1") Quat, Vector3, Vector3 GetNamedBone(Name boneName, bool include_offsets = true); + + native version("4.15.1") Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); + native version("4.15.1") Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); + + version("4.15.1") Vector3 GetBonePosition(int boneIndex, bool include_offsets = true) + { + return TransformByBone(boneIndex, GetBoneBasePosition(boneIndex), include_offsets:include_offsets); + } + + version("4.15.1") Vector3 GetNamedBonePosition(name boneName, bool include_offsets = true) + { + return GetBonePosition(GetBoneIndex(boneName), include_offsets); + } + + //================================================ + // + // Bone Matrix Getters + // + //================================================ + + //outMatrix will be a 16-length array containing the raw matrix data + native version("4.15.1") void GetBoneMatrixRaw(int boneIndex, out Array outMatrix, bool include_offsets = true); + native version("4.15.1") void GetNamedBoneMatrixRaw(Name boneName, out Array outMatrix, bool include_offsets = true); + + native version("4.15.1") void GetObjectToWorldMatrixRaw(out Array outMatrix); //================================================ // @@ -1460,16 +1510,15 @@ class Actor : Thinker native // //================================================ - native version("4.12") void SetAnimation(Name animName, double framerate = -1, int startFrame = -1, int loopFrame = -1, int endFrame = -1, int interpolateTics = -1, int flags = 0); native version("4.12") ui void SetAnimationUI(Name animName, double framerate = -1, int startFrame = -1, int loopFrame = -1, int endFrame = -1, int interpolateTics = -1, int flags = 0); native version("4.12") void SetAnimationFrameRate(double framerate); native version("4.12") ui void SetAnimationFrameRateUI(double framerate); - native version("4.12") void SetModelFlag(int flag); - native version("4.12") void ClearModelFlag(int flag); - native version("4.12") void ResetModelFlags(); + native version("4.12") void SetModelFlag(int flag, int iqmFlags = 0); + native version("4.12") void ClearModelFlag(int flag, int iqmFlags = 0); + native version("4.12") void ResetModelFlags(bool resetModel = true, bool resetIqm = false); action version("4.12") void A_SetAnimation(Name animName, double framerate = -1, int startFrame = -1, int loopFrame = -1, int endFrame = -1, int interpolateTics = -1, int flags = 0) diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index 5c1480e62..d2122f9fe 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1554,3 +1554,10 @@ enum ESetBoneMode SB_ADD = 1, SB_REPLACE = 2, }; + +enum EIQMFlags +{ + IQM_GET_BONE_INFO = 1 << 2, + IQM_GET_BONE_INFO_RECALC = 1 << 3, // RECALCULATE BONE INFO INSTANTLY WHEN STATE/ANIMATION CHANGES, MIGHT GET EXPENSIVE +}; + From 78770184715d26771f665facb48d5f2eac1a1875 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 11 Apr 2025 14:24:31 -0400 Subject: [PATCH 158/384] Added CRC to packets Verify that data is correctly ordered and reject packets that aren't. Also generates a random game id to ensure packets are coming from legitimate clients. --- src/common/engine/i_net.cpp | 97 ++++++++++++++++++++++++++----------- src/common/engine/i_net.h | 3 +- src/d_net.cpp | 3 -- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 7d9a9674f..8a25a2718 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -78,6 +78,7 @@ #include "c_cvars.h" #include "version.h" #include "i_net.h" +#include "m_random.h" /* [Petteri] Get more portable: */ #ifndef __WIN32__ @@ -162,13 +163,14 @@ int consoleplayer = 0; int Net_Arbitrator = 0; FClientStack NetworkClients = {}; -uint32_t GameID = DEFAULT_GAME_ID; +uint64_t GameID = 0u; uint8_t TicDup = 1u; int MaxClients = 1; int RemoteClient = -1; size_t NetBufferLength = 0u; uint8_t NetBuffer[MAX_MSGLEN] = {}; +static FRandom GameIDGen = {}; static u_short GamePort = (IPPORT_USERRESERVED + 29); static SOCKET MySocket = INVALID_SOCKET; static FConnection Connected[MAXPLAYERS] = {}; @@ -483,22 +485,34 @@ static void SendPacket(const sockaddr_in& to) assert(!(NetBuffer[0] & NCMD_COMPRESSED)); - uLong size = MaxTransmitSize - 1u; - int res = -1; + uint8_t* dataStart = &TransmitBuffer[4]; + uLong size = MaxTransmitSize - 5u; if (NetBufferLength >= MinCompressionSize) { - TransmitBuffer[0] = NetBuffer[0] | NCMD_COMPRESSED; + *dataStart = NetBuffer[0] | NCMD_COMPRESSED; + const int res = compress2(dataStart + 1, &size, NetBuffer + 1, NetBufferLength - 1u, 9); + if (res != Z_OK) + I_Error("Net compression failed (zlib error %d)", res); - res = compress2(TransmitBuffer + 1, &size, NetBuffer + 1, uint32_t(NetBufferLength - 1u), 9); ++size; } - - if (res == Z_OK && size < static_cast(NetBufferLength)) - res = sendto(MySocket, (const char*)TransmitBuffer, (int)size, 0, (const sockaddr*)&to, sizeof(to)); - else if (NetBufferLength > MaxTransmitSize) - I_Error("Net compression failed (zlib error %d)", res); else - res = sendto(MySocket, (const char*)NetBuffer, (int)NetBufferLength, 0, (const sockaddr*)&to, sizeof(to)); + { + memcpy(dataStart, NetBuffer, NetBufferLength); + size = NetBufferLength; + } + + if (size + 4 > MaxTransmitSize) + I_Error("Failed to compress data down to acceptable transmission size"); + + // If a connection packet, don't check the game id since they might not have it yet. + const uint32_t crc = (NetBuffer[0] & NCMD_SETUP) ? CalcCRC32(dataStart, size) : AddCRC32(CalcCRC32(dataStart, size), (uint8_t*)&GameID, sizeof(GameID)); + TransmitBuffer[0] = crc >> 24; + TransmitBuffer[1] = crc >> 16; + TransmitBuffer[2] = crc >> 8; + TransmitBuffer[3] = crc; + + sendto(MySocket, (const char*)TransmitBuffer, size + 4, 0, (const sockaddr*)&to, sizeof(to)); } static void GetPacket(sockaddr_in* const from = nullptr) @@ -544,7 +558,8 @@ static void GetPacket(sockaddr_in* const from = nullptr) } else if (msgSize > 0) { - if (client == -1 && !(TransmitBuffer[0] & NCMD_SETUP)) + const uint8_t* dataStart = &TransmitBuffer[4]; + if (client == -1 && !(*dataStart & NCMD_SETUP)) { msgSize = 0; } @@ -558,25 +573,37 @@ static void GetPacket(sockaddr_in* const from = nullptr) } else { - NetBuffer[0] = (TransmitBuffer[0] & ~NCMD_COMPRESSED); - if (TransmitBuffer[0] & NCMD_COMPRESSED) + const uint32_t check = (*dataStart & NCMD_SETUP) ? CalcCRC32(dataStart, msgSize - 4) : AddCRC32(CalcCRC32(dataStart, msgSize - 4), (uint8_t*)GameID, sizeof(GameID)); + const uint32_t crc = (TransmitBuffer[0] << 24) | (TransmitBuffer[1] << 16) | (TransmitBuffer[2] << 8) | TransmitBuffer[3]; + if (check != crc) { - uLongf size = MAX_MSGLEN - 1; - int err = uncompress(NetBuffer + 1, &size, TransmitBuffer + 1, msgSize - 1); - if (err != Z_OK) - { - Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars()); - client = -1; - msgSize = 0; - } - else - { - msgSize = size + 1; - } + DPrintf(DMSG_NOTIFY, "Checksum on packet failed: expected %u, got %u", check, crc); + client = -1; + msgSize = 0; } else { - memcpy(NetBuffer + 1, TransmitBuffer + 1, msgSize - 1); + NetBuffer[0] = (*dataStart & ~NCMD_COMPRESSED); + if (*dataStart & NCMD_COMPRESSED) + { + uLongf size = MAX_MSGLEN - 1; + int err = uncompress(NetBuffer + 1, &size, dataStart + 1, msgSize - 5); + if (err != Z_OK) + { + Printf("Net decompression failed (zlib error %s)\n", M_ZLibError(err).GetChars()); + client = -1; + msgSize = 0; + } + else + { + msgSize = size + 1; + } + } + else + { + msgSize -= 4; + memcpy(NetBuffer + 1, dataStart + 1, msgSize - 1); + } } } } @@ -847,7 +874,15 @@ static bool Host_CheckForConnections(void* connected) { NetBuffer[1] = PRE_GAME_INFO; NetBuffer[2] = TicDup; - NetBufferLength = 3u; + NetBuffer[3] = GameID >> 56; + NetBuffer[4] = GameID >> 48; + NetBuffer[5] = GameID >> 40; + NetBuffer[6] = GameID >> 32; + NetBuffer[7] = GameID >> 24; + NetBuffer[8] = GameID >> 16; + NetBuffer[9] = GameID >> 8; + NetBuffer[10] = GameID; + NetBufferLength = 11u; uint8_t* stream = &NetBuffer[NetBufferLength]; NetBufferLength += Net_SetGameInfo(stream); @@ -931,6 +966,7 @@ static bool HostGame(int arg, bool forcedNetMode) if (MaxClients > MAXPLAYERS) I_FatalError("Cannot host a game with %u players. The limit is currently %u", MaxClients, MAXPLAYERS); + GameID = GameIDGen.GenRand64(); NetworkClients += 0; Connected[consoleplayer].Status = CSTAT_READY; Net_SetupUserInfo(); @@ -1092,7 +1128,9 @@ static bool Guest_ContactHost(void* unused) if (!Connected[consoleplayer].bHasGameInfo) { TicDup = clamp(NetBuffer[2], 1, MAXTICDUP); - uint8_t* stream = &NetBuffer[3]; + GameID = ((uint64_t)NetBuffer[3] << 56) | ((uint64_t)NetBuffer[4] << 48) | ((uint64_t)NetBuffer[5] << 40) | ((uint64_t)NetBuffer[6] << 32) + | ((uint64_t)NetBuffer[7] << 24) | ((uint64_t)NetBuffer[8] << 16) | ((uint64_t)NetBuffer[9] << 8) | (uint64_t)NetBuffer[10]; + uint8_t* stream = &NetBuffer[11]; Net_ReadGameInfo(stream); Connected[consoleplayer].bHasGameInfo = true; } @@ -1255,6 +1293,7 @@ bool I_InitNetwork() else { // single player game + GameID = GameIDGen.GenRand64(); TicDup = 1; NetworkClients += 0; Connected[0].Status = CSTAT_READY; diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h index 64c781309..cd2b7d821 100644 --- a/src/common/engine/i_net.h +++ b/src/common/engine/i_net.h @@ -8,7 +8,6 @@ inline constexpr size_t MAXPLAYERS = 64u; enum ENetConstants { - DEFAULT_GAME_ID = 0x12345678, BACKUPTICS = 35 * 5, // Remember up to 5 seconds of data. MAXTICDUP = 3, MAXSENDTICS = 35 * 1, // Only send up to 1 second of data at a time. @@ -67,7 +66,7 @@ extern size_t NetBufferLength; extern uint8_t TicDup; extern int RemoteClient; extern int MaxClients; -extern uint32_t GameID; +extern uint64_t GameID; bool I_InitNetwork(); void I_ClearClient(size_t client); diff --git a/src/d_net.cpp b/src/d_net.cpp index 31e489c2b..dccc796d6 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1695,9 +1695,6 @@ bool D_CheckNetGame() if (!I_InitNetwork()) return false; - if (GameID != DEFAULT_GAME_ID) - I_FatalError("Invalid id set for network buffer"); - if (Args->CheckParm("-extratic")) net_extratic = true; From 58809a36890067a05f2cff1df4646b877815655b Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 11 Apr 2025 15:42:04 -0400 Subject: [PATCH 159/384] Fixed up game id Store it in a proper buffer. --- src/common/engine/i_net.cpp | 28 +++++++++++++--------------- src/common/engine/i_net.h | 2 +- src/d_main.cpp | 1 - src/d_net.cpp | 3 +++ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 8a25a2718..773f8db92 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -163,7 +163,6 @@ int consoleplayer = 0; int Net_Arbitrator = 0; FClientStack NetworkClients = {}; -uint64_t GameID = 0u; uint8_t TicDup = 1u; int MaxClients = 1; int RemoteClient = -1; @@ -171,6 +170,7 @@ size_t NetBufferLength = 0u; uint8_t NetBuffer[MAX_MSGLEN] = {}; static FRandom GameIDGen = {}; +static uint8_t GameID[8] = {}; static u_short GamePort = (IPPORT_USERRESERVED + 29); static SOCKET MySocket = INVALID_SOCKET; static FConnection Connected[MAXPLAYERS] = {}; @@ -348,6 +348,12 @@ static bool ClientsOnSameNetwork() return true; } +static void GenerateGameID() +{ + const uint64_t val = GameIDGen.GenRand64(); + memcpy(GameID, &val, sizeof(val)); +} + // Print a network-related message to the console. This doesn't print to the window so should // not be used for that and is mainly for logging. static void I_NetLog(const char* text, ...) @@ -506,7 +512,7 @@ static void SendPacket(const sockaddr_in& to) I_Error("Failed to compress data down to acceptable transmission size"); // If a connection packet, don't check the game id since they might not have it yet. - const uint32_t crc = (NetBuffer[0] & NCMD_SETUP) ? CalcCRC32(dataStart, size) : AddCRC32(CalcCRC32(dataStart, size), (uint8_t*)&GameID, sizeof(GameID)); + const uint32_t crc = (NetBuffer[0] & NCMD_SETUP) ? CalcCRC32(dataStart, size) : AddCRC32(CalcCRC32(dataStart, size), GameID, std::extent_v); TransmitBuffer[0] = crc >> 24; TransmitBuffer[1] = crc >> 16; TransmitBuffer[2] = crc >> 8; @@ -573,7 +579,7 @@ static void GetPacket(sockaddr_in* const from = nullptr) } else { - const uint32_t check = (*dataStart & NCMD_SETUP) ? CalcCRC32(dataStart, msgSize - 4) : AddCRC32(CalcCRC32(dataStart, msgSize - 4), (uint8_t*)GameID, sizeof(GameID)); + const uint32_t check = (*dataStart & NCMD_SETUP) ? CalcCRC32(dataStart, msgSize - 4) : AddCRC32(CalcCRC32(dataStart, msgSize - 4), GameID, std::extent_v); const uint32_t crc = (TransmitBuffer[0] << 24) | (TransmitBuffer[1] << 16) | (TransmitBuffer[2] << 8) | TransmitBuffer[3]; if (check != crc) { @@ -874,14 +880,7 @@ static bool Host_CheckForConnections(void* connected) { NetBuffer[1] = PRE_GAME_INFO; NetBuffer[2] = TicDup; - NetBuffer[3] = GameID >> 56; - NetBuffer[4] = GameID >> 48; - NetBuffer[5] = GameID >> 40; - NetBuffer[6] = GameID >> 32; - NetBuffer[7] = GameID >> 24; - NetBuffer[8] = GameID >> 16; - NetBuffer[9] = GameID >> 8; - NetBuffer[10] = GameID; + memcpy(&NetBuffer[3], GameID, 8); NetBufferLength = 11u; uint8_t* stream = &NetBuffer[NetBufferLength]; @@ -966,7 +965,7 @@ static bool HostGame(int arg, bool forcedNetMode) if (MaxClients > MAXPLAYERS) I_FatalError("Cannot host a game with %u players. The limit is currently %u", MaxClients, MAXPLAYERS); - GameID = GameIDGen.GenRand64(); + GenerateGameID(); NetworkClients += 0; Connected[consoleplayer].Status = CSTAT_READY; Net_SetupUserInfo(); @@ -1128,8 +1127,7 @@ static bool Guest_ContactHost(void* unused) if (!Connected[consoleplayer].bHasGameInfo) { TicDup = clamp(NetBuffer[2], 1, MAXTICDUP); - GameID = ((uint64_t)NetBuffer[3] << 56) | ((uint64_t)NetBuffer[4] << 48) | ((uint64_t)NetBuffer[5] << 40) | ((uint64_t)NetBuffer[6] << 32) - | ((uint64_t)NetBuffer[7] << 24) | ((uint64_t)NetBuffer[8] << 16) | ((uint64_t)NetBuffer[9] << 8) | (uint64_t)NetBuffer[10]; + memcpy(GameID, &NetBuffer[3], 8); uint8_t* stream = &NetBuffer[11]; Net_ReadGameInfo(stream); Connected[consoleplayer].bHasGameInfo = true; @@ -1293,7 +1291,7 @@ bool I_InitNetwork() else { // single player game - GameID = GameIDGen.GenRand64(); + GenerateGameID(); TicDup = 1; NetworkClients += 0; Connected[0].Status = CSTAT_READY; diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h index cd2b7d821..6e92e945f 100644 --- a/src/common/engine/i_net.h +++ b/src/common/engine/i_net.h @@ -66,12 +66,12 @@ extern size_t NetBufferLength; extern uint8_t TicDup; extern int RemoteClient; extern int MaxClients; -extern uint64_t GameID; bool I_InitNetwork(); void I_ClearClient(size_t client); void I_NetCmd(ENetCommand cmd); void I_NetDone(); void HandleIncomingConnection(); +void CloseNetwork(); #endif diff --git a/src/d_main.cpp b/src/d_main.cpp index d0df8d06f..28e96e5a6 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -150,7 +150,6 @@ extern void M_SetDefaultMode (); extern void G_NewInit (); extern void SetupPlayerClasses (); void DeinitMenus(); -void CloseNetwork(); void P_Shutdown(); void M_SaveDefaultsFinal(); void R_Shutdown(); diff --git a/src/d_net.cpp b/src/d_net.cpp index dccc796d6..b44bdbafd 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -324,6 +324,8 @@ public: void Net_ClearBuffers() { + CloseNetwork(); + for (int i = 0; i < MAXPLAYERS; ++i) { playeringame[i] = false; @@ -361,6 +363,7 @@ void Net_ClearBuffers() gametic = ClientTic = 0; SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; NetEvents.ResetStream(); + bCommandsReset = false; LevelStartAck = 0u; LevelStartDelay = LevelStartDebug = 0; From 385033e999149b9c40b3d8ef29f18ca1b3a1f425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sat, 10 May 2025 18:21:21 -0300 Subject: [PATCH 160/384] fix CalcModelFrame/SetBone/GetBone/SetAnimation not grabbing smf from modeldata for decoupled anims --- src/playsim/p_actionfunctions.cpp | 16 ++++++++++------ src/r_data/models.cpp | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/playsim/p_actionfunctions.cpp b/src/playsim/p_actionfunctions.cpp index be5463bae..bfa2dcc16 100644 --- a/src/playsim/p_actionfunctions.cpp +++ b/src/playsim/p_actionfunctions.cpp @@ -5133,7 +5133,9 @@ FModel * SetGetBoneShared(AActor * self, int model_index) ThrowAbortException(X_OTHER, isSet ? "Cannot set bone offset for non-decoupled actors" : (isOffset ? "Cannot get bone for non-decoupled actors" : "Cannot get bone offset for non-decoupled actors")); } - if(!BaseSpriteModelFrames.CheckKey(self->GetClass())) + auto smf_class = (self->modelData && self->modelData->modelDef) ? self->modelData->modelDef : self->GetClass(); + + if(!BaseSpriteModelFrames.CheckKey(smf_class)) { ThrowAbortException(X_OTHER, "Actor class is missing a MODELDEF definition or a MODELDEF BaseFrame"); } @@ -5144,9 +5146,9 @@ FModel * SetGetBoneShared(AActor * self, int model_index) { return Models[self->modelData->models[model_index].modelID]; } - else if(BaseSpriteModelFrames[self->GetClass()].modelIDs.SSize() > model_index) + else if(BaseSpriteModelFrames[smf_class].modelIDs.SSize() > model_index) { - return Models[BaseSpriteModelFrames[self->GetClass()].modelIDs[model_index]]; + return Models[BaseSpriteModelFrames[smf_class].modelIDs[model_index]]; } else { @@ -6193,7 +6195,9 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s ThrowAbortException(X_OTHER, "Cannot set animation for non-decoupled actors"); } - if(!BaseSpriteModelFrames.CheckKey(self->GetClass())) + auto smf_class = (self->modelData && self->modelData->modelDef) ? self->modelData->modelDef : self->GetClass(); + + if(!BaseSpriteModelFrames.CheckKey(smf_class)) { ThrowAbortException(X_OTHER, "Actor class is missing a MODELDEF definition or a MODELDEF BaseFrame"); } @@ -6227,7 +6231,7 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s } else { - animID = BaseSpriteModelFrames[self->GetClass()].animationIDs[0]; + animID = BaseSpriteModelFrames[smf_class].animationIDs[0]; } FModel * animation = nullptr; @@ -6241,7 +6245,7 @@ void SetAnimationInternal(AActor * self, FName animName, double framerate, int s } else { - animation = Models[BaseSpriteModelFrames[self->GetClass()].modelIDs[0]]; + animation = Models[BaseSpriteModelFrames[smf_class].modelIDs[0]]; } int animStart = animation->FindFirstFrame(animName); diff --git a/src/r_data/models.cpp b/src/r_data/models.cpp index 34693c20a..dc2ec93c5 100644 --- a/src/r_data/models.cpp +++ b/src/r_data/models.cpp @@ -368,7 +368,7 @@ CalcModelFrameInfo CalcModelFrame(FLevelLocals *Level, const FSpriteModelFrame * if(is_decoupled) { - smfNext = smf = &BaseSpriteModelFrames[actor->GetClass()]; + smfNext = smf = &BaseSpriteModelFrames[(data != nullptr && data->modelDef != nullptr) ? data->modelDef : actor->GetClass()]; if(data && !(data->curAnim.flags & MODELANIM_NONE)) { calcFrames(data->curAnim, tic, decoupled_frame, inter); From 501a21869c8380e0f390234e119f3fac2652314f Mon Sep 17 00:00:00 2001 From: biwa <6475593+biwa@users.noreply.github.com> Date: Sat, 10 May 2025 13:58:48 +0200 Subject: [PATCH 161/384] Fixed issue where seams can be seen when using animated fire textures --- src/common/textures/firetexture.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/textures/firetexture.cpp b/src/common/textures/firetexture.cpp index 4a89622f7..d470c0c9c 100644 --- a/src/common/textures/firetexture.cpp +++ b/src/common/textures/firetexture.cpp @@ -69,9 +69,9 @@ void FireTexture::SetPalette(TArray& colors) void FireTexture::Update() { - for (unsigned int y = 1; y < Height; y++) + for (unsigned int x = 0; x < Width; x++) { - for (unsigned int x = 0; x < Width; x++) + for (unsigned int y = 1; y < Height; y++) { uint8_t srcPixel = Image[y * Width + x]; From 0532a298d20034dbed38137af8aa46a11693ecf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 11 May 2025 01:12:55 -0300 Subject: [PATCH 162/384] fix MDL_USEROTATIONCENTER --- src/r_data/models.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/r_data/models.cpp b/src/r_data/models.cpp index dc2ec93c5..166387cd9 100644 --- a/src/r_data/models.cpp +++ b/src/r_data/models.cpp @@ -184,7 +184,7 @@ VSMatrix FSpriteModelFrame::ObjectToWorldMatrix(FLevelLocals *Level, DVector3 tr // 1) Applying actor angle, pitch and roll to the model if (flags & MDL_USEROTATIONCENTER) { - objectToWorldMatrix.translate(rotationCenterX, rotationCenterY/stretch, rotationCenterZ); + objectToWorldMatrix.translate(rotationCenterX, rotationCenterZ/stretch, rotationCenterY); objectToWorldMatrix.rotate(-rotation.Yaw.Degrees(), 0, 1, 0); objectToWorldMatrix.rotate(rotation.Pitch.Degrees(), 0, 0, 1); @@ -197,7 +197,7 @@ VSMatrix FSpriteModelFrame::ObjectToWorldMatrix(FLevelLocals *Level, DVector3 tr objectToWorldMatrix.rotate(rotateOffset, xrotate, yrotate, zrotate); } - objectToWorldMatrix.translate(-rotationCenterX, -rotationCenterY/stretch, -rotationCenterZ); + objectToWorldMatrix.translate(-rotationCenterX, -rotationCenterZ/stretch, -rotationCenterY); } else { @@ -210,9 +210,9 @@ VSMatrix FSpriteModelFrame::ObjectToWorldMatrix(FLevelLocals *Level, DVector3 tr // The rotation angle is based on the elapsed time. if(flags & MDL_ROTATING) { - objectToWorldMatrix.translate(rotationCenterX, rotationCenterY/stretch, rotationCenterZ); + objectToWorldMatrix.translate(rotationCenterX, rotationCenterZ/stretch, rotationCenterY); objectToWorldMatrix.rotate(rotateOffset, xrotate, yrotate, zrotate); - objectToWorldMatrix.translate(-rotationCenterX, -rotationCenterY/stretch, -rotationCenterZ); + objectToWorldMatrix.translate(-rotationCenterX, -rotationCenterZ/stretch, -rotationCenterY); } } From 1f1d9dc1b5ffaf605c0e506952497a8809ddab88 Mon Sep 17 00:00:00 2001 From: XLightningStormL Date: Wed, 14 May 2025 09:48:52 +1000 Subject: [PATCH 163/384] Fixed a major bug Fixed a potential loop crash caused by an undefined amount int in TakeInventory (and derivatives) now it will assume if by is less than 1 that the item is to be destroyed. --- wadsrc/static/zscript/actors/inventory/inventory.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/actors/inventory/inventory.zs b/wadsrc/static/zscript/actors/inventory/inventory.zs index 10ee3a5f2..986bf786d 100644 --- a/wadsrc/static/zscript/actors/inventory/inventory.zs +++ b/wadsrc/static/zscript/actors/inventory/inventory.zs @@ -932,7 +932,7 @@ class Inventory : Actor //=========================================================================== virtual void DepleteBy(int by) { - if (amount < 1 || by >= amount) + if (by < 1 || amount < 1 || by >= amount) { DepleteOrDestroy(); } From f4ac616b5775877fe4c29d53bf4af8d846046d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 14 May 2025 14:58:35 -0300 Subject: [PATCH 164/384] save extraArgs to CVar for easier testing --- src/d_iwad.cpp | 6 +++++- src/launcher/launcherwindow.cpp | 1 + src/launcher/playgamepage.cpp | 5 +++++ src/launcher/playgamepage.h | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/d_iwad.cpp b/src/d_iwad.cpp index 589b2fbb4..54fba56d7 100644 --- a/src/d_iwad.cpp +++ b/src/d_iwad.cpp @@ -584,6 +584,8 @@ FString FIWadManager::IWADPathFileSearch(const FString &file) return ""; } +CVAR(String, extra_args, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG); + int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char *iwad, const char *zdoom_wad, const char *optional_wad) { const char *iwadparm = Args->CheckValue ("-iwad"); @@ -791,13 +793,15 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char if (autoloadbrightmaps) flags |= 4; if (autoloadwidescreen) flags |= 8; - FString extraArgs; + FString extraArgs = *extra_args; pick = I_PickIWad(&wads[0], (int)wads.Size(), queryiwad, pick, flags, extraArgs); if (pick >= 0) { extraArgs.StripLeftRight(); + extra_args = extraArgs.GetChars(); + if(extraArgs.Len() > 0) { Args->AppendArgsString(extraArgs); diff --git a/src/launcher/launcherwindow.cpp b/src/launcher/launcherwindow.cpp index 860d1a109..11f1079d4 100644 --- a/src/launcher/launcherwindow.cpp +++ b/src/launcher/launcherwindow.cpp @@ -19,6 +19,7 @@ int LauncherWindow::ExecModal(WadStuff* wads, int numwads, int defaultiwad, int* auto launcher = std::make_unique(wads, numwads, defaultiwad, autoloadflags); launcher->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight); + if(extraArgs) launcher->PlayGame->SetExtraArgs(extraArgs->GetChars()); launcher->Show(); DisplayWindow::RunLoop(); diff --git a/src/launcher/playgamepage.cpp b/src/launcher/playgamepage.cpp index 46bd16a92..d561a672b 100644 --- a/src/launcher/playgamepage.cpp +++ b/src/launcher/playgamepage.cpp @@ -45,6 +45,11 @@ std::string PlayGamePage::GetExtraArgs() return ParametersEdit->GetText(); } +void PlayGamePage::SetExtraArgs(const std::string& args) +{ + ParametersEdit->SetText(args); +} + int PlayGamePage::GetSelectedGame() { return GamesList->GetSelectedItem(); diff --git a/src/launcher/playgamepage.h b/src/launcher/playgamepage.h index af0e95bc3..d54ba2ca1 100644 --- a/src/launcher/playgamepage.h +++ b/src/launcher/playgamepage.h @@ -14,6 +14,7 @@ public: PlayGamePage(LauncherWindow* launcher, WadStuff* wads, int numwads, int defaultiwad); void UpdateLanguage(); + void SetExtraArgs(const std::string& args); std::string GetExtraArgs(); int GetSelectedGame(); From c6a6ae23a6209467c6417c38230aa25673d44ca5 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Wed, 14 May 2025 21:34:36 -0600 Subject: [PATCH 165/384] ThickFogDistance and ThickFogMultiplier MAPINFO variables adds thicker fog (Vulkan and OpenGL only) beyond ThickFogDistance, as long as it is possible. But default it is -1.f (disabled). --- src/common/rendering/gl/gl_shader.cpp | 3 ++ .../rendering/gles/gles_renderstate.cpp | 2 ++ src/common/rendering/gles/gles_shader.cpp | 5 ++++ src/common/rendering/gles/gles_shader.h | 3 ++ .../hwrenderer/data/hw_viewpointuniforms.h | 3 ++ .../rendering/vulkan/shaders/vk_shader.cpp | 3 ++ src/console/c_cmds.cpp | 30 +++++++++++++++++++ src/g_dumpinfo.cpp | 1 + src/g_level.cpp | 3 ++ src/g_levellocals.h | 2 ++ src/gamedata/g_mapinfo.cpp | 16 ++++++++++ src/gamedata/g_mapinfo.h | 2 ++ src/p_saveg.cpp | 2 ++ .../hwrenderer/scene/hw_drawinfo.cpp | 2 ++ src/scripting/vmthunks.cpp | 4 +++ wadsrc/static/shaders/glsl/main.fp | 7 +++++ wadsrc/static/zscript/doombase.zs | 4 +++ 17 files changed, 92 insertions(+) diff --git a/src/common/rendering/gl/gl_shader.cpp b/src/common/rendering/gl/gl_shader.cpp index b891a1440..ccc255b8d 100644 --- a/src/common/rendering/gl/gl_shader.cpp +++ b/src/common/rendering/gl/gl_shader.cpp @@ -330,6 +330,9 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * int uShadowmapFilter; int uLightBlendMode; + + float uThickFogDistance; + float uThickFogMultiplier; }; uniform int uTextureMode; diff --git a/src/common/rendering/gles/gles_renderstate.cpp b/src/common/rendering/gles/gles_renderstate.cpp index 5995fd3b6..33d213d23 100644 --- a/src/common/rendering/gles/gles_renderstate.cpp +++ b/src/common/rendering/gles/gles_renderstate.cpp @@ -231,6 +231,8 @@ bool FGLRenderState::ApplyShader() activeShader->cur->muClipHeight.Set(mHwUniforms->mClipHeight); activeShader->cur->muClipHeightDirection.Set(mHwUniforms->mClipHeightDirection); //activeShader->cur->muShadowmapFilter.Set(mHwUniforms->mShadowmapFilter); + activeShader->cur->muThickFogDistance.Set(mHwUniforms->mThickFogDistance); + activeShader->cur->muThickFogMultiplier.Set(mHwUniforms->mThickFogMultiplier); } glVertexAttrib4fv(VATTR_COLOR, &mStreamData.uVertexColor.X); diff --git a/src/common/rendering/gles/gles_shader.cpp b/src/common/rendering/gles/gles_shader.cpp index dba9a233a..eb76ee78c 100644 --- a/src/common/rendering/gles/gles_shader.cpp +++ b/src/common/rendering/gles/gles_shader.cpp @@ -284,6 +284,9 @@ bool FShader::Load(const char * name, const char * vert_prog_lump_, const char * uniform float uClipHeightDirection; uniform int uShadowmapFilter; + uniform float uThickFogDistance; + uniform float uThickFogMultiplier; + uniform int uTextureMode; uniform vec2 uClipSplit; uniform float uAlphaThreshold; @@ -582,6 +585,8 @@ bool FShader::Load(const char * name, const char * vert_prog_lump_, const char * shaderData->muClipHeightDirection.Init(shaderData->hShader, "uClipHeightDirection"); shaderData->muShadowmapFilter.Init(shaderData->hShader, "uShadowmapFilter"); + shaderData->muThickFogDistance.Init(shaderData->hShader, "uThickFogDistance"); + shaderData->muThickFogMultiplier.Init(shaderData->hShader, "uThickFogMultiplier"); //// shaderData->muDesaturation.Init(shaderData->hShader, "uDesaturationFactor"); diff --git a/src/common/rendering/gles/gles_shader.h b/src/common/rendering/gles/gles_shader.h index 31ebffa39..3a2979cce 100644 --- a/src/common/rendering/gles/gles_shader.h +++ b/src/common/rendering/gles/gles_shader.h @@ -319,6 +319,9 @@ public: class ShaderVariantData FBufferedUniform1f muClipHeight; FBufferedUniform1f muClipHeightDirection; FBufferedUniform1i muShadowmapFilter; + + FBufferedUniform1f muThickFogDistance; + FBufferedUniform1f muThickFogMultiplier; ///// FBufferedUniform1f muDesaturation; diff --git a/src/common/rendering/hwrenderer/data/hw_viewpointuniforms.h b/src/common/rendering/hwrenderer/data/hw_viewpointuniforms.h index def3b4808..b21a692fb 100644 --- a/src/common/rendering/hwrenderer/data/hw_viewpointuniforms.h +++ b/src/common/rendering/hwrenderer/data/hw_viewpointuniforms.h @@ -30,6 +30,9 @@ struct HWViewpointUniforms int mLightBlendMode = 0; + float mThickFogDistance = -1.f; + float mThickFogMultiplier = 30.f; + void CalcDependencies() { mNormalViewMatrix.computeNormalMatrix(mViewMatrix); diff --git a/src/common/rendering/vulkan/shaders/vk_shader.cpp b/src/common/rendering/vulkan/shaders/vk_shader.cpp index 8206e4137..3ce26512e 100644 --- a/src/common/rendering/vulkan/shaders/vk_shader.cpp +++ b/src/common/rendering/vulkan/shaders/vk_shader.cpp @@ -204,6 +204,9 @@ static const char *shaderBindings = R"( int uShadowmapFilter; int uLightBlendMode; + + float uThickFogDistance; + float uThickFogMultiplier; }; layout(set = 1, binding = 1, std140) uniform MatricesUBO { diff --git a/src/console/c_cmds.cpp b/src/console/c_cmds.cpp index f92415477..9b5e8f612 100644 --- a/src/console/c_cmds.cpp +++ b/src/console/c_cmds.cpp @@ -952,6 +952,36 @@ CCMD(skymisttoggle) primaryLevel->flags3 ^= LEVEL3_SKYMIST; } +//----------------------------------------------------------------------------- +// +// +// +//----------------------------------------------------------------------------- +CCMD(thickfogdistance) +{ + if (argv.argc() > 1) + { + // Do this only on the primary level. + primaryLevel->thickfogdistance = (float)strtod(argv[1], NULL); + } + Printf("%f (positive means enabled)\n", primaryLevel->thickfogdistance); +} + +//----------------------------------------------------------------------------- +// +// +// +//----------------------------------------------------------------------------- +CCMD(thickfogmultiplier) +{ + if (argv.argc() > 1) + { + // Do this only on the primary level. + primaryLevel->thickfogmultiplier = max(0.f, (float)strtod(argv[1], NULL)); + } + Printf("%f\n", primaryLevel->thickfogmultiplier); +} + //----------------------------------------------------------------------------- // // diff --git a/src/g_dumpinfo.cpp b/src/g_dumpinfo.cpp index 99c4b6adf..84dc50b7b 100644 --- a/src/g_dumpinfo.cpp +++ b/src/g_dumpinfo.cpp @@ -430,6 +430,7 @@ CCMD(skyfog) // Do this only on the primary level. primaryLevel->skyfog = max(0, (int)strtoull(argv[1], NULL, 0)); } + Printf("%d\n", primaryLevel->skyfog); } diff --git a/src/g_level.cpp b/src/g_level.cpp index 69a4c9af2..4367d4b1c 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1903,6 +1903,9 @@ void FLevelLocals::Init() skyfog = info->skyfog; deathsequence = info->deathsequence; + thickfogdistance = info->thickfogdistance; + thickfogmultiplier = info->thickfogmultiplier; + pixelstretch = info->pixelstretch; compatflags->Callback(); diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 01c95dac1..9764ebb39 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -753,6 +753,8 @@ public: bool lightadditivesurfaces; bool notexturefill; int ImpactDecalCount; + float thickfogdistance; + float thickfogmultiplier; FGlobalDLightLists lightlists; diff --git a/src/gamedata/g_mapinfo.cpp b/src/gamedata/g_mapinfo.cpp index 3f72be352..8ee694878 100644 --- a/src/gamedata/g_mapinfo.cpp +++ b/src/gamedata/g_mapinfo.cpp @@ -313,6 +313,8 @@ void level_info_t::Reset() fogdensity = 0; outsidefogdensity = 0; skyfog = 0; + thickfogdistance = -1.0f; + thickfogmultiplier = 30.0f; pixelstretch = 1.2f; specialactions.Clear(); @@ -1575,6 +1577,20 @@ DEFINE_MAP_OPTION(skyfog, false) info->skyfog = parse.sc.Number; } +DEFINE_MAP_OPTION(thickfogdistance, false) +{ + parse.ParseAssign(); + parse.sc.MustGetFloat(); + info->thickfogdistance = (float)parse.sc.Float; +} + +DEFINE_MAP_OPTION(thickfogmultiplier, false) +{ + parse.ParseAssign(); + parse.sc.MustGetFloat(); + info->thickfogmultiplier = (float)parse.sc.Float; +} + DEFINE_MAP_OPTION(pixelratio, false) { parse.ParseAssign(); diff --git a/src/gamedata/g_mapinfo.h b/src/gamedata/g_mapinfo.h index 4c3d6a18a..d08fc5a27 100644 --- a/src/gamedata/g_mapinfo.h +++ b/src/gamedata/g_mapinfo.h @@ -382,6 +382,8 @@ struct level_info_t int fogdensity; int outsidefogdensity; int skyfog; + float thickfogdistance; + float thickfogmultiplier; float pixelstretch; // Redirection: If any player is carrying the specified item, then diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 6248f2329..99c77b1aa 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -982,6 +982,8 @@ void FLevelLocals::Serialize(FSerializer &arc, bool hubload) ("fogdensity", fogdensity) ("outsidefogdensity", outsidefogdensity) ("skyfog", skyfog) + ("thickfogdistance", thickfogdistance) + ("thickfogmultiplier", thickfogmultiplier) ("deathsequence", deathsequence) ("bodyqueslot", bodyqueslot) ("spawnindex", spawnindex) diff --git a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp index e16a549d9..a9f866a5c 100644 --- a/src/rendering/hwrenderer/scene/hw_drawinfo.cpp +++ b/src/rendering/hwrenderer/scene/hw_drawinfo.cpp @@ -175,6 +175,8 @@ void HWDrawInfo::StartScene(FRenderViewpoint &parentvp, HWViewpointUniforms *uni VPUniforms.mClipLine.X = -10000000.0f; VPUniforms.mShadowmapFilter = gl_shadowmap_filter; VPUniforms.mLightBlendMode = (level.info ? (int)level.info->lightblendmode : 0); + VPUniforms.mThickFogDistance = Level->thickfogdistance; + VPUniforms.mThickFogMultiplier = Level->thickfogmultiplier; } mClipper->SetViewpoint(Viewpoint); vClipper->SetViewpoint(Viewpoint); diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 8388b8f9c..17b44a27d 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2838,6 +2838,8 @@ DEFINE_FIELD_X(LevelInfo, level_info_t, deathsequence) DEFINE_FIELD_X(LevelInfo, level_info_t, fogdensity) DEFINE_FIELD_X(LevelInfo, level_info_t, outsidefogdensity) DEFINE_FIELD_X(LevelInfo, level_info_t, skyfog) +DEFINE_FIELD_X(LevelInfo, level_info_t, thickfogdistance) +DEFINE_FIELD_X(LevelInfo, level_info_t, thickfogmultiplier) DEFINE_FIELD_X(LevelInfo, level_info_t, pixelstretch) DEFINE_FIELD_X(LevelInfo, level_info_t, RedirectType) DEFINE_FIELD_X(LevelInfo, level_info_t, RedirectMapName) @@ -2889,6 +2891,8 @@ DEFINE_FIELD(FLevelLocals, teamdamage) DEFINE_FIELD(FLevelLocals, fogdensity) DEFINE_FIELD(FLevelLocals, outsidefogdensity) DEFINE_FIELD(FLevelLocals, skyfog) +DEFINE_FIELD(FLevelLocals, thickfogdistance) +DEFINE_FIELD(FLevelLocals, thickfogmultiplier) DEFINE_FIELD(FLevelLocals, pixelstretch) DEFINE_FIELD(FLevelLocals, MusicVolume) DEFINE_FIELD(FLevelLocals, deathsequence) diff --git a/wadsrc/static/shaders/glsl/main.fp b/wadsrc/static/shaders/glsl/main.fp index eb4685084..3d3f0da5f 100644 --- a/wadsrc/static/shaders/glsl/main.fp +++ b/wadsrc/static/shaders/glsl/main.fp @@ -855,6 +855,13 @@ void main() { fogdist = max(16.0, distance(pixelpos.xyz, uCameraPos.xyz)); } + if (uThickFogDistance > 0.0) + { + if (fogdist > uThickFogDistance) + { + fogdist = fogdist + uThickFogMultiplier * (fogdist - uThickFogDistance); + } + } fogfactor = exp2 (uFogDensity * fogdist); } diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index ffebf625f..91c06ff48 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -414,6 +414,8 @@ struct LevelInfo native native readonly int fogdensity; native readonly int outsidefogdensity; native readonly int skyfog; + native readonly float thickfogdistance; + native readonly float thickfogmultiplier; native readonly float pixelstretch; native readonly name RedirectType; native readonly String RedirectMapName; @@ -527,6 +529,8 @@ struct LevelLocals native native readonly int fogdensity; native readonly int outsidefogdensity; native readonly int skyfog; + native readonly float thickfogdistance; + native readonly float thickfogmultiplier; native readonly float pixelstretch; native readonly float MusicVolume; native name deathsequence; From f9f9fc602c8661e34a7f524967e3e695eeeed534 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Thu, 15 May 2025 12:01:17 -0600 Subject: [PATCH 166/384] ThickFogDistance and ThickFogMultiplier added to GLES main.fp shader file. --- wadsrc/static/shaders_gles/glsl/main.fp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wadsrc/static/shaders_gles/glsl/main.fp b/wadsrc/static/shaders_gles/glsl/main.fp index 90429f8e0..c4d25133e 100644 --- a/wadsrc/static/shaders_gles/glsl/main.fp +++ b/wadsrc/static/shaders_gles/glsl/main.fp @@ -514,6 +514,13 @@ void main() fogdist = max(16.0, distance(pixelpos.xyz, uCameraPos.xyz)); #endif + if (uThickFogDistance > 0.0) + { + if (fogdist > uThickFogDistance) + { + fogdist = fogdist + uThickFogMultiplier * (fogdist - uThickFogDistance); + } + } fogfactor = exp2 (uFogDensity * fogdist); } #endif From b56d184d8cdb2694fe7402ee90cbf439220eef4c Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Thu, 15 May 2025 19:03:24 -0600 Subject: [PATCH 167/384] Doing same thick fogdist calc for AmbientOcclusionColor. --- wadsrc/static/shaders/glsl/main.fp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wadsrc/static/shaders/glsl/main.fp b/wadsrc/static/shaders/glsl/main.fp index 3d3f0da5f..c561d4256 100644 --- a/wadsrc/static/shaders/glsl/main.fp +++ b/wadsrc/static/shaders/glsl/main.fp @@ -797,6 +797,13 @@ vec3 AmbientOcclusionColor() { fogdist = max(16.0, distance(pixelpos.xyz, uCameraPos.xyz)); } + if (uThickFogDistance > 0.0) + { + if (fogdist > uThickFogDistance) + { + fogdist = fogdist + uThickFogMultiplier * (fogdist - uThickFogDistance); + } + } fogfactor = exp2 (uFogDensity * fogdist); return mix(uFogColor.rgb, vec3(0.0), fogfactor); From b44f8f0cc985320c6760af503fa575fb9678dd5b Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Sat, 17 May 2025 00:47:13 +0800 Subject: [PATCH 168/384] - Add "Intensity" property for dynamic lights in GLDEFS that will multiply the brightness of the light, useful for overbrightening/underbrightening a light. - Also add an "intensity" parameter for A_AttachLight in ZScript. Note that for any kind of light overbrightening to do anything at all, one of the unclamped LightBlendModes in MAPINFO must be enabled. --- src/playsim/a_dynlight.cpp | 7 +++++-- src/playsim/a_dynlight.h | 5 +++++ src/r_data/a_dynlightdata.cpp | 2 ++ src/r_data/gldefs.cpp | 17 +++++++++++++++++ src/rendering/hwrenderer/hw_dynlightdata.cpp | 3 +++ .../hwrenderer/scene/hw_spritelight.cpp | 5 +++++ wadsrc/static/zscript/actors/actor.zs | 2 +- 7 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/playsim/a_dynlight.cpp b/src/playsim/a_dynlight.cpp index c8982528b..4025abfb7 100644 --- a/src/playsim/a_dynlight.cpp +++ b/src/playsim/a_dynlight.cpp @@ -113,6 +113,7 @@ void AttachLight(AActor *self) light->pSpotInnerAngle = &self->AngleVar(NAME_SpotInnerAngle); light->pSpotOuterAngle = &self->AngleVar(NAME_SpotOuterAngle); + light->lightDefIntensity = 1.0; light->pPitch = &self->Angles.Pitch; light->pLightFlags = (LightFlags*)&self->IntVar(NAME_lightflags); light->pArgs = self->args; @@ -863,7 +864,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, A_AttachLightDef, AttachLightDef) // //========================================================================== -int AttachLightDirect(AActor *self, int _lightid, int type, int color, int radius1, int radius2, int flags, double ofs_x, double ofs_y, double ofs_z, double param, double spoti, double spoto, double spotp) +int AttachLightDirect(AActor *self, int _lightid, int type, int color, int radius1, int radius2, int flags, double ofs_x, double ofs_y, double ofs_z, double param, double spoti, double spoto, double spotp, double intensity) { FName lightid = FName(ENamedName(_lightid)); auto userlight = self->UserLights[FindUserLight(self, lightid, true)]; @@ -879,6 +880,7 @@ int AttachLightDirect(AActor *self, int _lightid, int type, int color, int radiu userlight->SetParameter(type == PulseLight? param*TICRATE : param*360.); userlight->SetSpotInnerAngle(spoti); userlight->SetSpotOuterAngle(spoto); + userlight->SetLightDefIntensity(intensity); if (spotp >= -90. && spotp <= 90.) { userlight->SetSpotPitch(spotp); @@ -908,7 +910,8 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, A_AttachLight, AttachLightDirect) PARAM_FLOAT(spoti); PARAM_FLOAT(spoto); PARAM_FLOAT(spotp); - ACTION_RETURN_BOOL(AttachLightDirect(self, lightid.GetIndex(), type, color, radius1, radius2, flags, ofs_x, ofs_y, ofs_z, parami, spoti, spoto, spotp)); + PARAM_FLOAT(intensity); + ACTION_RETURN_BOOL(AttachLightDirect(self, lightid.GetIndex(), type, color, radius1, radius2, flags, ofs_x, ofs_y, ofs_z, parami, spoti, spoto, spotp, intensity)); } //========================================================================== diff --git a/src/playsim/a_dynlight.h b/src/playsim/a_dynlight.h index 5424a4a53..2f8bea462 100644 --- a/src/playsim/a_dynlight.h +++ b/src/playsim/a_dynlight.h @@ -77,6 +77,7 @@ public: void SetDontLightOthers(bool on) { if (on) m_lightFlags |= LF_DONTLIGHTOTHERS; else m_lightFlags &= ~LF_DONTLIGHTOTHERS; } void SetDontLightMap(bool on) { if (on) m_lightFlags |= LF_DONTLIGHTMAP; else m_lightFlags &= ~LF_DONTLIGHTMAP; } void SetNoShadowmap(bool on) { if (on) m_lightFlags |= LF_NOSHADOWMAP; else m_lightFlags &= ~LF_NOSHADOWMAP; } + void SetLightDefIntensity(double i) { m_LightDefIntensity = i; } void SetSpot(bool spot) { if (spot) m_lightFlags |= LF_SPOT; else m_lightFlags &= ~LF_SPOT; } void SetSpotInnerAngle(double angle) { m_spotInnerAngle = DAngle::fromDeg(angle); } void SetSpotOuterAngle(double angle) { m_spotOuterAngle = DAngle::fromDeg(angle); } @@ -128,6 +129,7 @@ protected: DAngle m_spotInnerAngle = DAngle::fromDeg(10.0); DAngle m_spotOuterAngle = DAngle::fromDeg(25.0); DAngle m_pitch = nullAngle; + double m_LightDefIntensity = 1.0; // Light over/underbright multiplication for GLDEFS-defined lights friend FSerializer &Serialize(FSerializer &arc, const char *key, FLightDefaults &value, FLightDefaults *def); }; @@ -231,6 +233,7 @@ struct FDynamicLight int GetBlue() const { return pArgs[LIGHT_BLUE]; } int GetIntensity() const { return pArgs[LIGHT_INTENSITY]; } int GetSecondaryIntensity() const { return pArgs[LIGHT_SECONDARY_INTENSITY]; } + double GetLightDefIntensity() const { return lightDefIntensity; } bool IsSubtractive() const { return !!((*pLightFlags) & LF_SUBTRACTIVE); } bool IsAdditive() const { return !!((*pLightFlags) & LF_ADDITIVE); } @@ -293,6 +296,8 @@ public: bool swapped; bool explicitpitch; + double lightDefIntensity; + FDynamicLightTouchLists touchlists; }; diff --git a/src/r_data/a_dynlightdata.cpp b/src/r_data/a_dynlightdata.cpp index c20c8d2e9..081eb1089 100644 --- a/src/r_data/a_dynlightdata.cpp +++ b/src/r_data/a_dynlightdata.cpp @@ -81,6 +81,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, FLightDefaults &value, ("spotinner", value.m_spotInnerAngle) ("spotouter", value.m_spotOuterAngle) ("pitch", value.m_pitch) + ("lightdefintensity", value.m_LightDefIntensity) .EndObject(); } return arc; @@ -125,6 +126,7 @@ void FLightDefaults::ApplyProperties(FDynamicLight * light) const light->m_active = true; light->lighttype = m_type; light->specialf1 = m_Param; + light->lightDefIntensity = m_LightDefIntensity; light->pArgs = m_Args; light->pLightFlags = &m_lightFlags; if (m_lightFlags & LF_SPOT) diff --git a/src/r_data/gldefs.cpp b/src/r_data/gldefs.cpp index 6bdb4f2d5..4b6812d69 100644 --- a/src/r_data/gldefs.cpp +++ b/src/r_data/gldefs.cpp @@ -200,6 +200,7 @@ static const char *LightTags[]= "noshadowmap", "dontlightothers", "dontlightmap", + "intensity", nullptr }; @@ -226,6 +227,7 @@ enum { LIGHTTAG_NOSHADOWMAP, LIGHTTAG_DONTLIGHTOTHERS, LIGHTTAG_DONTLIGHTMAP, + LIGHTTAG_INTENSITY, }; //========================================================================== @@ -541,6 +543,9 @@ class GLDefsParser defaults->SetSpotOuterAngle(outerAngle); } break; + case LIGHTTAG_INTENSITY: + defaults->SetLightDefIntensity(ParseFloat(sc)); + break; default: sc.ScriptError("Unknown tag: %s\n", sc.String); } @@ -643,6 +648,9 @@ class GLDefsParser defaults->SetSpotOuterAngle(outerAngle); } break; + case LIGHTTAG_INTENSITY: + defaults->SetLightDefIntensity(ParseFloat(sc)); + break; default: sc.ScriptError("Unknown tag: %s\n", sc.String); } @@ -748,6 +756,9 @@ class GLDefsParser defaults->SetSpotOuterAngle(outerAngle); } break; + case LIGHTTAG_INTENSITY: + defaults->SetLightDefIntensity(ParseFloat(sc)); + break; default: sc.ScriptError("Unknown tag: %s\n", sc.String); } @@ -852,6 +863,9 @@ class GLDefsParser defaults->SetSpotOuterAngle(outerAngle); } break; + case LIGHTTAG_INTENSITY: + defaults->SetLightDefIntensity(ParseFloat(sc)); + break; default: sc.ScriptError("Unknown tag: %s\n", sc.String); } @@ -953,6 +967,9 @@ class GLDefsParser defaults->SetSpotOuterAngle(outerAngle); } break; + case LIGHTTAG_INTENSITY: + defaults->SetLightDefIntensity(ParseFloat(sc)); + break; default: sc.ScriptError("Unknown tag: %s\n", sc.String); } diff --git a/src/rendering/hwrenderer/hw_dynlightdata.cpp b/src/rendering/hwrenderer/hw_dynlightdata.cpp index bf516e637..1bb7ec33a 100644 --- a/src/rendering/hwrenderer/hw_dynlightdata.cpp +++ b/src/rendering/hwrenderer/hw_dynlightdata.cpp @@ -95,6 +95,9 @@ void AddLightToList(FDynLightData &dld, int group, FDynamicLight * light, bool f if (light->target && (light->target->renderflags2 & RF2_LIGHTMULTALPHA)) cs *= (float)light->target->Alpha; + // Multiply intensity from GLDEFS + cs *= (float)light->GetLightDefIntensity(); + float r = light->GetRed() / 255.0f * cs; float g = light->GetGreen() / 255.0f * cs; float b = light->GetBlue() / 255.0f * cs; diff --git a/src/rendering/hwrenderer/scene/hw_spritelight.cpp b/src/rendering/hwrenderer/scene/hw_spritelight.cpp index ad2c59d7f..d3b164ba0 100644 --- a/src/rendering/hwrenderer/scene/hw_spritelight.cpp +++ b/src/rendering/hwrenderer/scene/hw_spritelight.cpp @@ -194,6 +194,11 @@ void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, FSec lb *= alpha; } + // Get GLDEFS intensity + lr *= light->GetLightDefIntensity(); + lg *= light->GetLightDefIntensity(); + lb *= light->GetLightDefIntensity(); + if (light->IsSubtractive()) { float bright = (float)FVector3(lr, lg, lb).Length(); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 4ce112424..aae41c81b 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1356,7 +1356,7 @@ class Actor : Thinker native action native void A_OverlayTranslation(int layer, name trname); native bool A_AttachLightDef(Name lightid, Name lightdef); - native bool A_AttachLight(Name lightid, int type, Color lightcolor, int radius1, int radius2, int flags = 0, Vector3 ofs = (0,0,0), double param = 0, double spoti = 10, double spoto = 25, double spotp = 0); + native bool A_AttachLight(Name lightid, int type, Color lightcolor, int radius1, int radius2, int flags = 0, Vector3 ofs = (0,0,0), double param = 0, double spoti = 10, double spoto = 25, double spotp = 0, double intensity = 1.0); native bool A_RemoveLight(Name lightid); //================================================ From 30a7ccb7da07074e26cb436b450a21d893690619 Mon Sep 17 00:00:00 2001 From: Robert Godward <3904713+RCGodward@users.noreply.github.com> Date: Tue, 20 May 2025 10:20:10 -0400 Subject: [PATCH 169/384] Move additional includes out of the FileSys namespace. --- src/common/filesystem/source/fs_findfile.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/common/filesystem/source/fs_findfile.cpp b/src/common/filesystem/source/fs_findfile.cpp index e3d515ab1..3d176af55 100644 --- a/src/common/filesystem/source/fs_findfile.cpp +++ b/src/common/filesystem/source/fs_findfile.cpp @@ -48,6 +48,11 @@ #include #include +#else + +#include +#include + #endif namespace FileSys { @@ -202,9 +207,6 @@ static size_t FS_GetFileSize(findstate_t* handle, const char* pathname) #else -#include -#include - std::wstring toWide(const char* str) { int len = (int)strlen(str); From 706d1b6978cb579ff73410cf2181a32c5018dc7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Mon, 19 May 2025 02:52:17 -0300 Subject: [PATCH 170/384] Scriptified scoreboard drawing --- src/common/2d/v_draw.cpp | 45 ++ .../scripting/interface/stringformat.cpp | 46 ++ src/ct_chat.cpp | 4 +- src/d_netinfo.cpp | 6 + src/hu_scores.cpp | 344 +------------- src/hu_stuff.h | 10 +- src/scripting/vmthunks.cpp | 7 + src/wi_stuff.cpp | 63 --- wadsrc/static/zscript.txt | 1 + wadsrc/static/zscript/actors/player/player.zs | 2 + wadsrc/static/zscript/constants.zs | 2 + wadsrc/static/zscript/doombase.zs | 2 + wadsrc/static/zscript/engine/base.zs | 11 + .../zscript/ui/statscreen/statscreen.zs | 39 +- .../static/zscript/ui/statusbar/scoreboard.zs | 449 ++++++++++++++++++ .../static/zscript/ui/statusbar/statusbar.zs | 1 + 16 files changed, 617 insertions(+), 415 deletions(-) create mode 100644 wadsrc/static/zscript/ui/statusbar/scoreboard.zs diff --git a/src/common/2d/v_draw.cpp b/src/common/2d/v_draw.cpp index 247a2916d..eb9529925 100644 --- a/src/common/2d/v_draw.cpp +++ b/src/common/2d/v_draw.cpp @@ -1840,6 +1840,51 @@ DEFINE_ACTION_FUNCTION(_Screen, DrawLineFrame) return 0; } +DEFINE_ACTION_FUNCTION(_Screen, GetTextureWidth) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayWidth() : -1); +} + +DEFINE_ACTION_FUNCTION(_Screen, GetTextureHeight) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayHeight() : -1); +} + + +DEFINE_ACTION_FUNCTION(_Screen, GetTextureLeftOffset) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayLeftOffset() : -1); +} + +DEFINE_ACTION_FUNCTION(_Screen, GetTextureTopOffset) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayTopOffset() : -1); +} + DEFINE_ACTION_FUNCTION(FCanvas, DrawLineFrame) { PARAM_SELF_PROLOGUE(FCanvas); diff --git a/src/common/scripting/interface/stringformat.cpp b/src/common/scripting/interface/stringformat.cpp index 3d7a3c428..b5b88a695 100644 --- a/src/common/scripting/interface/stringformat.cpp +++ b/src/common/scripting/interface/stringformat.cpp @@ -564,6 +564,52 @@ DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, Substitute, StringSubst) return 0; } +static int StringCompare(FString *self, const FString &other) +{ + return self->Compare(other); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, Compare, StringCompare) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + PARAM_STRING(other); + ACTION_RETURN_INT(StringCompare(self, other)); +} + +static int StringCompareNoCase(FString *self, const FString &other) +{ + return self->CompareNoCase(other); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, CompareNoCase, StringCompareNoCase) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + PARAM_STRING(other); + ACTION_RETURN_INT(StringCompareNoCase(self, other)); +} + +static int StringIsEmpty(FString *self) +{ + return self->IsEmpty(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, IsEmpty, StringIsEmpty) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + ACTION_RETURN_INT(StringIsEmpty(self)); +} + +static int StringIsNotEmpty(FString *self) +{ + return self->IsNotEmpty(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, IsNotEmpty, StringIsNotEmpty) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + ACTION_RETURN_INT(StringIsNotEmpty(self)); +} + static void StringStripRight(FString* self, const FString& junk) { if (junk.IsNotEmpty()) self->StripRight(junk); diff --git a/src/ct_chat.cpp b/src/ct_chat.cpp index 20636e859..e8ebcca45 100644 --- a/src/ct_chat.cpp +++ b/src/ct_chat.cpp @@ -45,6 +45,7 @@ #include "c_buttons.h" #include "d_buttons.h" #include "v_draw.h" +#include "r_utility.h" enum { @@ -241,6 +242,7 @@ void CT_PasteChat(const char *clip) void CT_Drawer (void) { + auto &vp = r_viewpoint; auto drawer = twod; FFont *displayfont = NewConsoleFont; @@ -254,7 +256,7 @@ void CT_Drawer (void) { // todo: check for summary screen } - if (!skipit) HU_DrawScores (&players[consoleplayer]); + if (!skipit) HU_DrawScores (consoleplayer, vp.TicFrac); } if (chatmodeon) { diff --git a/src/d_netinfo.cpp b/src/d_netinfo.cpp index bac6eddfe..c528aa5d1 100644 --- a/src/d_netinfo.cpp +++ b/src/d_netinfo.cpp @@ -232,6 +232,12 @@ DEFINE_ACTION_FUNCTION(_PlayerInfo, GetDisplayColor) ACTION_RETURN_INT(c); } +DEFINE_ACTION_FUNCTION(_PlayerInfo, GetAverageLatency) +{ + PARAM_SELF_STRUCT_PROLOGUE(player_t); + ACTION_RETURN_INT(ClientStates[self - players].AverageLatency); +} + // Find out which teams are present. If there is only one, // then another team should be chosen at random. // diff --git a/src/hu_scores.cpp b/src/hu_scores.cpp index f88621917..be93c6180 100644 --- a/src/hu_scores.cpp +++ b/src/hu_scores.cpp @@ -67,6 +67,7 @@ static void HU_DoDrawScores (player_t *, player_t *[MAXPLAYERS]); static void HU_DrawTimeRemaining (int y); static void HU_DrawPlayer(player_t *, bool, int, int, int, int, int, int, int, int, int); +static void HU_DrawColorBar(int x, int y, int height, int playernum); // EXTERNAL DATA DECLARATIONS ---------------------------------------------- @@ -132,8 +133,6 @@ void HU_SortPlayers */ bool SB_ForceActive = false; -static FFont *displayFont; -static int FontScale; // PRIVATE DATA DEFINITIONS ------------------------------------------------ @@ -146,345 +145,12 @@ static int FontScale; // //========================================================================== -void HU_DrawScores(player_t* player) +void HU_DrawScores(int player, double ticFrac) { - displayFont = NewSmallFont; - FontScale = max(screen->GetHeight() / 400, 1); - - int numPlayers = 0; - for (int i = 0; i < MAXPLAYERS; ++i) - numPlayers += playeringame[i]; - - if (numPlayers > 8) - FontScale = static_cast(ceil(FontScale * 0.75)); - - if (deathmatch) + IFVIRTUALPTR(StatusBar, DBaseStatusBar, Scoreboard_DrawScores) { - if (teamplay) - { - if (!sb_teamdeathmatch_enable) - return; - } - else if (!sb_deathmatch_enable) - { - return; - } - } - else if (!multiplayer || !sb_cooperative_enable) - { - return; - } - - player_t* sortedPlayers[MAXPLAYERS]; - if (player->camera && player->camera->player) - player = player->camera->player; - - sortedPlayers[MAXPLAYERS - 1] = player; - for (int i = 0, j = 0; j < MAXPLAYERS - 1; ++i, ++j) - { - if (&players[i] == player) - ++i; - sortedPlayers[j] = &players[i]; - } - - if (teamplay && deathmatch) - qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), compareteams); - else - qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), comparepoints); - - HU_DoDrawScores(player, sortedPlayers); -} - -//========================================================================== -// -// HU_GetPlayerWidths -// -// Returns the widest player name and class icon. -// -//========================================================================== - -void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight) -{ - constexpr char NameLabel[] = "Name"; - - displayFont = NewSmallFont; - maxNameWidth = displayFont->StringWidth(NameLabel); - maxScoreWidth = 0; - maxIconHeight = 0; - - for (int i = 0; i < MAXPLAYERS; ++i) - { - if (!playeringame[i]) - continue; - - int width = displayFont->StringWidth(players[i].userinfo.GetName(16)); - if (width > maxNameWidth) - maxNameWidth = width; - - auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon)); - if (icon.isValid()) - { - auto pic = TexMan.GetGameTexture(icon); - width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5); - if (width > maxScoreWidth) - maxScoreWidth = width; - - // The icon's top offset does not count toward its height, because - // zdoom.pk3's standard Hexen class icons are designed that way. - int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5); - if (height > maxIconHeight) - maxIconHeight = height; - } - } -} - -//========================================================================== -// -// HU_DoDrawScores -// -//========================================================================== - -static void HU_DrawFontScaled(double x, double y, int color, const char *text) -{ - DrawText(twod, displayFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, twod->GetWidth() / FontScale, DTA_VirtualHeight, twod->GetHeight() / FontScale, TAG_END); -} - -static void HU_DoDrawScores(player_t* player, player_t* sortedPlayers[MAXPLAYERS]) -{ - int color = sb_cooperative_headingcolor; - if (deathmatch) - { - if (teamplay) - color = sb_teamdeathmatch_headingcolor; - else - color = sb_deathmatch_headingcolor; - } - - int maxNameWidth, maxScoreWidth, maxIconHeight; - HU_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); - int height = displayFont->GetHeight() * FontScale; - int lineHeight = max(height, maxIconHeight * CleanYfac); - int yPadding = (lineHeight - height + 1) / 2; - - int bottom = StatusBar->GetTopOfStatusbar(); - int y = max(48 * CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); - - HU_DrawTimeRemaining(bottom - height); - - if (teamplay && deathmatch) - { - y -= (BigFont->GetHeight() + 8) * CleanYfac; - - for (size_t i = 0u; i < Teams.Size(); ++i) - { - Teams[i].m_iPlayerCount = 0; - Teams[i].m_iScore = 0; - } - - int numTeams = 0; - for (int i = 0; i < MAXPLAYERS; ++i) - { - if (playeringame[sortedPlayers[i]-players] && FTeam::IsValid(sortedPlayers[i]->userinfo.GetTeam())) - { - if (Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0) - ++numTeams; - - Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iScore += sortedPlayers[i]->fragcount; - } - } - - int scoreXWidth = twod->GetWidth() / max(8, numTeams); - int numScores = 0; - for (size_t i = 0u; i < Teams.Size(); ++i) - { - if (Teams[i].m_iPlayerCount) - ++numScores; - } - - int scoreX = (twod->GetWidth() - scoreXWidth * (numScores - 1)) / 2; - for (size_t i = 0u; i < Teams.Size(); ++i) - { - if (!Teams[i].m_iPlayerCount) - continue; - - char score[80]; - mysnprintf(score, countof(score), "%d", Teams[i].m_iScore); - - DrawText(twod, BigFont, Teams[i].GetTextColor(), - scoreX - BigFont->StringWidth(score)*CleanXfac/2, y, score, - DTA_CleanNoMove, true, TAG_DONE); - - scoreX += scoreXWidth; - } - - y += (BigFont->GetHeight() + 8) * CleanYfac; - } - - const char* text_color = GStrings.GetString("SCORE_COLOR"), - *text_frags = GStrings.GetString(deathmatch ? "SCORE_FRAGS" : "SCORE_KILLS"), - *text_name = GStrings.GetString("SCORE_NAME"), - *text_delay = GStrings.GetString("SCORE_DELAY"); - - int col2 = (displayFont->StringWidth(text_color) + 16) * FontScale; - int col3 = col2 + (displayFont->StringWidth(text_frags) + 16) * FontScale; - int col4 = col3 + maxScoreWidth * FontScale; - int col5 = col4 + (maxNameWidth + 16) * FontScale; - int x = (twod->GetWidth() >> 1) - (((displayFont->StringWidth(text_delay) * FontScale) + col5) >> 1); - - //HU_DrawFontScaled(x, y, color, text_color); - HU_DrawFontScaled(x + col2, y, color, text_frags); - HU_DrawFontScaled(x + col4, y, color, text_name); - HU_DrawFontScaled(x + col5, y, color, text_delay); - - y += height + 6 * CleanYfac; - bottom -= height; - - for (int i = 0; i < MAXPLAYERS && y <= bottom; ++i) - { - if (!playeringame[sortedPlayers[i] - players]) - continue; - - HU_DrawPlayer(sortedPlayers[i], player == sortedPlayers[i], x, col2, col3, col4, col5, maxNameWidth, y, yPadding, lineHeight); - y += lineHeight + CleanYfac; - } -} - -//========================================================================== -// -// HU_DrawTimeRemaining -// -//========================================================================== - -static void HU_DrawTimeRemaining (int y) -{ - if (deathmatch && timelimit && gamestate == GS_LEVEL) - { - char str[80]; - int timeleft = (int)(timelimit * TICRATE * 60) - primaryLevel->maptime; - int hours, minutes, seconds; - - if (timeleft < 0) - timeleft = 0; - - hours = timeleft / (TICRATE * 3600); - timeleft -= hours * TICRATE * 3600; - minutes = timeleft / (TICRATE * 60); - timeleft -= minutes * TICRATE * 60; - seconds = timeleft / TICRATE; - - if (hours) - mysnprintf (str, countof(str), "Level ends in %d:%02d:%02d", hours, minutes, seconds); - else - mysnprintf (str, countof(str), "Level ends in %d:%02d", minutes, seconds); - - HU_DrawFontScaled(twod->GetWidth() / 2 - displayFont->StringWidth(str) / 2 * FontScale, y, CR_GRAY, str); - } -} - -//========================================================================== -// -// HU_DrawPlayer -// -//========================================================================== - -static void HU_DrawPlayer (player_t *player, bool highlight, int col1, int col2, int col3, int col4, int col5, int maxnamewidth, int y, int ypadding, int height) -{ - int color; - char str[80]; - - if (highlight) - { - // The teamplay mode uses colors to show teams, so we need some - // other way to do highlighting. And it may as well be used for - // all modes for the sake of consistancy. - Dim(twod, MAKERGB(200,245,255), 0.125f, col1 - 12*FontScale, y - 1, col5 + (maxnamewidth + 24)*FontScale, height + 2); - } - - col2 += col1; - col3 += col1; - col4 += col1; - col5 += col1; - - color = HU_GetRowColor(player, highlight); - HU_DrawColorBar(col1, y, height, (int)(player - players)); - mysnprintf (str, countof(str), "%d", deathmatch ? player->fragcount : player->killcount); - - HU_DrawFontScaled(col2, y + ypadding, color, player->playerstate == PST_DEAD && !deathmatch ? "DEAD" : str); - - auto icon = FSetTextureID(player->mo->IntVar(NAME_ScoreIcon)); - if (icon.isValid()) - { - DrawTexture(twod, icon, false, col3, y, - DTA_CleanNoMove, true, - TAG_DONE); - } - - HU_DrawFontScaled(col4, y + ypadding, color, player->userinfo.GetName()); - - mysnprintf(str, countof(str), "%u", ClientStates[player - players].AverageLatency); - - HU_DrawFontScaled(col5, y + ypadding, color, str); - - const int team = player->userinfo.GetTeam(); - if (team != TEAM_NONE && teamplay && Teams[team].GetLogo().IsNotEmpty ()) - { - auto pic = TexMan.GetGameTextureByName(Teams[player->userinfo.GetTeam()].GetLogo().GetChars ()); - DrawTexture(twod, pic, col1 - (pic->GetDisplayWidth() + 2) * CleanXfac, y, - DTA_CleanNoMove, true, TAG_DONE); - } -} - -//========================================================================== -// -// HU_DrawColorBar -// -//========================================================================== - -void HU_DrawColorBar(int x, int y, int height, int playernum) -{ - float h, s, v, r, g, b; - - D_GetPlayerColor (playernum, &h, &s, &v, NULL); - HSVtoRGB (&r, &g, &b, h, s, v); - - ClearRect(twod, x, y, x + 24*FontScale, y + height, -1, - MAKEARGB(255,clamp(int(r*255.f),0,255), - clamp(int(g*255.f),0,255), - clamp(int(b*255.f),0,255))); -} - -//========================================================================== -// -// HU_GetRowColor -// -//========================================================================== - -int HU_GetRowColor(player_t *player, bool highlight) -{ - if (teamplay && deathmatch) - { - if (FTeam::IsValid (player->userinfo.GetTeam())) - return Teams[player->userinfo.GetTeam()].GetTextColor(); - else - return CR_GREY; - } - else - { - if (!highlight) - { - if (demoplayback && player == &players[consoleplayer]) - { - return CR_GOLD; - } - else - { - return deathmatch ? sb_deathmatch_otherplayercolor : sb_cooperative_otherplayercolor; - } - } - else - { - return deathmatch ? sb_deathmatch_yourplayercolor : sb_cooperative_yourplayercolor; - } + VMValue params[] = { (DObject*)StatusBar, player, ticFrac }; + VMCall(func, params, countof(params), nullptr, 0); } } diff --git a/src/hu_stuff.h b/src/hu_stuff.h index 01cf429cc..d9f05089d 100644 --- a/src/hu_stuff.h +++ b/src/hu_stuff.h @@ -42,16 +42,8 @@ void CT_Drawer (void); // [RH] Draw deathmatch scores -void HU_DrawScores(player_t* me); -void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight); -void HU_DrawColorBar(int x, int y, int height, int playernum); -int HU_GetRowColor(player_t *player, bool hightlight); +void HU_DrawScores(int me, double ticFrac); extern bool SB_ForceActive; -// Sorting routines - -int comparepoints(const void *arg1, const void *arg2); -int compareteams(const void *arg1, const void *arg2); - #endif diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 17b44a27d..8db1e73ec 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2369,6 +2369,13 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetUDMFString, ZGetUDMFString) ACTION_RETURN_STRING(GetUDMFString(self, type, index, key)); } +DEFINE_ACTION_FUNCTION(FLevelLocals, PlayerNum) +{ + PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); + PARAM_POINTER(player, player_t); + ACTION_RETURN_INT(self->PlayerNum(player)); +} + DEFINE_ACTION_FUNCTION(FLevelLocals, GetChecksum) { PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); diff --git a/src/wi_stuff.cpp b/src/wi_stuff.cpp index 8a5c7945c..99fc66b3d 100644 --- a/src/wi_stuff.cpp +++ b/src/wi_stuff.cpp @@ -1219,69 +1219,6 @@ DObject* WI_Start(wbstartstruct_t *wbstartstruct) // //==================================================================== -DEFINE_ACTION_FUNCTION(DStatusScreen, GetPlayerWidths) -{ - PARAM_PROLOGUE; - int maxnamewidth, maxscorewidth, maxiconheight; - HU_GetPlayerWidths(maxnamewidth, maxscorewidth, maxiconheight); - if (numret > 0) ret[0].SetInt(maxnamewidth); - if (numret > 1) ret[1].SetInt(maxscorewidth); - if (numret > 2) ret[2].SetInt(maxiconheight); - return min(numret, 3); -} - -//==================================================================== -// -// -// -//==================================================================== - -DEFINE_ACTION_FUNCTION(DStatusScreen, GetRowColor) -{ - PARAM_PROLOGUE; - PARAM_POINTER(p, player_t); - PARAM_BOOL(highlight); - ACTION_RETURN_INT(HU_GetRowColor(p, highlight)); -} - -//==================================================================== -// -// -// -//==================================================================== - -DEFINE_ACTION_FUNCTION(DStatusScreen, GetSortedPlayers) -{ - PARAM_PROLOGUE; - PARAM_POINTER(array, TArray); - PARAM_BOOL(teamplay); - - player_t *sortedplayers[MAXPLAYERS]; - // Sort all players - for (int i = 0; i < MAXPLAYERS; i++) - { - sortedplayers[i] = &players[i]; - } - - if (teamplay) - qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams); - else - qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints); - - array->Resize(MAXPLAYERS); - for (unsigned i = 0; i < MAXPLAYERS; i++) - { - (*array)[i] = int(sortedplayers[i] - players); - } - return 0; -} - -//==================================================================== -// -// -// -//==================================================================== - DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, skills); DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, sitems); DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, ssecret); diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt index 371063dab..55469c4e4 100644 --- a/wadsrc/static/zscript.txt +++ b/wadsrc/static/zscript.txt @@ -308,6 +308,7 @@ version "4.15.1" #include "zscript/ui/statusbar/sbarinfowrapper.zs" #include "zscript/ui/statusbar/statusbar.zs" #include "zscript/ui/statusbar/strife_sbar.zs" +#include "zscript/ui/statusbar/scoreboard.zs" #include "zscript/ui/intermission.zs" diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index eda846144..7eab28a19 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -3002,6 +3002,8 @@ struct PlayerInfo native play // self is what internally is known as player_t native clearscope bool GetClassicFlight() const; native void SendPitchLimits(); native clearscope bool HasWeaponsInSlot(int slot) const; + + native clearscope int GetAverageLatency() const; // The actual implementation is on PlayerPawn where it can be overridden. Use that directly in the future. deprecated("3.7", "MorphPlayer() should be used on a PlayerPawn object") bool MorphPlayer(PlayerInfo activator, class spawnType, int duration, EMorphFlags style, class enterFlash = "TeleportFog", class exitFlash = "TeleportFog") diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index d2122f9fe..ba2598093 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1,6 +1,8 @@ // for flag changer functions. const FLAG_NO_CHANGE = -1; const MAXPLAYERS = 64; +const TEAM_NONE = 255; +const TEAM_MAXIMUM = 16; enum EStateUseFlags { diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 91c06ff48..4c5d4799c 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -587,6 +587,8 @@ struct LevelLocals native native clearscope int ActorOnLineSide(Actor mo, Line l) const; native clearscope int BoxOnLineSide(Vector2 pos, double radius, Line l) const; + native clearscope int PlayerNum(PlayerInfo player) const; + native String GetChecksum() const; native void ChangeSky(TextureID sky1, TextureID sky2 ); diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index ee9161656..63e315649 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -578,6 +578,11 @@ struct Screen native native static void ClearStencil(); native static void SetTransform(Shape2DTransform transform); native static void ClearTransform(); + + native static double GetTextureWidth(TextureID texture, bool animated = false); + native static double GetTextureHeight(TextureID texture, bool animated = false); + native static double GetTextureLeftOffset(TextureID texture, bool animated = false); + native static double GetTextureTopOffset(TextureID texture, bool animated = false); } struct Font native @@ -948,6 +953,12 @@ struct StringStruct native unsafe(internal) native void StripLeft(String junk = ""); native void StripRight(String junk = ""); native void StripLeftRight(String junk = ""); + + native int Compare(String other) const; // strcmp + native int CompareNoCase(String other) const; // stricmp + + native bool IsEmpty() const; // strcmp + native bool IsNotEmpty() const; // stricmp } struct Translation version("2.4") diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen.zs b/wadsrc/static/zscript/ui/statscreen/statscreen.zs index 3ae9cf447..94343f80d 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen.zs @@ -967,7 +967,40 @@ class StatusScreen : ScreenJob abstract version("2.5") protected virtual void updateStats() {} protected virtual void drawStats() {} - native static int, int, int GetPlayerWidths(); - native static Color GetRowColor(PlayerInfo player, bool highlight); - native static void GetSortedPlayers(in out Array sorted, bool teamplay); + static int, int, int GetPlayerWidths() + { + int maxNameWidth; + int maxScoreWidth; + int maxIconHeight; + + StatusBar.Scoreboard_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); + + return maxNameWidth, maxScoreWidth, maxIconHeight; + } + + static Color GetRowColor(PlayerInfo player, bool highlight) + { + return StatusBar.Scoreboard_GetRowColor(player, highlight); + } + + static void GetSortedPlayers(in out Array sorted, bool teamplay) + { + sorted.clear(); + for(int i = 0; i < MAXPLAYERS; i++) + { + if(playeringame[i]) + { + sorted.Push(i); + } + } + + if(teamplay) + { + StatusBar.Scoreboard_SortPlayers(sorted, BaseStatusBar.Scoreboard_CompareByTeams); + } + else + { + StatusBar.Scoreboard_SortPlayers(sorted, BaseStatusBar.Scoreboard_CompareByPoints); + } + } } diff --git a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs new file mode 100644 index 000000000..7b8952770 --- /dev/null +++ b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs @@ -0,0 +1,449 @@ + +extend class BaseStatusBar +{ + Font scoreboardFont; + + virtual void InitScoreboard() + { + scoreboardFont = NewSmallFont; + } + + static clearscope int Scoreboard_CompareByTeams(int playerA, int playerB) + { + // Compare first by teams, then by frags, then by name. + PlayerInfo p1 = players[playerA]; + PlayerInfo p2 = players[playerB]; + + int diff = p1.GetTeam() - p2.GetTeam(); + + if(diff == 0) + { + diff = p2.fragcount - p1.fragcount; + if(diff == 0) + { + diff = p1.GetUserName().CompareNoCase(p2.GetUserName()); + } + } + return diff; + } + + static clearscope int Scoreboard_CompareByPoints(int playerA, int playerB) + { + // Compare first by frags/kills, then by name. + PlayerInfo p1 = players[playerA]; + PlayerInfo p2 = players[playerB]; + + int diff = deathmatch ? (p2.fragcount - p1.fragcount) : (p2.killcount - p1.killcount); + + if(diff == 0) + { + diff = p1.GetUserName().CompareNoCase(p2.GetUserName()); + } + return diff; + } + + static void Scoreboard_SortPlayers(out Array players, Function compareFunc) + { + Array unsorted; + unsorted.Move(players); + + players.Push(unsorted[0]); + + for(int i = 1; i < unsorted.Size(); i++) + { + bool inserted = false; + + for(int j = 0; j < players.Size(); j++) + { + if(compareFunc.Call(players[j], unsorted[i]) > 0) + { + players.Insert(j, unsorted[i]); + inserted = true; + break; + } + } + + if(!inserted) + { + players.Push(unsorted[i]); + } + } + + } + + virtual void Scoreboard_DrawScores(int playerNum, double ticFrac) + { + if(deathmatch) + { + if(teamplay) + { + if(!sb_teamdeathmatch_enable) + return; + } + else if(!sb_deathmatch_enable) + { + return; + } + } + else if(!multiplayer || !sb_cooperative_enable) + { + return; + } + + if(!scoreboardFont) InitScoreboard(); + + PlayerInfo player = players[playernum]; + + if(player.camera && player.camera.player) + { + player = player.camera.player; + playerNum = Level.PlayerNum(player); + } + + Array sortedPlayers; + + for(int i = 0; i < MAXPLAYERS; i++) + { + if(playeringame[i]) + { + sortedPlayers.Push(i); + } + } + + /* + console.printf("SortedPlayers was ["); + + foreach(p : sortedPlayers) + { + console.printf("%d", p); + } + + console.printf("]"); + */ + + if(teamplay && deathmatch) + { + Scoreboard_SortPlayers(sortedPlayers, Scoreboard_CompareByTeams); + } + else + { + Scoreboard_SortPlayers(sortedPlayers, Scoreboard_CompareByPoints); + } + + /* + console.printf("SortedPlayers is now ["); + + foreach(p : sortedPlayers) + { + console.printf("%d", p); + } + + console.printf("]"); + */ + + int numPlayers = sortedPlayers.size(); + + int FontScale = max(screen.GetHeight() / 400, 1); + + if(numPlayers > 8) + FontScale = int(ceil(FontScale * 0.75)); + + Scoreboard_DoDrawScores(player, sortedPlayers, FontScale, ticFrac); + } + + //========================================================================== + // + // HU_DrawFontScaled + // + //========================================================================== + + void Scoreboard_DrawFontScaled(double x, double y, Color color, String text, int FontScale) + { + screen.DrawText(scoreboardFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, screen.GetWidth() / FontScale, DTA_VirtualHeight, screen.GetHeight() / FontScale); + } + + //========================================================================== + // + // HU_DoDrawScores + // + //========================================================================== + + virtual void Scoreboard_DoDrawScores(PlayerInfo player, Array sortedPlayers, int FontScale, double ticFrac) + { + Color _color = sb_cooperative_headingcolor; + if(deathmatch) + { + if (teamplay) + _color = sb_teamdeathmatch_headingcolor; + else + _color = sb_deathmatch_headingcolor; + } + + int maxNameWidth, maxScoreWidth, maxIconHeight; + Scoreboard_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); + int height = scoreboardFont.GetHeight() * FontScale; + int lineHeight = max(height, maxIconHeight * CleanYfac); + int yPadding = (lineHeight - height + 1) / 2; + + int bottom = GetTopOfStatusbar(); + int y = max(48 * CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); + + Scoreboard_DrawTimeRemaining(bottom - height, FontScale); + + Array teamPlayerCounts; + Array teamScores; + + teamPlayerCounts.Resize(Teams.Size()); + teamScores.Resize(Teams.Size()); + + if(teamplay && deathmatch) + { + y -= (BigFont.GetHeight() + 8) * CleanYfac; + + int numTeams = 0; + for(int i = 0; i < MAXPLAYERS; ++i) + { + PlayerInfo p = players[sortedPlayers[i]]; + if (playeringame[sortedPlayers[i]] && Team.IsValid(p.GetTeam())) + { + if (teamPlayerCounts[p.GetTeam()]++ == 0) + ++numTeams; + + teamScores[p.GetTeam()] += p.fragcount; + } + } + + int scoreXWidth = screen.GetWidth() / max(8, numTeams); + int numScores = 0; + for(int i = 0; i < Teams.Size(); ++i) + { + if (teamPlayerCounts[i]) + ++numScores; + } + + int scoreX = (screen.GetWidth() - scoreXWidth * (numScores - 1)) / 2; + for(int i = 0; i < Teams.Size(); ++i) + { + if (!teamPlayerCounts[i]) + continue; + + String score = String.Format("%d", teamScores[i]); + + screen.DrawText(BigFont, Teams[i].GetTextColor(), + scoreX - BigFont.StringWidth(score)*CleanXfac/2, y, score, + DTA_CleanNoMove, true); + + scoreX += scoreXWidth; + } + + y += (BigFont.GetHeight() + 8) * CleanYfac; + } + + String text_color = StringTable.Localize("$SCORE_COLOR"), + text_frags = StringTable.Localize(deathmatch ? "$SCORE_FRAGS" : "$SCORE_KILLS"), + text_name = StringTable.Localize("$SCORE_NAME"), + text_delay = StringTable.Localize("$SCORE_DELAY"); + + int col2 = (scoreboardFont.StringWidth(text_color) + 16) * FontScale; + int col3 = col2 + (scoreboardFont.StringWidth(text_frags) + 16) * FontScale; + int col4 = col3 + maxScoreWidth * FontScale; + int col5 = col4 + (maxNameWidth + 16) * FontScale; + int x = (screen.GetWidth() >> 1) - (((scoreboardFont.StringWidth(text_delay) * FontScale) + col5) >> 1); + + //Scoreboard_DrawFontScaled(x, y, _color, text_color); + Scoreboard_DrawFontScaled(x + col2, y, _color, text_frags, FontScale); + Scoreboard_DrawFontScaled(x + col4, y, _color, text_name, FontScale); + Scoreboard_DrawFontScaled(x + col5, y, _color, text_delay, FontScale); + + y += height + 6 * CleanYfac; + bottom -= height; + + for(int i = 0; i < sortedPlayers.Size() && y <= bottom; ++i) + { + Scoreboard_DrawPlayer(players[sortedPlayers[i]], Level.PlayerNum(player) == sortedPlayers[i], x, col2, col3, col4, col5, maxNameWidth, y, yPadding, lineHeight, FontScale); + y += lineHeight + CleanYfac; + } + } + + //========================================================================== + // + // HU_DrawTimeRemaining + // + //========================================================================== + + void Scoreboard_DrawTimeRemaining(int y, int FontScale) + { + if(deathmatch && timelimit && gamestate == GS_LEVEL) + { + int timeleft = int(timelimit * GameTicRate * 60) - Level.maptime; + + int hours, minutes, seconds; + + if(timeleft < 0) timeleft = 0; + + hours = timeleft / (GameTicRate * 3600); + timeleft -= hours * GameTicRate * 3600; + minutes = timeleft / (GameTicRate * 60); + timeleft -= minutes * GameTicRate * 60; + seconds = timeleft / GameTicRate; + + String str; + if(hours) + { + str = String.Format("Level ends in %d:%02d:%02d", hours, minutes, seconds); + } + else + { + str = String.Format("Level ends in %d:%02d", minutes, seconds); + } + + Scoreboard_DrawFontScaled(screen.GetWidth() / 2 - scoreboardFont.StringWidth(str) / 2 * FontScale, y, Font.CR_GRAY, str, FontScale); + } + } + + + //========================================================================== + // + // HU_DrawPlayer + // + //========================================================================== + + void Scoreboard_DrawPlayer(PlayerInfo player, bool highlight, int col1, int col2, int col3, int col4, int col5, int maxnamewidth, int y, int ypadding, int height, int FontScale) + { + String str; + + if(highlight) + { + // The teamplay mode uses colors to show teams, so we need some + // other way to do highlighting. And it may as well be used for + // all modes for the sake of consistancy. + screen.Dim(Color(200,245,255), 0.125f, col1 - 12 * FontScale, y - 1, col5 + (maxnamewidth + 24) * FontScale, height + 2); + } + + col2 += col1; + col3 += col1; + col4 += col1; + col5 += col1; + + Color _color = Scoreboard_GetRowColor(player, highlight); + Scoreboard_DrawColorBar(col1, y, height, player, FontScale); + + str = String.Format("%d", deathmatch ? player.fragcount : player.killcount); + + Scoreboard_DrawFontScaled(col2, y + ypadding, _color, player.playerstate == PST_DEAD && !deathmatch ? "DEAD" : str, FontScale); + + TextureID icon = player.mo.ScoreIcon; + + if(icon.isValid()) + { + screen.DrawTexture(icon, false, col3, y, DTA_CleanNoMove, true); + } + + Scoreboard_DrawFontScaled(col4, y + ypadding, _color, player.GetUserName(), FontScale); + + str = String.Format("%d", player.GetAverageLatency()); + + Scoreboard_DrawFontScaled(col5, y + ypadding, _color, str, FontScale); + + int team = player.GetTeam(); + + if(team != TEAM_NONE && teamplay && Teams[team].GetLogoName().IsNotEmpty()) + { + TextureID pic = Teams[team].GetLogo(); + screen.DrawTexture(pic, col1 - (screen.GetTextureWidth(pic) + 2) * CleanXfac, y, DTA_CleanNoMove, true); + } + } + + //========================================================================== + // + // HU_DrawColorBar + // + //========================================================================== + + static void Scoreboard_DrawColorBar(int x, int y, int height, PlayerInfo player, int FontScale) + { + Screen.Clear(x, y, x + 24 * FontScale, y + height, player.GetDisplayColor()); + } + + //========================================================================== + // + // HU_GetRowColor + // + //========================================================================== + + static Color Scoreboard_GetRowColor(PlayerInfo player, bool highlight) + { + if(teamplay && deathmatch) + { + if(Team.IsValid(player.GetTeam())) + { + Color teamColor = Teams[player.GetTeam()].GetTextColor(); + return teamColor; + } + else + { + return Font.CR_GREY; + } + } + else + { + if(!highlight) + { + if(demoplayback && Level.PlayerNum(player) == consoleplayer) + { + return Font.CR_GOLD; + } + else + { + return deathmatch ? sb_deathmatch_otherplayercolor : sb_cooperative_otherplayercolor; + } + } + else + { + return deathmatch ? sb_deathmatch_yourplayercolor : sb_cooperative_yourplayercolor; + } + } + } + + //========================================================================== + // + // HU_GetPlayerWidths + // + // Returns the widest player name and class icon. + // + //========================================================================== + + void Scoreboard_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight) + { + if(!scoreboardFont) InitScoreboard(); + + maxNameWidth = scoreboardFont.StringWidth("Name"); + maxScoreWidth = 0; + maxIconHeight = 0; + + for (int i = 0; i < MAXPLAYERS; ++i) + { + if (!playeringame[i]) + continue; + + int width = scoreboardFont.StringWidth(players[i].GetUserName(16)); + if (width > maxNameWidth) + maxNameWidth = width; + + TextureID icon = players[i].mo.ScoreIcon; + if (icon.isValid()) + { + width = int(screen.GetTextureWidth(icon) - screen.GetTextureLeftOffset(icon) + 2.5); + if (width > maxScoreWidth) + maxScoreWidth = width; + + // The icon's top offset does not count toward its height, because + // zdoom.pk3's standard Hexen class icons are designed that way. + int height = int(screen.GetTextureHeight(icon) - screen.GetTextureTopOffset(icon) + 0.5); + if (height > maxIconHeight) + maxIconHeight = height; + } + } + } + +} \ No newline at end of file diff --git a/wadsrc/static/zscript/ui/statusbar/statusbar.zs b/wadsrc/static/zscript/ui/statusbar/statusbar.zs index fa2b66c6b..b4dc83739 100644 --- a/wadsrc/static/zscript/ui/statusbar/statusbar.zs +++ b/wadsrc/static/zscript/ui/statusbar/statusbar.zs @@ -214,6 +214,7 @@ class BaseStatusBar : StatusBarCore native virtual void Init() { + InitScoreboard(); } virtual void Draw (int state, double TicFrac) From b952e2d6fdc13fd1b52b74d6ce523c991ff90ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Mon, 19 May 2025 02:57:36 -0300 Subject: [PATCH 171/384] remove commented out debug code --- .../static/zscript/ui/statusbar/scoreboard.zs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs index 7b8952770..54e544c8a 100644 --- a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs +++ b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs @@ -110,17 +110,6 @@ extend class BaseStatusBar } } - /* - console.printf("SortedPlayers was ["); - - foreach(p : sortedPlayers) - { - console.printf("%d", p); - } - - console.printf("]"); - */ - if(teamplay && deathmatch) { Scoreboard_SortPlayers(sortedPlayers, Scoreboard_CompareByTeams); @@ -130,17 +119,6 @@ extend class BaseStatusBar Scoreboard_SortPlayers(sortedPlayers, Scoreboard_CompareByPoints); } - /* - console.printf("SortedPlayers is now ["); - - foreach(p : sortedPlayers) - { - console.printf("%d", p); - } - - console.printf("]"); - */ - int numPlayers = sortedPlayers.size(); int FontScale = max(screen.GetHeight() / 400, 1); From 51a069a1a553857ea1dfca067d0ed1baa5939e67 Mon Sep 17 00:00:00 2001 From: RaveYard <29225776+MrRaveYard@users.noreply.github.com> Date: Thu, 22 May 2025 02:21:08 +0200 Subject: [PATCH 172/384] Delete invalid polyobjects from level array in PO_Init --- src/maploader/polyobjects.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/maploader/polyobjects.cpp b/src/maploader/polyobjects.cpp index 1f662b134..12bdd27e5 100644 --- a/src/maploader/polyobjects.cpp +++ b/src/maploader/polyobjects.cpp @@ -396,13 +396,19 @@ void MapLoader::PO_Init (void) } // check for a startspot without an anchor point - for (auto &poly : Level->Polyobjects) + for (int i = 0, size = Level->Polyobjects.Size(); i < size; ++i) { + const auto& poly = Level->Polyobjects[i]; + if (poly.OriginalPts.Size() == 0) { Printf (TEXTCOLOR_RED "PO_Init: StartSpot located without an Anchor point: %d\n", poly.tag); + Level->Polyobjects.Delete(i--); + --size; } } + Level->Polyobjects.ShrinkToFit(); + InitPolyBlockMap(); // [RH] Don't need the side lists anymore From d3f965e862a0c64cba8a01b583c630339308a092 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 21 May 2025 23:50:21 -0400 Subject: [PATCH 173/384] Fixed MBF21_GunFlashTo This needs to use the original vanilla behavior for processing PSprites since it's dehacked. --- wadsrc/static/zscript/actors/mbf21.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/actors/mbf21.zs b/wadsrc/static/zscript/actors/mbf21.zs index c286f7bde..8116672d2 100644 --- a/wadsrc/static/zscript/actors/mbf21.zs +++ b/wadsrc/static/zscript/actors/mbf21.zs @@ -500,7 +500,7 @@ extend class Weapon if (!weapon) return; if (!dontchangeplayer) player.mo.PlayAttacking2(); - player.SetPsprite(PSP_FLASH, tstate); + player.SetPsprite(PSP_FLASH, tstate, true); } // needed to call A_SeekerMissile with proper defaults. From d357b7956f18f45aeb1a1dce064aaeafa4ab3aa8 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Fri, 23 May 2025 14:44:32 -0400 Subject: [PATCH 174/384] - fix ironlich whirlwind blaming the victim for its damage --- .../static/zscript/actors/heretic/ironlich.zs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/wadsrc/static/zscript/actors/heretic/ironlich.zs b/wadsrc/static/zscript/actors/heretic/ironlich.zs index b7831f045..f5d2b600d 100644 --- a/wadsrc/static/zscript/actors/heretic/ironlich.zs +++ b/wadsrc/static/zscript/actors/heretic/ironlich.zs @@ -299,29 +299,29 @@ class Whirlwind : Actor Stop; } - override int DoSpecialDamage (Actor target, int damage, Name damagetype) + override int DoSpecialDamage (Actor victim, int damage, Name damagetype) { int randVal; if (!target.bDontThrust) { - target.angle += Random2[WhirlwindDamage]() * (360 / 4096.); - target.Vel.X += Random2[WhirlwindDamage]() / 64.; - target.Vel.Y += Random2[WhirlwindDamage]() / 64.; + victim.angle += Random2[WhirlwindDamage]() * (360 / 4096.); + victim.Vel.X += Random2[WhirlwindDamage]() / 64.; + victim.Vel.Y += Random2[WhirlwindDamage]() / 64.; } - if ((Level.maptime & 16) && !target.bBoss && !target.bDontThrust) + if ((Level.maptime & 16) && !victim.bBoss && !victim.bDontThrust) { randVal = min(160, random[WhirlwindSeek]()); - target.Vel.Z += randVal / 32.; - if (target.Vel.Z > 12) + victim.Vel.Z += randVal / 32.; + if (victim.Vel.Z > 12) { - target.Vel.Z = 12; + victim.Vel.Z = 12; } } if (!(Level.maptime & 7)) { - target.DamageMobj (null, target, 3, 'Melee'); + target.DamageMobj (null, victim, 3, 'Melee'); } return -1; } From 95164cce5152b9eb0a60312e7cf923fc1e9c87ad Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sun, 18 May 2025 17:48:32 -0600 Subject: [PATCH 175/384] Exposed some skymist variables to zscript. Created zscript levellocals functions ChangeSkyMist(TextureID skymist, bool usemist = true), SetSkyFog(int density), and SetThickFog(float distance, float multiplier), so people aren't tied to MAPINFO for such things. --- src/gamedata/g_mapinfo.cpp | 4 +++- src/scripting/vmthunks.cpp | 28 ++++++++++++++++++++++++++++ wadsrc/static/zscript/constants.zs | 1 + wadsrc/static/zscript/doombase.zs | 7 +++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/gamedata/g_mapinfo.cpp b/src/gamedata/g_mapinfo.cpp index 8ee694878..c30fb4b0e 100644 --- a/src/gamedata/g_mapinfo.cpp +++ b/src/gamedata/g_mapinfo.cpp @@ -1588,7 +1588,9 @@ DEFINE_MAP_OPTION(thickfogmultiplier, false) { parse.ParseAssign(); parse.sc.MustGetFloat(); - info->thickfogmultiplier = (float)parse.sc.Float; + // [DVR] Negative multiplier does have a crazy saturated-white effect, like a nuke blastfront + // Could be useful for something some day + if ((float)parse.sc.Float > 0.0) info->thickfogmultiplier = (float)parse.sc.Float; } DEFINE_MAP_OPTION(pixelratio, false) diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 8db1e73ec..87a4858b2 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -1742,11 +1742,39 @@ DEFINE_ACTION_FUNCTION_NATIVE(_Sector, SetXOffset, SetXOffset) { PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); PARAM_INT(skymist); + PARAM_BOOL(usemist); self->skymisttexture = FSetTextureID(skymist); + if (usemist) + { + self->flags3 |= LEVEL3_SKYMIST; + } + else + { + self->flags3 &= ~LEVEL3_SKYMIST; + } InitSkyMap(self); return 0; } + DEFINE_ACTION_FUNCTION(FLevelLocals, SetSkyFog) + { + PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); + PARAM_INT(fogdensity); + self->skyfog = fogdensity; + InitSkyMap(self); + return 0; + } + + DEFINE_ACTION_FUNCTION(FLevelLocals, SetThickFog) + { + PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); + PARAM_FLOAT(distance); + PARAM_FLOAT(multiplier); + self->thickfogdistance = distance; + if (multiplier > 0.0) self->thickfogmultiplier = multiplier; + return 0; + } + DEFINE_ACTION_FUNCTION(FLevelLocals, StartIntermission) { PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index ba2598093..cf1140a10 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1420,6 +1420,7 @@ enum ELevelFlags LEVEL3_LIGHTCREATED = 0x00080000, // a light had been created in the last frame LEVEL3_NOFOGOFWAR = 0x00100000, // disables effect of r_radarclipper CVAR on this map LEVEL3_SECRET = 0x00200000, // level is a secret level + LEVEL3_SKYMIST = 0x00400000, // level skyfog uses the skymist texture }; // [RH] Compatibility flags. diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 4c5d4799c..185a27f2a 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -389,6 +389,7 @@ struct LevelInfo native native readonly String NextSecretMap; native readonly String SkyPic1; native readonly String SkyPic2; + native readonly String SkyMistPic; native readonly String F1Pic; native readonly int cluster; native readonly int partime; @@ -404,6 +405,7 @@ struct LevelInfo native native readonly int musicorder; native readonly float skyspeed1; native readonly float skyspeed2; + native readonly float skymistspeed; native readonly int cdtrack; native readonly double gravity; native readonly double aircontrol; @@ -496,8 +498,10 @@ struct LevelLocals native native readonly int musicorder; native readonly TextureID skytexture1; native readonly TextureID skytexture2; + native readonly TextureID skymisttexture; native float skyspeed1; native float skyspeed2; + native float skymistspeed; native int total_secrets; native int found_secrets; native int total_items; @@ -592,6 +596,9 @@ struct LevelLocals native native String GetChecksum() const; native void ChangeSky(TextureID sky1, TextureID sky2 ); + native void ChangeSkyMist(TextureID skymist, bool usemist = true); + native void SetSkyFog(int fogdensity); + native void SetThickFog(float distance, float multiplier); native void ForceLightning(int mode = 0, sound tempSound = ""); native clearscope Thinker CreateClientsideThinker(class type, int statnum = Thinker.STAT_DEFAULT); From d7d18c7cc0cc12b57d5fa398e5a430c7e56ccc2c Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Mon, 26 May 2025 02:06:15 -0400 Subject: [PATCH 176/384] - whoops, missed renaming one of the variables in whirlwind.DoSpecialDamage() virtual --- wadsrc/static/zscript/actors/heretic/ironlich.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/actors/heretic/ironlich.zs b/wadsrc/static/zscript/actors/heretic/ironlich.zs index f5d2b600d..d6ece9374 100644 --- a/wadsrc/static/zscript/actors/heretic/ironlich.zs +++ b/wadsrc/static/zscript/actors/heretic/ironlich.zs @@ -303,7 +303,7 @@ class Whirlwind : Actor { int randVal; - if (!target.bDontThrust) + if (!victim.bDontThrust) { victim.angle += Random2[WhirlwindDamage]() * (360 / 4096.); victim.Vel.X += Random2[WhirlwindDamage]() / 64.; From 5c74250b739f68c2a1d53052f43e37f2802bc60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 22 May 2025 21:04:21 -0300 Subject: [PATCH 177/384] return rotation with GetBonePosition --- wadsrc/static/zscript/actors/actor.zs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index aae41c81b..2d82a4d38 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1475,21 +1475,23 @@ class Actor : Thinker native // //================================================ - /* rotation, translation, scaling */ + /* rotation, translation, scaling, doesn't include parent bones */ native version("4.15.1") Quat, Vector3, Vector3 GetBone(int boneIndex, bool include_offsets = true); native version("4.15.1") Quat, Vector3, Vector3 GetNamedBone(Name boneName, bool include_offsets = true); native version("4.15.1") Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); native version("4.15.1") Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); - version("4.15.1") Vector3 GetBonePosition(int boneIndex, bool include_offsets = true) + version("4.15.1") Vector3, Vector3 GetBonePosition(int boneIndex, bool include_offsets = true) { - return TransformByBone(boneIndex, GetBoneBasePosition(boneIndex), include_offsets:include_offsets); + let [a, b] = TransformByBone(boneIndex, GetBoneBasePosition(boneIndex), include_offsets:include_offsets); + return a, b; } - version("4.15.1") Vector3 GetNamedBonePosition(name boneName, bool include_offsets = true) + version("4.15.1") Vector3, Vector3 GetNamedBonePosition(name boneName, bool include_offsets = true) { - return GetBonePosition(GetBoneIndex(boneName), include_offsets); + let [a, b] = GetBonePosition(GetBoneIndex(boneName), include_offsets); + return a, b; } //================================================ From 097c99032c52d024b80c34626748d39ef7cf0a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 23 May 2025 01:35:13 -0300 Subject: [PATCH 178/384] rename GetBone to GetBoneTRS in zscript --- src/playsim/p_actionfunctions.cpp | 4 ++-- wadsrc/static/zscript/actors/actor.zs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/playsim/p_actionfunctions.cpp b/src/playsim/p_actionfunctions.cpp index bfa2dcc16..776945da6 100644 --- a/src/playsim/p_actionfunctions.cpp +++ b/src/playsim/p_actionfunctions.cpp @@ -5947,7 +5947,7 @@ DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneBaseRotation) // //================================================ -DEFINE_ACTION_FUNCTION(AActor, GetBone) +DEFINE_ACTION_FUNCTION(AActor, GetBoneTRS) { PARAM_SELF_PROLOGUE(AActor); PARAM_INT(bone_index); @@ -5987,7 +5987,7 @@ DEFINE_ACTION_FUNCTION(AActor, GetBone) return numret; } -DEFINE_ACTION_FUNCTION(AActor, GetNamedBone) +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneTRS) { PARAM_SELF_PROLOGUE(AActor); PARAM_NAME(bone_name); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 2d82a4d38..812c1b245 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1476,8 +1476,8 @@ class Actor : Thinker native //================================================ /* rotation, translation, scaling, doesn't include parent bones */ - native version("4.15.1") Quat, Vector3, Vector3 GetBone(int boneIndex, bool include_offsets = true); - native version("4.15.1") Quat, Vector3, Vector3 GetNamedBone(Name boneName, bool include_offsets = true); + native version("4.15.1") Quat, Vector3, Vector3 GetBoneTRS(int boneIndex, bool include_offsets = true); + native version("4.15.1") Quat, Vector3, Vector3 GetNamedBoneTRS(Name boneName, bool include_offsets = true); native version("4.15.1") Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); native version("4.15.1") Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); From cffdfa802e7855e53bcd5dffef0744059a6c2752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 23 May 2025 03:20:44 -0300 Subject: [PATCH 179/384] up vector for TransformByBone/GetBonePosition --- src/playsim/actor.h | 2 +- src/playsim/p_actionfunctions.cpp | 42 +++++++++++++++++++-------- src/playsim/p_mobj.cpp | 20 ++++++++----- wadsrc/static/zscript/actors/actor.zs | 15 +++++----- 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/playsim/actor.h b/src/playsim/actor.h index c4ec8a8b8..2f51398ab 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -831,7 +831,7 @@ public: //outmat must be double[16] void GetBoneMatrix(int model_index, int bone_index, bool with_override, double *outMat); - void GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &normal); + void GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &fwd, DVector3 &up); void GetObjectToWorldMatrix(double *outMat); static AActor *StaticSpawn (FLevelLocals *Level, PClassActor *type, const DVector3 &pos, replace_t allowreplacement, bool SpawningMapThing = false); diff --git a/src/playsim/p_actionfunctions.cpp b/src/playsim/p_actionfunctions.cpp index 776945da6..d8e13ec26 100644 --- a/src/playsim/p_actionfunctions.cpp +++ b/src/playsim/p_actionfunctions.cpp @@ -6037,24 +6037,33 @@ DEFINE_ACTION_FUNCTION(AActor, TransformByBone) PARAM_FLOAT(pos_x); PARAM_FLOAT(pos_y); PARAM_FLOAT(pos_z); - PARAM_FLOAT(dir_x); - PARAM_FLOAT(dir_y); - PARAM_FLOAT(dir_z); + PARAM_FLOAT(fwd_x); + PARAM_FLOAT(fwd_y); + PARAM_FLOAT(fwd_z); + PARAM_FLOAT(up_x); + PARAM_FLOAT(up_y); + PARAM_FLOAT(up_z); PARAM_BOOL(with_override); FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); DVector3 position(pos_x, pos_y, pos_z); - DVector3 direction(dir_x, dir_y, dir_z); + DVector3 fwd(fwd_x, fwd_y, fwd_z); + DVector3 up(up_x, up_y, up_z); if(mdl) { - self->GetBonePosition(0, bone_index, with_override, position, direction); + self->GetBonePosition(0, bone_index, with_override, position, fwd, up); + } + + if(numret > 2) + { + ret[2].SetVector(up); } if(numret > 1) { - ret[1].SetVector(direction); + ret[1].SetVector(fwd); } if(numret > 0) @@ -6072,9 +6081,12 @@ DEFINE_ACTION_FUNCTION(AActor, TransformByNamedBone) PARAM_FLOAT(pos_x); PARAM_FLOAT(pos_y); PARAM_FLOAT(pos_z); - PARAM_FLOAT(dir_x); - PARAM_FLOAT(dir_y); - PARAM_FLOAT(dir_z); + PARAM_FLOAT(fwd_x); + PARAM_FLOAT(fwd_y); + PARAM_FLOAT(fwd_z); + PARAM_FLOAT(up_x); + PARAM_FLOAT(up_y); + PARAM_FLOAT(up_z); PARAM_BOOL(with_override); int bone_index; @@ -6082,16 +6094,22 @@ DEFINE_ACTION_FUNCTION(AActor, TransformByNamedBone) FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); DVector3 position(pos_x, pos_y, pos_z); - DVector3 direction(dir_x, dir_y, dir_z); + DVector3 fwd(fwd_x, fwd_y, fwd_z); + DVector3 up(up_x, up_y, up_z); if(mdl) { - self->GetBonePosition(0, bone_index, with_override, position, direction); + self->GetBonePosition(0, bone_index, with_override, position, fwd, up); + } + + if(numret > 2) + { + ret[2].SetVector(up); } if(numret > 1) { - ret[1].SetVector(direction); + ret[1].SetVector(fwd); } if(numret > 0) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 08182167b..a29c6928e 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -4309,7 +4309,7 @@ void AActor::GetBoneMatrix(int model_index, int bone_index, bool with_override, } } -void AActor::GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &normal) +void AActor::GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &fwd, DVector3 &up) { if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) { @@ -4324,20 +4324,26 @@ void AActor::GetBonePosition(int model_index, int bone_index, bool with_override FVector4 oldPos(pos.X, pos.Z, pos.Y, 1.0); FVector4 newPos; - FVector4 oldNormal(normal.X, normal.Z, normal.Y, 0.0); - FVector4 newNormal; + FVector4 oldFwd(fwd.X, fwd.Z, fwd.Y, 0.0); + FVector4 newFwd; + FVector4 oldUp(up.X, up.Z, up.Y, 0.0); + FVector4 newUp; boneMatrix.multMatrixPoint(&oldPos.X, &newPos.X); - boneMatrix.multMatrixPoint(&oldNormal.X, &newNormal.X); + boneMatrix.multMatrixPoint(&oldFwd.X, &newFwd.X); + boneMatrix.multMatrixPoint(&oldUp.X, &newUp.X); oldPos = FVector4(FVector3(newPos.X, newPos.Y, newPos.Z) / newPos.W, 1.0); - oldNormal = FVector4(FVector3(newNormal.X, newNormal.Y, newNormal.Z) / newNormal.W, 0.0); + oldFwd = FVector4(FVector3(newFwd.X, newFwd.Y, newFwd.Z) / newFwd.W, 0.0); + oldUp = FVector4(FVector3(newUp.X, newUp.Y, newUp.Z) / newUp.W, 0.0); worldMatrix.multMatrixPoint(&oldPos.X, &newPos.X); - worldMatrix.multMatrixPoint(&oldNormal.X, &newNormal.X); + worldMatrix.multMatrixPoint(&oldFwd.X, &newFwd.X); + worldMatrix.multMatrixPoint(&oldUp.X, &newUp.X); pos = DVector3(newPos.X, newPos.Z, newPos.Y); - normal = DVector3(newNormal.X, newNormal.Z, newNormal.Y); + fwd = DVector3(newFwd.X, newFwd.Z, newFwd.Y); + up = DVector3(newUp.X, newUp.Z, newUp.Y); } } diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 812c1b245..fbff082cc 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1479,19 +1479,20 @@ class Actor : Thinker native native version("4.15.1") Quat, Vector3, Vector3 GetBoneTRS(int boneIndex, bool include_offsets = true); native version("4.15.1") Quat, Vector3, Vector3 GetNamedBoneTRS(Name boneName, bool include_offsets = true); - native version("4.15.1") Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); - native version("4.15.1") Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 direction = (0,0,0), bool include_offsets = true); + //input position/direction vectors are in xzy, model space + native version("4.15.1") Vector3, Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 forward = (1,0,0), Vector3 up = (0,1,0), bool include_offsets = true); + native version("4.15.1") Vector3, Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 forward = (1,0,0) Vector3 up = (0,1,0), bool include_offsets = true); - version("4.15.1") Vector3, Vector3 GetBonePosition(int boneIndex, bool include_offsets = true) + version("4.15.1") Vector3, Vector3, Vector3 GetBonePosition(int boneIndex, bool include_offsets = true) { - let [a, b] = TransformByBone(boneIndex, GetBoneBasePosition(boneIndex), include_offsets:include_offsets); - return a, b; + let [a, b, c] = TransformByBone(boneIndex, GetBoneBasePosition(boneIndex), include_offsets:include_offsets); + return a, b, c; } version("4.15.1") Vector3, Vector3 GetNamedBonePosition(name boneName, bool include_offsets = true) { - let [a, b] = GetBonePosition(GetBoneIndex(boneName), include_offsets); - return a, b; + let [a, b, c] = GetBonePosition(GetBoneIndex(boneName), include_offsets); + return a, b, c; } //================================================ From c0c4b784cad07e26f1dbf5759c74f7ceaa656f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 23 May 2025 03:21:21 -0300 Subject: [PATCH 180/384] GetBoneEulerAngles --- src/common/utility/vectors.h | 15 +++++ src/playsim/actor.h | 1 + src/playsim/p_actionfunctions.cpp | 39 ++++++++++++ src/playsim/p_mobj.cpp | 88 +++++++++++++++++++++++++-- wadsrc/static/zscript/actors/actor.zs | 10 ++- 5 files changed, 146 insertions(+), 7 deletions(-) diff --git a/src/common/utility/vectors.h b/src/common/utility/vectors.h index a2dfae389..154d42f7c 100644 --- a/src/common/utility/vectors.h +++ b/src/common/utility/vectors.h @@ -1293,6 +1293,21 @@ public: return TAngle(double(rad * (180.0 / pi::pi()))); } + static constexpr TAngle fromCos(double cos) + { + return fromRad(g_acos(cos)); + } + + static constexpr TAngle fromSin(double sin) + { + return fromRad(g_asin(sin)); + } + + static constexpr TAngle fromTan(double tan) + { + return fromRad(g_atan(tan)); + } + static constexpr TAngle fromBam(int f) { return TAngle(f * (90. / 0x40000000)); diff --git a/src/playsim/actor.h b/src/playsim/actor.h index 2f51398ab..2737a1cf8 100644 --- a/src/playsim/actor.h +++ b/src/playsim/actor.h @@ -831,6 +831,7 @@ public: //outmat must be double[16] void GetBoneMatrix(int model_index, int bone_index, bool with_override, double *outMat); + DVector3 GetBoneEulerAngles(int model_index, int bone_index, bool with_override); void GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &fwd, DVector3 &up); void GetObjectToWorldMatrix(double *outMat); diff --git a/src/playsim/p_actionfunctions.cpp b/src/playsim/p_actionfunctions.cpp index d8e13ec26..613575ea5 100644 --- a/src/playsim/p_actionfunctions.cpp +++ b/src/playsim/p_actionfunctions.cpp @@ -6030,6 +6030,45 @@ DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneTRS) } + + +DEFINE_ACTION_FUNCTION(AActor, GetBoneEulerAngles) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_INT(bone_index); + PARAM_BOOL(with_override); + + FModel * mdl = GetBoneShared(self, 0, bone_index, nullptr); + + if(mdl) + { + ACTION_RETURN_VEC3(self->GetBoneEulerAngles(0, bone_index, with_override)); + } + + + ACTION_RETURN_VEC3(DVector3(0,0,0)); +} + +DEFINE_ACTION_FUNCTION(AActor, GetNamedBoneEulerAngles) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_NAME(bone_name); + PARAM_BOOL(with_override); + + int bone_index; + + FModel * mdl = GetBoneShared(self, 0, bone_index, &bone_name); + + if(mdl) + { + ACTION_RETURN_VEC3(self->GetBoneEulerAngles(0, bone_index, with_override)); + } + + ACTION_RETURN_VEC3(DVector3(0,0,0)); +} + + + DEFINE_ACTION_FUNCTION(AActor, TransformByBone) { PARAM_SELF_PROLOGUE(AActor); diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index a29c6928e..da81dd83b 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -4309,6 +4309,86 @@ void AActor::GetBoneMatrix(int model_index, int bone_index, bool with_override, } } +DVector3 AActor::GetBoneEulerAngles(int model_index, int bone_index, bool with_override) +{ + if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) + { + if(picnum.isValid()) return DVector3(0,0,0); // picnum overrides don't render models + + FSpriteModelFrame *smf = FindModelFrame(this, sprite, frame, false); // dropped flag is for voxels + + FVector3 objPos = FVector3(Pos() + WorldOffset); + + VSMatrix boneMatrix = (with_override ? modelData->modelBoneInfo[model_index].positions_with_override : modelData->modelBoneInfo[model_index].positions)[bone_index]; + VSMatrix worldMatrix = smf->ObjectToWorldMatrix(this, objPos.X, objPos.Y, objPos.Z, 1.0); + + FVector4 oldFwd(1.0, 0.0, 0.0, 0.0); + FVector4 newFwd; + FVector4 oldUp(0.0, 1.0, 0.0, 0.0); + FVector4 newUp; + + boneMatrix.multMatrixPoint(&oldFwd.X, &newFwd.X); + boneMatrix.multMatrixPoint(&oldUp.X, &newUp.X); + + oldFwd = FVector4(FVector3(newFwd.X, newFwd.Y, newFwd.Z).Unit(), 0.0); + oldUp = FVector4(FVector3(newUp.X, newUp.Y, newUp.Z).Unit(), 0.0); + + worldMatrix.multMatrixPoint(&oldFwd.X, &newFwd.X); + worldMatrix.multMatrixPoint(&oldUp.X, &newUp.X); + + //billion thanks to https://www.jldoty.com/code/DirectX/YPRfromUF/YPRfromUF.html for helping figure out all this math + + DVector3 fwd = DVector3(newFwd.X, newFwd.Y, newFwd.Z).Unit(); + DVector3 up = DVector3(newUp.X, newUp.Y, newUp.Z).Unit(); + + DVector3 y(0,1,0); + + DAngle yaw = DAngle::fromRad(atan2(fwd.Z, fwd.X)); + DAngle pitch = DAngle::fromSin(-fwd.Y); + DAngle roll; + + if(fwd == y) + { // gimbal lock + yaw = DAngle::fromDeg(0); + roll = DAngle::fromSin(-up.X); + } + else + { + DVector3 right = (up ^ fwd).Unit(); // fwd and up must be perpendicular, thus right must be a unit vector + + DVector3 x0 = (y ^ fwd).Unit(); // right vector with no roll, y and fwd aren't perpendicular, so it must be normalized into a unit vector + DVector3 y0 = (fwd ^ x0).Unit(); // up vector with no roll + + double cos = y0 | up; // cos of roll-less up vector and "rolled" up vector + + double sin; + + double max = std::max(std::abs(x0.Z), std::max(std::abs(x0.X), std::abs(x0.Y))); + if(std::abs(x0.X) == max) + { + assert(x0.X != 0); // at least one of x0.X, x0.Y, x0.Z must be nonzero + sin = ((y0.X * cos) - up.X) / x0.X; + } + else if(std::abs(x0.Y) == max) + { + assert(x0.Y != 0); // at least one of x0.X, x0.Y, x0.Z must be nonzero + sin = ((y0.Y * cos) - up.Y) / x0.Y; + } + else //if(std::abs(x0.Z) == max) + { + assert(x0.Z != 0); // at least one of x0.X, x0.Y, x0.Z must be nonzero + sin = ((y0.Z * cos) - up.Z) / x0.Z; + } + + roll = DAngle::fromSin(sin); + } + + return DVector3(yaw.Degrees(), pitch.Degrees(), roll.Degrees()); + } + + return DVector3(0,0,0); +} + void AActor::GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &fwd, DVector3 &up) { if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) @@ -4334,16 +4414,16 @@ void AActor::GetBonePosition(int model_index, int bone_index, bool with_override boneMatrix.multMatrixPoint(&oldUp.X, &newUp.X); oldPos = FVector4(FVector3(newPos.X, newPos.Y, newPos.Z) / newPos.W, 1.0); - oldFwd = FVector4(FVector3(newFwd.X, newFwd.Y, newFwd.Z) / newFwd.W, 0.0); - oldUp = FVector4(FVector3(newUp.X, newUp.Y, newUp.Z) / newUp.W, 0.0); + oldFwd = FVector4(FVector3(newFwd.X, newFwd.Y, newFwd.Z).Unit(), 0.0); + oldUp = FVector4(FVector3(newUp.X, newUp.Y, newUp.Z).Unit(), 0.0); worldMatrix.multMatrixPoint(&oldPos.X, &newPos.X); worldMatrix.multMatrixPoint(&oldFwd.X, &newFwd.X); worldMatrix.multMatrixPoint(&oldUp.X, &newUp.X); pos = DVector3(newPos.X, newPos.Z, newPos.Y); - fwd = DVector3(newFwd.X, newFwd.Z, newFwd.Y); - up = DVector3(newUp.X, newUp.Z, newUp.Y); + fwd = DVector3(newFwd.X, newFwd.Z, newFwd.Y).Unit(); + up = DVector3(newUp.X, newUp.Z, newUp.Y).Unit(); } } diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index fbff082cc..dc0892a3a 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -1479,9 +1479,13 @@ class Actor : Thinker native native version("4.15.1") Quat, Vector3, Vector3 GetBoneTRS(int boneIndex, bool include_offsets = true); native version("4.15.1") Quat, Vector3, Vector3 GetNamedBoneTRS(Name boneName, bool include_offsets = true); + /* angle, pitch, roll, includes parent bones */ + native version("4.15.1") Vector3 GetBoneEulerAngles(int boneIndex, bool include_offsets = true); + native version("4.15.1") Vector3 GetNamedBoneEulerAngles(Name boneName, bool include_offsets = true); + //input position/direction vectors are in xzy, model space - native version("4.15.1") Vector3, Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 forward = (1,0,0), Vector3 up = (0,1,0), bool include_offsets = true); - native version("4.15.1") Vector3, Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 forward = (1,0,0) Vector3 up = (0,1,0), bool include_offsets = true); + native version("4.15.1") Vector3, Vector3, Vector3 TransformByBone(int boneIndex, Vector3 position, Vector3 forward = (1,0,0), Vector3 up = (0,0,1), bool include_offsets = true); + native version("4.15.1") Vector3, Vector3, Vector3 TransformByNamedBone(Name boneName, Vector3 position, Vector3 forward = (1,0,0), Vector3 up = (0,0,1), bool include_offsets = true); version("4.15.1") Vector3, Vector3, Vector3 GetBonePosition(int boneIndex, bool include_offsets = true) { @@ -1489,7 +1493,7 @@ class Actor : Thinker native return a, b, c; } - version("4.15.1") Vector3, Vector3 GetNamedBonePosition(name boneName, bool include_offsets = true) + version("4.15.1") Vector3, Vector3, Vector3 GetNamedBonePosition(name boneName, bool include_offsets = true) { let [a, b, c] = GetBonePosition(GetBoneIndex(boneName), include_offsets); return a, b, c; From c2031e0af1d15347d445298a587a697b9352455a Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Tue, 27 May 2025 20:27:21 -0400 Subject: [PATCH 181/384] - fix OOB VM abort for scoreboard in teamplay games - fixes #3101 --- wadsrc/static/zscript/ui/statusbar/scoreboard.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs index 54e544c8a..b3d27a362 100644 --- a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs +++ b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs @@ -179,7 +179,7 @@ extend class BaseStatusBar y -= (BigFont.GetHeight() + 8) * CleanYfac; int numTeams = 0; - for(int i = 0; i < MAXPLAYERS; ++i) + for(int i = 0; i < MAXPLAYERS; i++) { PlayerInfo p = players[sortedPlayers[i]]; if (playeringame[sortedPlayers[i]] && Team.IsValid(p.GetTeam())) From 4bd373745a1458412f350d55ac782cb9cbe00831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 28 May 2025 00:25:35 -0300 Subject: [PATCH 182/384] fix bad loop condition --- wadsrc/static/zscript/ui/statusbar/scoreboard.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs index b3d27a362..596e58c4f 100644 --- a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs +++ b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs @@ -179,7 +179,7 @@ extend class BaseStatusBar y -= (BigFont.GetHeight() + 8) * CleanYfac; int numTeams = 0; - for(int i = 0; i < MAXPLAYERS; i++) + for(int i = 0; i < sortedPlayers.Size(); i++) { PlayerInfo p = players[sortedPlayers[i]]; if (playeringame[sortedPlayers[i]] && Team.IsValid(p.GetTeam())) From 6c42ea75189c52ac821cc663f3e763004d96657b Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Wed, 28 May 2025 01:09:24 -0400 Subject: [PATCH 183/384] - fix pointer mixup in whirlwind DoSpecialDamage() --- wadsrc/static/zscript/actors/heretic/ironlich.zs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wadsrc/static/zscript/actors/heretic/ironlich.zs b/wadsrc/static/zscript/actors/heretic/ironlich.zs index d6ece9374..c904f207f 100644 --- a/wadsrc/static/zscript/actors/heretic/ironlich.zs +++ b/wadsrc/static/zscript/actors/heretic/ironlich.zs @@ -321,7 +321,7 @@ class Whirlwind : Actor } if (!(Level.maptime & 7)) { - target.DamageMobj (null, victim, 3, 'Melee'); + victim.DamageMobj (null, target, 3, 'Melee'); } return -1; } From 486be3a5b67c31b7fdd703915bade7e5502e3748 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 29 May 2025 12:49:48 -0400 Subject: [PATCH 184/384] Clear next and prev list pointers on VTs when destroyed --- src/playsim/p_effect.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playsim/p_effect.cpp b/src/playsim/p_effect.cpp index dabf0ed16..0cbc86a64 100644 --- a/src/playsim/p_effect.cpp +++ b/src/playsim/p_effect.cpp @@ -1023,7 +1023,7 @@ void DVisualThinker::OnDestroy() _next->_prev = _prev; if (Level->VisualThinkerHead == this) Level->VisualThinkerHead = _next; - + _next = _prev = nullptr; PT.alpha = 0.0; // stops all rendering. Super::OnDestroy(); } From e7d0991798144b02133c1c03eb823e35fdfc31f8 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 28 May 2025 15:36:20 -0400 Subject: [PATCH 185/384] Fixed JIT error with Conjugate/Inverse These need to be compiler intrinsics since faux types aren't supported with self. --- src/common/engine/namedef.h | 2 ++ src/common/scripting/backend/codegen.cpp | 17 +++++++++- src/common/scripting/interface/vmnatives.cpp | 34 -------------------- src/common/scripting/jit/jit_math.cpp | 24 ++++++++++++++ src/common/scripting/vm/vmexec.h | 7 ++++ src/common/scripting/vm/vmops.h | 1 + src/common/utility/quaternion.h | 4 +-- wadsrc/static/zscript/engine/base.zs | 4 +-- 8 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/common/engine/namedef.h b/src/common/engine/namedef.h index df4c574ec..5e8c9a711 100644 --- a/src/common/engine/namedef.h +++ b/src/common/engine/namedef.h @@ -172,6 +172,8 @@ xx(Sum) xx(Unit) xx(Angle) xx(PlusZ) +xx(Conjugate) +xx(Inverse) xx(ToVector) xx(Size) xx(Push) diff --git a/src/common/scripting/backend/codegen.cpp b/src/common/scripting/backend/codegen.cpp index b9f1aa54c..82d82d893 100644 --- a/src/common/scripting/backend/codegen.cpp +++ b/src/common/scripting/backend/codegen.cpp @@ -9063,7 +9063,7 @@ FxExpression *FxMemberFunctionCall::Resolve(FCompileContext& ctx) else if (Self->IsQuaternion()) { // Reuse vector built-ins for quaternion - if (MethodName == NAME_Length || MethodName == NAME_LengthSquared || MethodName == NAME_Unit) + if (MethodName == NAME_Length || MethodName == NAME_LengthSquared || MethodName == NAME_Unit || MethodName == NAME_Conjugate || MethodName == NAME_Inverse) { if (ArgList.Size() > 0) { @@ -10361,6 +10361,9 @@ FxExpression *FxVectorBuiltin::Resolve(FCompileContext &ctx) ValueType = TypeFloat64; break; + case NAME_Conjugate: + case NAME_Inverse: + assert(Self->IsQuaternion()); case NAME_Unit: ValueType = Self->ValueType; break; @@ -10413,6 +10416,18 @@ ExpEmit FxVectorBuiltin::Emit(VMFunctionBuilder *build) build->Emit(vecSize == 2 ? OP_DIVVF2_RR : vecSize == 3 ? OP_DIVVF3_RR : OP_DIVVF4_RR, to.RegNum, op.RegNum, len.RegNum); len.Free(build); } + else if (Function == NAME_Conjugate) + { + build->Emit(OP_CONJQ, to.RegNum, op.RegNum); + } + else if (Function == NAME_Inverse) + { + ExpEmit len(build, REGT_FLOAT); + build->Emit(OP_DOTV4_RR, len.RegNum, op.RegNum, op.RegNum); + build->Emit(OP_CONJQ, to.RegNum, op.RegNum); + build->Emit(OP_DIVVF4_RR, to.RegNum, to.RegNum, len.RegNum); + len.Free(build); + } else if (Function == NAME_Angle) { build->Emit(OP_ATAN2, to.RegNum, op.RegNum + 1, op.RegNum); diff --git a/src/common/scripting/interface/vmnatives.cpp b/src/common/scripting/interface/vmnatives.cpp index 1b4cdb746..48d4ac1dc 100644 --- a/src/common/scripting/interface/vmnatives.cpp +++ b/src/common/scripting/interface/vmnatives.cpp @@ -1396,40 +1396,6 @@ DEFINE_ACTION_FUNCTION_NATIVE(_QuatStruct, SLerp, QuatSLerp) ACTION_RETURN_QUAT(quat); } -void QuatConjugate( - double x, double y, double z, double w, - DQuaternion* pquat -) -{ - *pquat = DQuaternion(x, y, z, w).Conjugate(); -} - -DEFINE_ACTION_FUNCTION_NATIVE(_QuatStruct, Conjugate, QuatConjugate) -{ - PARAM_SELF_STRUCT_PROLOGUE(DQuaternion); - - DQuaternion quat; - QuatConjugate(self->X, self->Y, self->Z, self->W, &quat); - ACTION_RETURN_QUAT(quat); -} - -void QuatInverse( - double x, double y, double z, double w, - DQuaternion* pquat -) -{ - *pquat = DQuaternion(x, y, z, w).Inverse(); -} - -DEFINE_ACTION_FUNCTION_NATIVE(_QuatStruct, Inverse, QuatInverse) -{ - PARAM_SELF_STRUCT_PROLOGUE(DQuaternion); - - DQuaternion quat; - QuatInverse(self->X, self->Y, self->Z, self->W, &quat); - ACTION_RETURN_QUAT(quat); -} - PFunction * FindFunctionPointer(PClass * cls, int fn_name) { auto fn = dyn_cast(cls->FindSymbol(ENamedName(fn_name), true)); diff --git a/src/common/scripting/jit/jit_math.cpp b/src/common/scripting/jit/jit_math.cpp index 24328b9d3..dd50d071f 100644 --- a/src/common/scripting/jit/jit_math.cpp +++ b/src/common/scripting/jit/jit_math.cpp @@ -1617,6 +1617,11 @@ void FuncMULQV3(void *result, double ax, double ay, double az, double aw, double *reinterpret_cast(result) = DQuaternion(ax, ay, az, aw) * DVector3(bx, by, bz); } +void FuncCONJQ(void* result, double x, double y, double z, double w) +{ + *reinterpret_cast(result) = DQuaternion(-x, -y, -z, w); +} + void JitCompiler::EmitMULQQ_RR() { auto stack = GetTemporaryVectorStackStorage(); @@ -1661,6 +1666,25 @@ void JitCompiler::EmitMULQV3_RR() cc.movsd(regF[A + 2], asmjit::x86::qword_ptr(tmp, 16)); } +void JitCompiler::EmitCONJQ() +{ + auto stack = GetTemporaryVectorStackStorage(); + auto tmp = newTempIntPtr(); + cc.lea(tmp, stack); + + auto call = CreateCall(FuncCONJQ); + call->setArg(0, tmp); + call->setArg(1, regF[B + 0]); + call->setArg(2, regF[B + 1]); + call->setArg(3, regF[B + 2]); + call->setArg(4, regF[B + 3]); + + cc.movsd(regF[A + 0], asmjit::x86::qword_ptr(tmp, 0)); + cc.movsd(regF[A + 1], asmjit::x86::qword_ptr(tmp, 8)); + cc.movsd(regF[A + 2], asmjit::x86::qword_ptr(tmp, 16)); + cc.movsd(regF[A + 3], asmjit::x86::qword_ptr(tmp, 24)); +} + ///////////////////////////////////////////////////////////////////////////// // Pointer math. diff --git a/src/common/scripting/vm/vmexec.h b/src/common/scripting/vm/vmexec.h index 0d5737d25..5a9cb8cc3 100644 --- a/src/common/scripting/vm/vmexec.h +++ b/src/common/scripting/vm/vmexec.h @@ -1908,6 +1908,13 @@ static int ExecScriptFunc(VMFrameStack *stack, VMReturn *ret, int numret) reinterpret_cast(reg.f[A]) = q1 * q2; } NEXTOP; + OP(CONJQ): + ASSERTF(a + 3); ASSERTF(B + 3); + { + const DQuaternion& q = reinterpret_cast(reg.f[B]); + reinterpret_cast(reg.f[A]) = q.Conjugate(); + } + NEXTOP; OP(ADDA_RR): ASSERTA(a); ASSERTA(B); ASSERTD(C); c = reg.d[C]; diff --git a/src/common/scripting/vm/vmops.h b/src/common/scripting/vm/vmops.h index 9280b746c..fa89d532a 100644 --- a/src/common/scripting/vm/vmops.h +++ b/src/common/scripting/vm/vmops.h @@ -281,6 +281,7 @@ xx(EQV4_K, beqv4, CVRK, NOP, 0, 0) // this will never be used. // Quaternion math xx(MULQQ_RR, mulqq, RVRVRV, NOP, 0, 0) // qA = qB * qC xx(MULQV3_RR, mulqv3, RVRVRV, NOP, 0, 0) // qA = qB * vC +xx(CONJQ, conjq, RVRVRV, NOP, 0, 0) // qA = qB.Conjugate // Pointer math. xx(ADDA_RR, add, RPRPRI, NOP, 0, 0) // pA = pB + dkC diff --git a/src/common/utility/quaternion.h b/src/common/utility/quaternion.h index 9f2af2619..17c88e7bf 100644 --- a/src/common/utility/quaternion.h +++ b/src/common/utility/quaternion.h @@ -292,11 +292,11 @@ public: return TVector3(r.X, r.Y, r.Z); } - TQuaternion Conjugate() + TQuaternion Conjugate() const { return TQuaternion(-X, -Y, -Z, +W); } - TQuaternion Inverse() + TQuaternion Inverse() const { return Conjugate() / LengthSquared(); } diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index 63e315649..c57db2952 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -977,11 +977,11 @@ struct QuatStruct native unsafe(internal) native static Quat NLerp(Quat from, Quat to, double t); native static Quat FromAngles(double yaw, double pitch, double roll); native static Quat AxisAngle(Vector3 xyz, double angle); - native Quat Conjugate(); - native Quat Inverse(); // native double Length(); // native double LengthSquared(); // native Quat Unit(); + // native Quat Conjugate(); + // native Quat Inverse(); } struct ScriptSavedPos From 62d258a689ee2cf023d13db63635933732226110 Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Wed, 4 Jun 2025 04:10:55 +0400 Subject: [PATCH 186/384] Exported DoubleBindings --- src/common/scripting/interface/vmnatives.cpp | 1 + wadsrc/static/zscript/engine/base.zs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/common/scripting/interface/vmnatives.cpp b/src/common/scripting/interface/vmnatives.cpp index 48d4ac1dc..bfd39261e 100644 --- a/src/common/scripting/interface/vmnatives.cpp +++ b/src/common/scripting/interface/vmnatives.cpp @@ -1283,6 +1283,7 @@ DEFINE_GLOBAL_NAMED(PClass::AllClasses, AllClasses) DEFINE_GLOBAL(AllServices) DEFINE_GLOBAL(Bindings) +DEFINE_GLOBAL(DoubleBindings) DEFINE_GLOBAL(AutomapBindings) DEFINE_GLOBAL(generic_ui) diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index c57db2952..d182f5234 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -195,6 +195,7 @@ struct _ native unsafe(internal) // These are the global variables, the struct i native internal readonly Map AllServices; native readonly bool multiplayer; native @KeyBindings Bindings; + native @KeyBindings DoubleBindings; native @KeyBindings AutomapBindings; native readonly @GameInfoStruct gameinfo; native readonly ui bool netgame; From 5e35ebc8fe698f86c8b0c4c98774bc397f30e7d4 Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Wed, 4 Jun 2025 04:21:42 +0400 Subject: [PATCH 187/384] Added two option menu items for double binds 1. DoubleTapControl: Simply assigns a double-tap key to a command. 2. DoubleControl: Assigns a standard key press to a command and uses the same key to make a double-tap bind to the second specified command. --- .../zscript/engine/ui/menu/optionmenuitems.zs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs index 39430278a..00a9b23f6 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs @@ -569,6 +569,52 @@ class OptionMenuItemControl : OptionMenuItemControlBase } } +class OptionMenuItemDoubleTapControl : OptionMenuItemControlBase +{ + OptionMenuItemDoubleTapControl Init(String label, Name command) + { + Super.Init(label, command, DoubleBindings); + return self; + } +} + +class OptionMenuItemDoubleControl : OptionMenuItemControlBase +{ + string mDoubleAction; + KeyBindings mDoubleBindings; + + OptionMenuItemDoubleControl Init(String label, Name command, Name doublecommand) + { + Super.Init(label, command, Bindings); + mDoubleAction = doublecommand; + mDoubleBindings = DoubleBindings; + return self; + } + + override bool MenuEvent(int mkey, bool fromcontroller) + { + if (mkey == Menu.MKEY_Input) + { + mWaiting = false; + mBindings.SetBind(mInput, mAction); + mDoubleBindings.SetBind(mInput, mDoubleAction); + return true; + } + else if (mkey == Menu.MKEY_Clear) + { + mBindings.UnbindACommand(mAction); + mDoubleBindings.UnbindACommand(mDoubleAction); + return true; + } + else if (mkey == Menu.MKEY_Abort) + { + mWaiting = false; + return true; + } + return false; + } +} + class OptionMenuItemMapControl : OptionMenuItemControlBase { OptionMenuItemMapControl Init(String label, Name command) From 0e3682ae24cf08c7b38fc8f7a828f54d55ba6d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Fri, 6 Jun 2025 17:13:20 -0300 Subject: [PATCH 188/384] add proper range to bone getter functions, prevents crash if called between enabling bone getters but before actually calculating bones --- src/playsim/p_mobj.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index da81dd83b..31f3975dd 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -4292,7 +4292,7 @@ void AActor::CalcBones(bool recalc) TRS AActor::GetBoneTRS(int model_index, int bone_index, bool with_override) { - if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index > 0 && bone_index > 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].bones.SSize() < bone_index) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].bones.SSize() < bone_index) { return with_override ? modelData->modelBoneInfo[model_index].bones_with_override[bone_index] : modelData->modelBoneInfo[model_index].bones[bone_index]; } @@ -4301,17 +4301,20 @@ TRS AActor::GetBoneTRS(int model_index, int bone_index, bool with_override) void AActor::GetBoneMatrix(int model_index, int bone_index, bool with_override, double *outMat) { - VSMatrix boneMatrix = (with_override ? modelData->modelBoneInfo[model_index].positions_with_override : modelData->modelBoneInfo[model_index].positions)[bone_index]; - - for(int i = 0; i < 16; i++) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].positions.SSize() < bone_index) { - outMat[i] = boneMatrix.mMatrix[i]; + VSMatrix boneMatrix = (with_override ? modelData->modelBoneInfo[model_index].positions_with_override : modelData->modelBoneInfo[model_index].positions)[bone_index]; + + for(int i = 0; i < 16; i++) + { + outMat[i] = boneMatrix.mMatrix[i]; + } } } DVector3 AActor::GetBoneEulerAngles(int model_index, int bone_index, bool with_override) { - if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].positions.SSize() < bone_index) { if(picnum.isValid()) return DVector3(0,0,0); // picnum overrides don't render models @@ -4391,7 +4394,7 @@ DVector3 AActor::GetBoneEulerAngles(int model_index, int bone_index, bool with_o void AActor::GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &fwd, DVector3 &up) { - if(modelData && modelData->flags & MODELDATA_GET_BONE_INFO) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].positions.SSize() < bone_index) { if(picnum.isValid()) return; // picnum overrides don't render models From bfefd2363e40795d47e3fa87c5a9babdb3dc6619 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 7 Jun 2025 14:10:11 -0400 Subject: [PATCH 189/384] Stop HUD messages from ticking when game is paused These are often synchronized to the world so should be treated as such. --- src/g_statusbar/shared_sbar.cpp | 39 +++++++++++++++++-------------- src/scripting/vmthunks.cpp | 20 ++++++++++++++++ wadsrc/static/zscript/doombase.zs | 2 ++ 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 27575a5c6..94715337f 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -71,6 +71,8 @@ #define XHAIRPICKUPSIZE (2+XHAIRSHRINKSIZE) #define POWERUPICONSIZE 32 +int WorldPaused(); + IMPLEMENT_CLASS(DBaseStatusBar, false, true) IMPLEMENT_POINTERS_START(DBaseStatusBar) @@ -682,31 +684,34 @@ int DBaseStatusBar::GetPlayer () void DBaseStatusBar::Tick () { - PrevCrosshairSize = CrosshairSize; - - for (size_t i = 0; i < countof(Messages); ++i) + if (!WorldPaused()) { - DHUDMessageBase *msg = Messages[i]; + PrevCrosshairSize = CrosshairSize; - while (msg) + for (size_t i = 0; i < countof(Messages); ++i) { - DHUDMessageBase *next = msg->Next; + DHUDMessageBase* msg = Messages[i]; - if (msg->CallTick ()) + while (msg) { - DetachMessage(msg); - msg->Destroy(); + DHUDMessageBase* next = msg->Next; + + if (msg->CallTick()) + { + DetachMessage(msg); + msg->Destroy(); + } + msg = next; } - msg = next; - } - // If the crosshair has been enlarged, shrink it. - if (CrosshairSize > 1.) - { - CrosshairSize -= XHAIRSHRINKSIZE; - if (CrosshairSize < 1.) + // If the crosshair has been enlarged, shrink it. + if (CrosshairSize > 1.) { - CrosshairSize = 1.; + CrosshairSize -= XHAIRSHRINKSIZE; + if (CrosshairSize < 1.) + { + CrosshairSize = 1.; + } } } } diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 87a4858b2..099a98196 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -61,6 +61,9 @@ #include "texturemanager.h" #include "v_draw.h" +extern int paused; +extern bool pauseext; + DVector2 AM_GetPosition(); int Net_GetLatency(int *ld, int *ad); void PrintPickupMessage(bool localview, const FString &str); @@ -499,6 +502,23 @@ DEFINE_ACTION_FUNCTION_NATIVE(_Sector, RemoveForceField, RemoveForceField) return 0; } +int WorldPaused() +{ + if (paused) + return true; + + if (netgame || gamestate != GS_LEVEL) + return false; + + return pauseext || menuactive == MENU_On || ConsoleState != c_up; +} + +DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, WorldPaused, WorldPaused) +{ + PARAM_PROLOGUE; + ACTION_RETURN_BOOL(WorldPaused()); +} + static sector_t *PointInSectorXY(FLevelLocals *self, double x, double y) { return self->PointInSector(x ,y); diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 185a27f2a..79a96c30d 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -625,6 +625,8 @@ struct LevelLocals native native void SpawnParticle(FSpawnParticleParams p); native VisualThinker SpawnVisualThinker(Class type); + + clearscope native static bool WorldPaused(); } // a few values of this need to be readable by the play code. From b0624279427cd31d6b929d677d7621c4662bbf47 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Mon, 9 Jun 2025 14:31:25 -0400 Subject: [PATCH 190/384] - github is retiring these, if i do this will they stop sending me emails about it? --- .github/workflows/continuous_integration.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 7efd95cef..a5c2fb4e6 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -18,10 +18,6 @@ jobs: os: windows-2022 build_type: Debug - - name: Visual Studio 2019 - os: windows-2019 - build_type: Release - - name: macOS os: macos-14 deps_cmdline: brew install libvpx From abfe5601c7b0e792070208769de63a3f990d7d0d Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sun, 8 Jun 2025 16:52:00 -0400 Subject: [PATCH 191/384] Check for SDL_JOYDEVICEADDED and SDL_JOYDEVICEREMOVED --- src/common/platform/posix/sdl/i_input.cpp | 8 +++++++ src/common/platform/posix/sdl/i_joystick.cpp | 23 +++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp index 02cf31e45..0148eaad2 100644 --- a/src/common/platform/posix/sdl/i_input.cpp +++ b/src/common/platform/posix/sdl/i_input.cpp @@ -32,6 +32,7 @@ */ #include #include "m_argv.h" +#include "m_joy.h" #include "v_video.h" #include "d_eventbase.h" @@ -467,6 +468,13 @@ void MessagePump (const SDL_Event &sev) if(event.data1 != 0) D_PostEvent(&event); break; + + case SDL_JOYDEVICEADDED: + case SDL_JOYDEVICEREMOVED: + I_UpdateDeviceList(); + event.type = EV_DeviceChange; + D_PostEvent (&event); + break; } } diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index 42837ecba..c08686465 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -281,7 +281,22 @@ class SDLInputJoystickManager public: SDLInputJoystickManager() { - for(int i = 0;i < SDL_NumJoysticks();i++) + this->UpdateDeviceList(); + } + ~SDLInputJoystickManager() + { + for(unsigned int i = 0;i < Joysticks.Size();i++) + delete Joysticks[i]; + } + + void UpdateDeviceList() + { + for (int i = 0; i < Joysticks.SSize(); i++) + { + delete Joysticks[i]; + } + Joysticks.clear(); + for(int i = 0; i < SDL_NumJoysticks(); i++) { SDLInputJoystick *device = new SDLInputJoystick(i); if(device->IsValid()) @@ -290,11 +305,6 @@ public: delete device; } } - ~SDLInputJoystickManager() - { - for(unsigned int i = 0;i < Joysticks.Size();i++) - delete Joysticks[i]; - } void AddAxes(float axes[NUM_JOYAXIS]) { @@ -364,5 +374,6 @@ void I_ProcessJoysticks() IJoystickConfig *I_UpdateDeviceList() { + JoystickManager->UpdateDeviceList(); return NULL; } From 481848b3c55cd399458f5ae0d8b9b254a8dd0921 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 9 Jun 2025 09:06:25 -0400 Subject: [PATCH 192/384] Now with TDeletingArray --- src/common/platform/posix/sdl/i_joystick.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index c08686465..8dc9fd8ac 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -283,19 +283,10 @@ public: { this->UpdateDeviceList(); } - ~SDLInputJoystickManager() - { - for(unsigned int i = 0;i < Joysticks.Size();i++) - delete Joysticks[i]; - } void UpdateDeviceList() { - for (int i = 0; i < Joysticks.SSize(); i++) - { - delete Joysticks[i]; - } - Joysticks.clear(); + Joysticks.DeleteAndClear(); for(int i = 0; i < SDL_NumJoysticks(); i++) { SDLInputJoystick *device = new SDLInputJoystick(i); @@ -311,6 +302,7 @@ public: for(unsigned int i = 0;i < Joysticks.Size();i++) Joysticks[i]->AddAxes(axes); } + void GetDevices(TArray &sticks) { for(unsigned int i = 0;i < Joysticks.Size();i++) @@ -325,8 +317,9 @@ public: for(unsigned int i = 0;i < Joysticks.Size();++i) if(Joysticks[i]->Enabled) Joysticks[i]->ProcessInput(); } + protected: - TArray Joysticks; + TDeletingArray Joysticks; }; static SDLInputJoystickManager *JoystickManager; From a8eed72b38047c5e7e060f1f6b5a232169b8236f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Mon, 9 Jun 2025 17:35:20 -0300 Subject: [PATCH 193/384] =?UTF-8?q?wrong=20comparison=20sign=20?= =?UTF-8?q?=F0=9F=A4=A6=E2=80=8D=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/playsim/p_mobj.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 31f3975dd..115c1495f 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -4292,7 +4292,7 @@ void AActor::CalcBones(bool recalc) TRS AActor::GetBoneTRS(int model_index, int bone_index, bool with_override) { - if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].bones.SSize() < bone_index) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() > model_index && modelData->modelBoneInfo[model_index].bones.SSize() > bone_index) { return with_override ? modelData->modelBoneInfo[model_index].bones_with_override[bone_index] : modelData->modelBoneInfo[model_index].bones[bone_index]; } @@ -4301,7 +4301,7 @@ TRS AActor::GetBoneTRS(int model_index, int bone_index, bool with_override) void AActor::GetBoneMatrix(int model_index, int bone_index, bool with_override, double *outMat) { - if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].positions.SSize() < bone_index) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() > model_index && modelData->modelBoneInfo[model_index].positions.SSize() > bone_index) { VSMatrix boneMatrix = (with_override ? modelData->modelBoneInfo[model_index].positions_with_override : modelData->modelBoneInfo[model_index].positions)[bone_index]; @@ -4314,7 +4314,7 @@ void AActor::GetBoneMatrix(int model_index, int bone_index, bool with_override, DVector3 AActor::GetBoneEulerAngles(int model_index, int bone_index, bool with_override) { - if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].positions.SSize() < bone_index) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() > model_index && modelData->modelBoneInfo[model_index].positions.SSize() > bone_index) { if(picnum.isValid()) return DVector3(0,0,0); // picnum overrides don't render models @@ -4394,7 +4394,7 @@ DVector3 AActor::GetBoneEulerAngles(int model_index, int bone_index, bool with_o void AActor::GetBonePosition(int model_index, int bone_index, bool with_override, DVector3 &pos, DVector3 &fwd, DVector3 &up) { - if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() < model_index && modelData->modelBoneInfo[model_index].positions.SSize() < bone_index) + if(modelData && (modelData->flags & MODELDATA_GET_BONE_INFO) && model_index >= 0 && bone_index >= 0 && modelData->modelBoneInfo.SSize() > model_index && modelData->modelBoneInfo[model_index].positions.SSize() > bone_index) { if(picnum.isValid()) return; // picnum overrides don't render models From 11809748a13ae088395e59c763b0f9114a4585a0 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Tue, 10 Jun 2025 23:03:22 -0400 Subject: [PATCH 194/384] Fixed outdated example in config file Config file said to use 'doom.doom2.Autoload' and 'doom.doom2.commercial.Autoload', which do not seem to work anymore. Replaced with 'doom.id.doom2.Autoload' and 'doom.id.doom2.commercial.Autoload', respectively. --- src/gameconfigfile.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gameconfigfile.cpp b/src/gameconfigfile.cpp index b61279ebc..efdad2fd5 100644 --- a/src/gameconfigfile.cpp +++ b/src/gameconfigfile.cpp @@ -299,9 +299,9 @@ void FGameConfigFile::DoAutoloadSetup (FIWadManager *iwad_man) "# playing. You may have have files that are loaded for all similar IWADs\n" "# (the game) and files that are only loaded for particular IWADs. For example,\n" "# any files listed under 'doom.Autoload' will be loaded for any version of Doom,\n" - "# but files listed under 'doom.doom2.Autoload' will only load when you are\n" - "# playing a Doom 2 based game (doom2.wad, tnt.wad or plutonia.wad), and files listed under\n" - "# 'doom.doom2.commercial.Autoload' only when playing doom2.wad.\n\n"); + "# but files listed under 'doom.id.doom2.Autoload' will only load when you are\n" + "# playing a Doom 2 based game (doom2.wad, tnt.wad or plutonia.wad), and files\n" + "# listed under 'doom.id.doom2.commercial.Autoload' only when playing doom2.wad.\n\n"); } void FGameConfigFile::DoGlobalSetup () From e435a4ef649585d47c3e29ca626312f49c7012d8 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 12 Jun 2025 10:38:50 -0400 Subject: [PATCH 195/384] Fixed player assignments when loading multiple players --- src/p_saveg.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp index 99c77b1aa..a4d82fea6 100644 --- a/src/p_saveg.cpp +++ b/src/p_saveg.cpp @@ -734,8 +734,7 @@ void FLevelLocals::ReadMultiplePlayers(FSerializer &arc, int numPlayers, bool fr { TArray tempPlayers = {}; tempPlayers.Reserve(numPlayers); - TArray assignedPlayers = {}; - assignedPlayers.Reserve(MAXPLAYERS); + bool assignedPlayers[MAXPLAYERS] = {}; // Read all the save game players into a temporary array for (auto& p : tempPlayers) From 033ea084a6cd5f402a1d3578da4f8edb92d07eab Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Fri, 13 Jun 2025 03:58:12 +0800 Subject: [PATCH 196/384] Lightmap parsing update --- specs/udmf_zdoom.txt | 1 + src/maploader/udmf.cpp | 10 ++++++++++ src/namedef_custom.h | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/specs/udmf_zdoom.txt b/specs/udmf_zdoom.txt index ff0f50e4a..32f9b7ffb 100644 --- a/specs/udmf_zdoom.txt +++ b/specs/udmf_zdoom.txt @@ -431,6 +431,7 @@ Note: All fields default to false unless mentioned otherwise. lm_sunintensity = ; // lightmap sun intensity multiplier. Default = 1.0 [VKDOOM] lm_ao = ; // enables ambient occlusion baking with VKTool. [VKDOOM] lm_bounce = ; // enables bounce light baking with VKTool. [VKDOOM] + lm_dynamic = ; // enables dynamic lightmap updates for the whole map (without having to do it per sector). Use with care as this can drag down performance. Default = false [VKDOOM] // Dynamic and lightmap light fields [VKDOOM] light_softshadowradius = ; // lightmap light and raytraced dynamic light soft shadow amount. Higher values produce softer shadows. Default = 5.0 [VKDOOM] diff --git a/src/maploader/udmf.cpp b/src/maploader/udmf.cpp index 1fd9869c3..7a2e20b35 100644 --- a/src/maploader/udmf.cpp +++ b/src/maploader/udmf.cpp @@ -792,8 +792,17 @@ public: break; case NAME_light_softshadowradius: + case NAME_light_linearity: + case NAME_light_noshadowmap: + case NAME_light_dontlightactors: + case NAME_light_dontlightmap: + case NAME_light_shadowminquality: case NAME_lm_suncolor: + case NAME_lm_sunintensity: case NAME_lm_sampledist: + case NAME_lm_bounce: + case NAME_lm_ao: + case NAME_lm_dynamic: CHECK_N(Zd | Zdt) break; @@ -2205,6 +2214,7 @@ public: case NAME_lm_sampledist_floor: case NAME_lm_sampledist_ceiling: + case NAME_lm_dynamic: CHECK_N(Zd | Zdt) break; diff --git a/src/namedef_custom.h b/src/namedef_custom.h index cefcd6787..bfa97008c 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -871,9 +871,17 @@ xx(lm_sampledist_floor) xx(lm_sampledist_ceiling) xx(lm_dynamic) xx(lm_suncolor) +xx(lm_sunintensity) +xx(lm_bounce) +xx(lm_ao) // Light keywords xx(light_softshadowradius) +xx(light_linearity) +xx(light_noshadowmap) +xx(light_dontlightactors) +xx(light_dontlightmap) +xx(light_shadowminquality) xx(skew_bottom_type) xx(skew_middle_type) From 264168921653b5facfdf0aac220e010c6c59ad91 Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Fri, 13 Jun 2025 01:09:20 +0800 Subject: [PATCH 197/384] Expose direct inventory hotkeys for Heretic, Hexen and Strife in the controls menu --- wadsrc/static/menudef.txt | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 27d11edef..148ed9e91 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -640,6 +640,56 @@ OptionMenu "InventoryControlsMenu" protected StaticText "" Control "$CNTRLMNU_DROPWEAPON" , "weapdrop" + + IfGame(Heretic, Hexen, Strife) + { + StaticText "" + StaticText "$CNTRLMNU_INVENTORY_HOTKEYS" + } + + IfGame(Heretic) + { + Control "$CNTRLMNU_ARTI_CHAOS", "use ArtiTeleport" + Control "$CNTRLMNU_ARTI_EGG", "use ArtiEgg" + Control "$CNTRLMNU_ARTI_URN", "use ArtiSuperHealth" + Control "$CNTRLMNU_ARTI_FLASK", "use ArtiHealth" + Control "$CNTRLMNU_ARTI_RING", "use ArtiInvulnerability" + Control "$CNTRLMNU_ARTI_SHADOW", "use ArtiInvisibility" + Control "$CNTRLMNU_ARTI_BOMB", "use ArtiTimeBomb" + Control "$CNTRLMNU_ARTI_TOME", "use ArtiTomeOfPower" + Control "$CNTRLMNU_ARTI_TORCH", "use ArtiTorch" + Control "$CNTRLMNU_ARTI_WINGS", "use ArtiFly" + } + + IfGame(Hexen) + { + Control "$CNTRLMNU_ARTI_BANISH", "use ArtiTeleportOther" + Control "$CNTRLMNU_ARTI_BOOTS", "use ArtiSpeedboots" + Control "$CNTRLMNU_ARTI_CHAOS", "use ArtiTeleport" + Control "$CNTRLMNU_ARTI_SERVANT", "use ArtiDarkServant" + Control "$CNTRLMNU_ARTI_BRACERS", "use ArtiBoostArmor" + Control "$CNTRLMNU_ARTI_FLECHETTE", "useflechette" + Control "$CNTRLMNU_ARTI_RING", "use ArtiInvulnerability2" + Control "$CNTRLMNU_ARTI_KRATER", "use ArtiBoostMana" + Control "$CNTRLMNU_ARTI_INCANT", "use ArtiHealingRadius" + Control "$CNTRLMNU_ARTI_URN", "use ArtiSuperHealth" + Control "$CNTRLMNU_ARTI_PORK", "use ArtiPork" + Control "$CNTRLMNU_ARTI_FLASK", "use ArtiHealth" + Control "$CNTRLMNU_ARTI_TORCH", "use ArtiTorch" + Control "$CNTRLMNU_ARTI_WINGS", "use ArtiFly" + } + + IfGame(Strife) + { + //Control "$CNTRLMNU_INV_STRIFEHEALTH", "usestrifehealth" + Control "$CNTRLMNU_INV_LARMOR", "use LeatherArmor" + Control "$CNTRLMNU_INV_MARMOR", "use MetalArmor" + Control "$CNTRLMNU_INV_ENVSUIT", "use EnvironmentalSuit" + Control "$CNTRLMNU_INV_SHADOWARMOR", "use ShadowArmor" + Control "$CNTRLMNU_INV_BEACON", "use TeleporterBeacon" + Control "$CNTRLMNU_INV_TARGETER", "use Targeter" + Control "$CNTRLMNU_INV_SCANNER", "use Scanner" + } } OptionMenu "OtherControlsMenu" protected From 8be9f700d9b4b6e3d15a001d3e72911b40b402a1 Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Fri, 13 Jun 2025 12:50:26 +0800 Subject: [PATCH 198/384] Some fixes for the inventory hotkey localizations --- wadsrc/static/language.def | 30 ++++++++++++++++++++++++++++++ wadsrc/static/menudef.txt | 4 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/wadsrc/static/language.def b/wadsrc/static/language.def index 2700fbb20..67dadfcd0 100644 --- a/wadsrc/static/language.def +++ b/wadsrc/static/language.def @@ -414,3 +414,33 @@ TAG_KEY_CASTLE = "$$TXT_KEY_CASTLE"; TAG_ITEMHEALTH = "$$TXT_ITEMHEALTH"; OPTMNU_SWRENDERER = "$$DSPLYMNU_SWOPT"; + +CNTRLMNU_ARTI_CHAOS = "$$TAG_ARTITELEPORT"; +CNTRLMNU_ARTI_EGG = "$$TAG_ARTIEGG"; +CNTRLMNU_ARTI_URN = "$$TAG_ARTISUPERHEALTH"; +CNTRLMNU_ARTI_FLASK = "$$TAG_ARTIHEALTH"; +CNTRLMNU_ARTI_RING = "$$TAG_ARTIINVULNERABILITY"; +CNTRLMNU_ARTI_SHADOW = "$$TAG_ARTIINVISIBILITY"; +CNTRLMNU_ARTI_BOMB = "$$TAG_ARTIFIREBOMB"; +CNTRLMNU_ARTI_TOME = "$$TAG_ARTITOMEOFPOWER"; +CNTRLMNU_ARTI_TORCH = "$$TAG_ARTITORCH"; +CNTRLMNU_ARTI_WINGS = "$$TAG_ARTIFLY"; + +CNTRLMNU_ARTI_BANISH = "$$TAG_ARTITELEPORTOTHER"; +CNTRLMNU_ARTI_BOOTS = "$$TAG_ARTISPEED"; +CNTRLMNU_ARTI_SERVANT = "$$TAG_ARTISUMMON"; +CNTRLMNU_ARTI_BRACERS = "$$TAG_ARTIBOOSTARMOR"; +CNTRLMNU_ARTI_FLECHETTE = "$$TAG_ARTIPOISONBAG"; +CNTRLMNU_ARTI_ICON = "$$TAG_ARTIDEFENDER"; +CNTRLMNU_ARTI_KRATER = "$$TAG_ARTIBOOSTMANA"; +CNTRLMNU_ARTI_INCANT = "$$TAG_ARTIHEALINGRADIUS"; +CNTRLMNU_ARTI_PORK = "$$TAG_ARTIPORK"; + +CNTRLMNU_INV_SURGERYKIT = "$$TAG_SURGERYKIT"; +CNTRLMNU_INV_LARMOR = "$$TAG_LEATHER"; +CNTRLMNU_INV_MARMOR = "$$TAG_METALARMOR"; +CNTRLMNU_INV_ENVSUIT = "$$TAG_ENVSUIT"; +CNTRLMNU_INV_SHADOWARMOR = "$$TAG_SHADOWARMOR"; +CNTRLMNU_INV_BEACON = "$$TAG_TELEPORTERBEACON"; +CNTRLMNU_INV_TARGETER = "$$TAG_TARGETER"; +CNTRLMNU_INV_SCANNER = "$$TAG_SCANNER"; diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 148ed9e91..44e324612 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -669,7 +669,7 @@ OptionMenu "InventoryControlsMenu" protected Control "$CNTRLMNU_ARTI_SERVANT", "use ArtiDarkServant" Control "$CNTRLMNU_ARTI_BRACERS", "use ArtiBoostArmor" Control "$CNTRLMNU_ARTI_FLECHETTE", "useflechette" - Control "$CNTRLMNU_ARTI_RING", "use ArtiInvulnerability2" + Control "$CNTRLMNU_ARTI_ICON", "use ArtiInvulnerability2" Control "$CNTRLMNU_ARTI_KRATER", "use ArtiBoostMana" Control "$CNTRLMNU_ARTI_INCANT", "use ArtiHealingRadius" Control "$CNTRLMNU_ARTI_URN", "use ArtiSuperHealth" @@ -681,7 +681,7 @@ OptionMenu "InventoryControlsMenu" protected IfGame(Strife) { - //Control "$CNTRLMNU_INV_STRIFEHEALTH", "usestrifehealth" + Control "$CNTRLMNU_INV_SURGERYKIT", "use SurgeryKit" Control "$CNTRLMNU_INV_LARMOR", "use LeatherArmor" Control "$CNTRLMNU_INV_MARMOR", "use MetalArmor" Control "$CNTRLMNU_INV_ENVSUIT", "use EnvironmentalSuit" From c6825a9881e686a790706e24acb99f1e44b4d486 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 7 Jun 2025 22:42:48 -0400 Subject: [PATCH 199/384] Added ViewModelFOV field for models Allows manually setting FOV for models instead of scaling from 90 degrees. Positive values are exact FOVs while negative FOVs are scalars from 90. SCALEWEAPONFOV does not work with exact values since it automatically scales based on FOV. --- src/common/models/model.h | 1 + src/r_data/models.cpp | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/common/models/model.h b/src/common/models/model.h index 3fa293073..c2b379d2f 100644 --- a/src/common/models/model.h +++ b/src/common/models/model.h @@ -47,6 +47,7 @@ struct FSpriteModelFrame float xrotate, yrotate, zrotate; float rotationCenterX, rotationCenterY, rotationCenterZ; float rotationSpeed; + float viewModelFOV; private: unsigned int flags; public: diff --git a/src/r_data/models.cpp b/src/r_data/models.cpp index 166387cd9..8a469cf2b 100644 --- a/src/r_data/models.cpp +++ b/src/r_data/models.cpp @@ -250,12 +250,23 @@ void RenderHUDModel(FModelRenderer *renderer, DPSprite *psp, FVector3 translatio // but we need to position it correctly in the world for light to work properly. VSMatrix objectToWorldMatrix = renderer->GetViewToWorldMatrix(); - // [Nash] Optional scale weapon FOV float fovscale = 1.0f; - if (smf_flags & MDL_SCALEWEAPONFOV) + if (smf->viewModelFOV <= 0.0f) { - fovscale = tan(players[consoleplayer].DesiredFOV * (0.5f * M_PI / 180.f)); - fovscale = 1.f + (fovscale - 1.f) * cl_scaleweaponfov; + if (smf->viewModelFOV < 0.0f) + fovscale = 1.0f / fabs(smf->viewModelFOV); + + // [Nash] Optional scale weapon FOV + if (smf_flags & MDL_SCALEWEAPONFOV) + { + float newScale = tan(players[consoleplayer].DesiredFOV * (0.5f * M_PI / 180.f)); + newScale = 1.f + (newScale - 1.f) * cl_scaleweaponfov; + fovscale *= newScale; + } + } + else if (players[consoleplayer].DesiredFOV != smf->viewModelFOV) + { + fovscale = tan(players[consoleplayer].DesiredFOV * (0.5f * M_PI / 180.f)) / tan(smf->viewModelFOV * (0.5f * M_PI / 180.f)); } // Scaling model (y scale for a sprite means height, i.e. z in the world!). @@ -935,6 +946,13 @@ void ParseModelDefLump(int Lump) { smf.flags |= MDL_MODELSAREATTACHMENTS; } + else if (sc.Compare("viewmodelfov")) + { + sc.MustGetFloat(); + smf.viewModelFOV = sc.Float; + if (smf.viewModelFOV > 0.0f) + smf.viewModelFOV = min(smf.viewModelFOV, 175.0f); + } else if (sc.Compare("rotating")) { smf.flags |= MDL_ROTATING; From 4e71ec9d46cedcc2b0678c8da82090e2cc40df5b Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 14 Jun 2025 09:47:48 -0400 Subject: [PATCH 200/384] Fixed camera interpolating when using outdated information If it's been more than a tick since the last render then disable interpolation as the data for the Actor at this point is likely too outdated. Also fixes quaking while the console and menu are open. --- src/rendering/r_utility.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/rendering/r_utility.cpp b/src/rendering/r_utility.cpp index adc3df2f1..ab0a8c6e6 100644 --- a/src/rendering/r_utility.cpp +++ b/src/rendering/r_utility.cpp @@ -922,6 +922,8 @@ static void R_DoActorTickerAngleChanges(player_t* const player, DRotator& angles EXTERN_CVAR(Float, chase_dist) EXTERN_CVAR(Float, chase_height) +int WorldPaused(); + void R_SetupFrame(FRenderViewpoint& viewPoint, const FViewWindow& viewWindow, AActor* const actor) { viewPoint.TicFrac = I_GetTimeFrac(); @@ -958,8 +960,17 @@ void R_SetupFrame(FRenderViewpoint& viewPoint, const FViewWindow& viewWindow, AA iView->AngleOffsets.Zero(); if (iView->prevTic != -1 && curTic > iView->prevTic) { - iView->prevTic = curTic; - iView->Old = iView->New; + // If it's been more than a tic since it was rendered, don't interpolate + // from whatever last position it had (it's probably no longer valid). + if (curTic - iView->prevTic > 1) + { + iView->prevTic = -1; + } + else + { + iView->prevTic = curTic; + iView->Old = iView->New; + } } const auto& mainView = r_viewpoint; @@ -1034,7 +1045,7 @@ void R_SetupFrame(FRenderViewpoint& viewPoint, const FViewWindow& viewWindow, AA viewPoint.sector = viewPoint.ViewLevel->PointInRenderSubsector(camPos)->sector; } - if (!paused) + if (!WorldPaused()) { FQuakeJiggers jiggers; if (DEarthquake::StaticGetQuakeIntensities(viewPoint.TicFrac, viewPoint.camera, jiggers) > 0) From 989a355f8081958f8e2222b0497cd0fe99b7cb7a Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 14 Jun 2025 10:45:38 -0400 Subject: [PATCH 201/384] Moved BobTimer to playerinfo This was a bit too invasive for mods that used full PlayerThink overrides. --- src/playsim/d_player.h | 1 + src/playsim/p_user.cpp | 5 +++++ wadsrc/static/zscript/actors/player/player.zs | 9 ++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/playsim/d_player.h b/src/playsim/d_player.h index 025355d95..536da4aed 100644 --- a/src/playsim/d_player.h +++ b/src/playsim/d_player.h @@ -335,6 +335,7 @@ public: double viewheight = 0; // base height above floor for viewz double deltaviewheight = 0; // squat speed. double bob = 0; // bounded/scaled total velocity + int BobTimer = 0; // killough 10/98: used for realistic bobbing (i.e. not simply overall speed) // mo->velx and mo->vely represent true velocity experienced by player. diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 28dd271b7..ccc75998e 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -284,6 +284,7 @@ void player_t::CopyFrom(player_t &p, bool copyPSP) viewheight = p.viewheight; deltaviewheight = p.deltaviewheight; bob = p.bob; + BobTimer = p.BobTimer; Vel = p.Vel; centering = p.centering; turnticks = p.turnticks; @@ -1294,6 +1295,8 @@ void P_PlayerThink (player_t *player) player->LastSafePos = player->mo->Pos(); } + ++player->BobTimer; + // Bots do not think in freeze mode. if (player->mo->Level->isFrozen() && player->Bot != nullptr) { @@ -1710,6 +1713,7 @@ void player_t::Serialize(FSerializer &arc) ("viewheight", viewheight) ("deltaviewheight", deltaviewheight) ("bob", bob) + ("bobtimer", BobTimer) ("vel", Vel) ("centering", centering) ("health", health) @@ -1819,6 +1823,7 @@ DEFINE_FIELD_X(PlayerInfo, player_t, viewz) DEFINE_FIELD_X(PlayerInfo, player_t, viewheight) DEFINE_FIELD_X(PlayerInfo, player_t, deltaviewheight) DEFINE_FIELD_X(PlayerInfo, player_t, bob) +DEFINE_FIELD_X(PlayerInfo, player_t, BobTimer) DEFINE_FIELD_X(PlayerInfo, player_t, Vel) DEFINE_FIELD_X(PlayerInfo, player_t, centering) DEFINE_FIELD_X(PlayerInfo, player_t, turnticks) diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index 7eab28a19..f6d12a81e 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -18,7 +18,6 @@ class PlayerPawn : Actor // 16 pixels of bob const MAXBOB = 16.; - int BobTimer; // Use a local timer for this so it can be predicted correctly. int crouchsprite; int MaxHealth; int BonusHealth; @@ -634,7 +633,7 @@ class PlayerPawn : Actor { if (player.health > 0) { - angle = BobTimer / (120 * TICRATE / 35.) * 360.; + angle = player.BobTimer / (120 * TICRATE / 35.) * 360.; bob = player.GetStillBob() * sin(angle); } else @@ -644,7 +643,7 @@ class PlayerPawn : Actor } else { - angle = BobTimer / (ViewBobSpeed * TICRATE / 35.) * 360.; + angle = player.BobTimer / (ViewBobSpeed * TICRATE / 35.) * 360.; bob = player.bob * sin(angle) * (waterlevel > 1 ? 0.25f : 0.5f); } @@ -1668,7 +1667,6 @@ class PlayerPawn : Actor PlayerFlags |= PF_VOODOO_ZOMBIE; } - ++BobTimer; CheckFOV(); CheckCheats(); @@ -2543,7 +2541,7 @@ class PlayerPawn : Actor for (int i = 0; i < 2; i++) { // Bob the weapon based on movement speed. ([SP] And user's bob speed setting) - double angle = (BobSpeed * player.GetWBobSpeed() * 35 / TICRATE*(BobTimer - 1 + i)) * (360. / 8192.); + double angle = (BobSpeed * player.GetWBobSpeed() * 35 / TICRATE*(player.BobTimer - 1 + i)) * (360. / 8192.); // [RH] Smooth transitions between bobbing and not-bobbing frames. // This also fixes the bug where you can "stick" a weapon off-center by @@ -2894,6 +2892,7 @@ struct PlayerInfo native play // self is what internally is known as player_t native double viewheight; native double deltaviewheight; native double bob; + native int BobTimer; native vector2 vel; native bool centering; native uint8 turnticks; From e981064e5cdb999c024e4c133136ceb946cc493d Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 14 Jun 2025 11:18:42 -0400 Subject: [PATCH 202/384] Fixed BT_RUN getting toggled off on command clear This doesn't get delta'd when networking so its state has to be kept between wipes since it's built entirely from client data. --- src/d_net.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/d_net.cpp b/src/d_net.cpp index b44bdbafd..b92831db9 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -407,7 +407,9 @@ void Net_ResetCommands(bool midTic) // Make sure not to run its current command either. auto& curTic = state.Tics[tic % BACKUPTICS]; + const int running = (curTic.Command.buttons & BT_RUN); // This isn't delta'd so needs to be kept. memset(&curTic.Command, 0, sizeof(curTic.Command)); + curTic.Command.buttons |= running; } NetEvents.ResetStream(); From 1e281bfce2fe525b10f4447537446383c9b4a729 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sun, 15 Jun 2025 15:40:11 -0400 Subject: [PATCH 203/384] Added player iterators Allows for easily iterating through players currently in the game. --- src/playsim/d_player.h | 3 ++ src/playsim/p_user.cpp | 35 +++++++++++++++++++ wadsrc/static/zscript/actors/player/player.zs | 3 ++ 3 files changed, 41 insertions(+) diff --git a/src/playsim/d_player.h b/src/playsim/d_player.h index 536da4aed..6e03436e0 100644 --- a/src/playsim/d_player.h +++ b/src/playsim/d_player.h @@ -477,6 +477,9 @@ public: bool HasWeaponsInSlot(int slot) const; bool Resurrect(); + static player_t* GetNextPlayer(player_t* p, bool noBots = false); + static int GetNextPlayerNumber(int pNum, bool noBots = false); + // Scaled angle adjustment info. Not for direct manipulation. DRotator angleOffsetTargets; }; diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index ccc75998e..1f109d808 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -746,6 +746,41 @@ DEFINE_ACTION_FUNCTION(_PlayerInfo, Resurrect) ACTION_RETURN_BOOL(self->Resurrect()); } +player_t* player_t::GetNextPlayer(player_t* p, bool noBots) +{ + int pNum = player_t::GetNextPlayerNumber(p == nullptr ? -1 : p - players); + return pNum != -1 ? &players[pNum] : nullptr; +} + +DEFINE_ACTION_FUNCTION_NATIVE(_PlayerInfo, GetNextPlayer, player_t::GetNextPlayer) +{ + PARAM_PROLOGUE; + PARAM_POINTER(p, player_t); + PARAM_BOOL(noBots); + + ACTION_RETURN_POINTER(player_t::GetNextPlayer(p, noBots)); +} + +int player_t::GetNextPlayerNumber(int pNum, bool noBots) +{ + int i = max(pNum + 1, 0); + for (; i < MaxClients; ++i) + { + if (playeringame[i] && (!noBots || players[i].Bot == nullptr)) + break; + } + + return i < MaxClients ? i : -1; +} + +DEFINE_ACTION_FUNCTION_NATIVE(_PlayerInfo, GetNextPlayerNumber, player_t::GetNextPlayerNumber) +{ + PARAM_PROLOGUE; + PARAM_INT(pNum); + PARAM_BOOL(noBots); + + ACTION_RETURN_INT(player_t::GetNextPlayerNumber(pNum, noBots)); +} DEFINE_ACTION_FUNCTION(_PlayerInfo, GetUserName) { diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index f6d12a81e..a64fca8f9 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -3004,6 +3004,9 @@ struct PlayerInfo native play // self is what internally is known as player_t native clearscope int GetAverageLatency() const; + native clearscope static PlayerInfo GetNextPlayer(PlayerInfo p, bool noBots = false); + native clearscope static int GetNextPlayerNumber(int pNum, bool noBots = false); + // The actual implementation is on PlayerPawn where it can be overridden. Use that directly in the future. deprecated("3.7", "MorphPlayer() should be used on a PlayerPawn object") bool MorphPlayer(PlayerInfo activator, class spawnType, int duration, EMorphFlags style, class enterFlash = "TeleportFog", class exitFlash = "TeleportFog") { From 6f1422aec54f4da131d7486a06fa66c0730e3bba Mon Sep 17 00:00:00 2001 From: Kevin Caccamo Date: Sun, 15 Jun 2025 05:11:05 -0400 Subject: [PATCH 204/384] Attempt to fix KEYCONF reader memory issues Fix Windows-style line-ending assumptions Make inQuote a bool, since that's how it's used Make pointer usage smarter Add more eof checks, since ASan builds will crash without them Use a better name than 'i' Properly truncate ini names of key sections --- src/gamedata/keysections.cpp | 48 ++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/gamedata/keysections.cpp b/src/gamedata/keysections.cpp index 25b71671b..6f3847927 100644 --- a/src/gamedata/keysections.cpp +++ b/src/gamedata/keysections.cpp @@ -99,8 +99,12 @@ CCMD (addkeysection) return; } + FString name(argv[2]); // Limit the ini name to 32 chars - FString name(argv[2], 32); + if (name.Len() > 32) { + DPrintf(DMSG_ERROR, "WARNING: %s is too long as an ini name! The ini name should be 32 bytes or less.\n", &name[0]); + name.Truncate(32); + } for (unsigned i = 0; i < KeySections.Size(); i++) { @@ -170,39 +174,41 @@ void D_LoadWadSettings () while (conf < eof) { - size_t i = 0; + size_t linepos = 0; // Fetch a line to execute command.Clear(); - for (i = 0; conf + i < eof && conf[i] != '\n'; ++i) + for (linepos = 0; (conf + linepos) < eof && conf[linepos] != '\n' && conf[linepos] != '\r'; ++linepos) { - command.Push(conf[i]); + command.Push(conf[linepos]); } - if (i == 0) // Blank line + if (linepos == 0 && conf >= eof) // End of file { - conf++; - continue; + break; } - command.Push(0); - conf += i; - if (conf >= eof || *conf == '\n') + // Increment 'conf' pointer to next line + conf += linepos; + while (conf < eof && (*conf == '\n' || *conf == '\r')) { conf++; } + // Does 'command' have a comment? If so, remove it. // Comments begin with // - char *stop = &command[i - 1]; + char *stop = &command[linepos]; char *comment = &command[0]; - int inQuote = 0; + bool inQuote = false; - if (*stop == '\r') + if (*stop == '\r' || *stop == '\n') { *stop-- = 0; + } while (comment < stop) { + // if (*comment != '\\' && *(comment + 1) == '\"') if (*comment == '\"') { - inQuote ^= 1; + inQuote = !inQuote; } else if (!inQuote && *comment == '/' && *(comment + 1) == '/') { @@ -210,15 +216,15 @@ void D_LoadWadSettings () } comment++; } - if (comment == &command[0]) - { // Comment at line beginning - continue; - } - else if (comment < stop) - { // Comment in middle of line + // 'comment' will either be the end of the string, or the starting + // position of an inline comment. + if ((comment - &command[0]) < linepos) { *comment = 0; + } else { + // Just in case 'comment' is at EOF + command.Push(0); } - + // DPrintf(DMSG_ERROR, "command: %s\n", &command[0]); AddCommandString (&command[0]); } } From f9536ec918af4701efbfb5631141f6752833b299 Mon Sep 17 00:00:00 2001 From: Kevin Caccamo Date: Sun, 15 Jun 2025 07:13:01 -0400 Subject: [PATCH 205/384] Fix some things I overlooked Remove some useless lines of code which may cause a read error Fix the casing of linepos variable --- src/gamedata/keysections.cpp | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/gamedata/keysections.cpp b/src/gamedata/keysections.cpp index 6f3847927..bdb103a74 100644 --- a/src/gamedata/keysections.cpp +++ b/src/gamedata/keysections.cpp @@ -174,20 +174,20 @@ void D_LoadWadSettings () while (conf < eof) { - size_t linepos = 0; + size_t linePos = 0; // Fetch a line to execute command.Clear(); - for (linepos = 0; (conf + linepos) < eof && conf[linepos] != '\n' && conf[linepos] != '\r'; ++linepos) + for (linePos = 0; (conf + linePos) < eof && conf[linePos] != '\n' && conf[linePos] != '\r'; ++linePos) { - command.Push(conf[linepos]); + command.Push(conf[linePos]); } - if (linepos == 0 && conf >= eof) // End of file + if (linePos == 0 && conf >= eof) // End of file { break; } // Increment 'conf' pointer to next line - conf += linepos; + conf += linePos; while (conf < eof && (*conf == '\n' || *conf == '\r')) { conf++; @@ -195,14 +195,10 @@ void D_LoadWadSettings () // Does 'command' have a comment? If so, remove it. // Comments begin with // - char *stop = &command[linepos]; + char *stop = &command[linePos]; char *comment = &command[0]; bool inQuote = false; - if (*stop == '\r' || *stop == '\n') { - *stop-- = 0; - } - while (comment < stop) { // if (*comment != '\\' && *(comment + 1) == '\"') @@ -218,7 +214,7 @@ void D_LoadWadSettings () } // 'comment' will either be the end of the string, or the starting // position of an inline comment. - if ((comment - &command[0]) < linepos) { + if ((comment - &command[0]) < linePos) { *comment = 0; } else { // Just in case 'comment' is at EOF From 9a2fd53f1da0cd36176a53362b6257c3129c3685 Mon Sep 17 00:00:00 2001 From: Kevin Caccamo Date: Sun, 15 Jun 2025 17:23:48 -0400 Subject: [PATCH 206/384] Remove dereferences of comment + 1 If a line ends with a single slash, then you'll get an invalid read --- src/gamedata/keysections.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gamedata/keysections.cpp b/src/gamedata/keysections.cpp index bdb103a74..ebb1efd28 100644 --- a/src/gamedata/keysections.cpp +++ b/src/gamedata/keysections.cpp @@ -197,19 +197,22 @@ void D_LoadWadSettings () // Comments begin with // char *stop = &command[linePos]; char *comment = &command[0]; + char prevChar = 0; bool inQuote = false; while (comment < stop) { - // if (*comment != '\\' && *(comment + 1) == '\"') + // if (prevChar != '\\' && *comment == '\"') if (*comment == '\"') { inQuote = !inQuote; } - else if (!inQuote && *comment == '/' && *(comment + 1) == '/') + else if (!inQuote && prevChar == '/' && *comment == '/') { + comment--; // 'comment' is on the second slash break; } + prevChar = *comment; comment++; } // 'comment' will either be the end of the string, or the starting From 0835fe0eab70495e3d044c8bd6ecce1c5c76ddbb Mon Sep 17 00:00:00 2001 From: Kevin Caccamo Date: Sun, 15 Jun 2025 21:29:24 -0400 Subject: [PATCH 207/384] Fix code style Make code style consistent with the rest of the code --- src/gamedata/keysections.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/gamedata/keysections.cpp b/src/gamedata/keysections.cpp index ebb1efd28..ca37e5017 100644 --- a/src/gamedata/keysections.cpp +++ b/src/gamedata/keysections.cpp @@ -101,7 +101,8 @@ CCMD (addkeysection) FString name(argv[2]); // Limit the ini name to 32 chars - if (name.Len() > 32) { + if (name.Len() > 32) + { DPrintf(DMSG_ERROR, "WARNING: %s is too long as an ini name! The ini name should be 32 bytes or less.\n", &name[0]); name.Truncate(32); } @@ -217,9 +218,12 @@ void D_LoadWadSettings () } // 'comment' will either be the end of the string, or the starting // position of an inline comment. - if ((comment - &command[0]) < linePos) { + if ((comment - &command[0]) < linePos) + { *comment = 0; - } else { + } + else + { // Just in case 'comment' is at EOF command.Push(0); } From ba050c112e41afb2fc7bfa603257940acb14790e Mon Sep 17 00:00:00 2001 From: Kevin Caccamo Date: Mon, 16 Jun 2025 03:05:28 -0400 Subject: [PATCH 208/384] Remove useless if statement If conf is at eof, linePos and command.Size() will be 0, and the other statements will not run due to eof checks and pointer checks --- src/gamedata/keysections.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/gamedata/keysections.cpp b/src/gamedata/keysections.cpp index ca37e5017..763d5331f 100644 --- a/src/gamedata/keysections.cpp +++ b/src/gamedata/keysections.cpp @@ -183,10 +183,6 @@ void D_LoadWadSettings () { command.Push(conf[linePos]); } - if (linePos == 0 && conf >= eof) // End of file - { - break; - } // Increment 'conf' pointer to next line conf += linePos; while (conf < eof && (*conf == '\n' || *conf == '\r')) From e0baf7a85cd90db4a99f904771764a9a680072c7 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 16 Jun 2025 10:26:36 -0400 Subject: [PATCH 209/384] Added OnRevive virtual Called when a monster is resurrected (allows resetting properties without needing an event handler). --- src/playsim/p_mobj.cpp | 4 ++++ wadsrc/static/zscript/actors/actor.zs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 115c1495f..072afdd3c 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -8485,6 +8485,10 @@ void AActor::Revive() Level->total_monsters++; } + IFOVERRIDENVIRTUALPTRNAME(this, NAME_Actor, OnRevive) + { + VMCallVoid(func, this); + } // [ZZ] resurrect hook Level->localEventManager->WorldThingRevived(this); } diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index dc0892a3a..0bc9ee76f 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -595,6 +595,9 @@ class Actor : Thinker native return true; } + // Called after an Actor has been resurrected. + virtual void OnRevive() {} + // Called when an actor is to be reflected by a disc of repulsion. // Returns true to continue normal blast processing. virtual bool SpecialBlastHandling (Actor source, double strength) From 8071fd13685553f08910058668d0581d4d3ed702 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 17 Jun 2025 17:28:24 -0400 Subject: [PATCH 210/384] Scriptified ReactToDamage Allows pain handling to be overridden without needing to override the entirety of DamageMobj. --- src/playsim/p_interaction.cpp | 47 ++++++++++++++++++++++----- wadsrc/static/zscript/actors/actor.zs | 1 + 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/playsim/p_interaction.cpp b/src/playsim/p_interaction.cpp index 5202041f3..6cd3aa6c8 100644 --- a/src/playsim/p_interaction.cpp +++ b/src/playsim/p_interaction.cpp @@ -859,8 +859,9 @@ static inline bool isFakePain(AActor *target, AActor *inflictor, int damage) } // [MC] Completely ripped out of DamageMobj to make it less messy. -static void ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int damage, FName mod, int flags, int originaldamage) +static int ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int damage, int mod, int flags, int originaldamage) { + FName modName = ENamedName(mod); bool justhit = false; int painchance = 0; FState *woundstate = nullptr; @@ -871,17 +872,17 @@ static void ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int // Dead or non-existent entity, do not react. Especially if the damage is cancelled. if (target == nullptr || target->health < 1 || damage < 0) - return; + return false; player_t *player = target->player; if (player && player->mo) { if ((player->cheats & CF_GODMODE2) || (player->mo->flags5 & MF5_NOPAIN) || ((player->cheats & CF_GODMODE) && damage < TELEFRAG_DAMAGE)) - return; + return false; } - woundstate = target->FindState(NAME_Wound, mod); + woundstate = target->FindState(NAME_Wound, modName); if (woundstate != nullptr) { int woundhealth = target->WoundHealth; @@ -889,7 +890,7 @@ static void ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int if (target->health <= woundhealth) { target->SetState(woundstate); - return; + return true; } } // [MC] NOPAIN will not stop the actor from waking up if damaged. @@ -904,10 +905,10 @@ static void ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int && (forcedPain || damage >= target->PainThreshold)) { if (inflictor && inflictor->PainType != NAME_None) - mod = inflictor->PainType; + modName = inflictor->PainType; // Not called from ZScript. - justhit = TriggerPainChance(target, mod, forcedPain, false); + justhit = TriggerPainChance(target, modName, forcedPain, false); } if (wakeup && target->player == nullptr) target->reactiontime = 0; // we're awake now... @@ -946,6 +947,36 @@ static void ReactToDamage(AActor *target, AActor *inflictor, AActor *source, int // killough 11/98: Don't attack a friend, unless hit by that friend. if (justhit && (target->target == source || !target->target || !target->IsFriend(target->target))) target->flags |= MF_JUSTHIT; // fight back! + + return justhit; +} + +static int CallReactToDamage(AActor* target, AActor* inflictor, AActor* source, int damage, FName mod, int flags, int originaldamage) +{ + int res = false; + IFVIRTUALPTR(target, AActor, ReactToDamage) + { + res = VMCallSingle(func, target, inflictor, source, damage, mod.GetIndex(), flags, originaldamage); + } + else + { + res = ReactToDamage(target, inflictor, source, damage, mod.GetIndex(), flags, originaldamage); + } + + return res; +} + +DEFINE_ACTION_FUNCTION_NATIVE(AActor, ReactToDamage, ReactToDamage) +{ + PARAM_SELF_PROLOGUE(AActor); + PARAM_OBJECT(inflictor, AActor); + PARAM_OBJECT(source, AActor); + PARAM_INT(damage); + PARAM_NAME(mod); + PARAM_INT(flags); + PARAM_INT(originaldamage); + + ACTION_RETURN_BOOL(ReactToDamage(self, inflictor, source, damage, mod.GetIndex(), flags, originaldamage)); } static bool TriggerPainChance(AActor *target, FName mod = NAME_None, bool forcedPain = false, bool zscript = false) @@ -1537,7 +1568,7 @@ static int DoDamageMobj(AActor *target, AActor *inflictor, AActor *source, int d bool needevent = true; int realdamage = DamageMobj(target, inflictor, source, damage, mod, flags, angle, needevent); if (realdamage >= 0) //Keep this check separated. Mods relying upon negative numbers may break otherwise. - ReactToDamage(target, inflictor, source, realdamage, mod, flags, damage); + CallReactToDamage(target, inflictor, source, realdamage, mod, flags, damage); if (realdamage > 0 && needevent) { diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 0bc9ee76f..15bd68236 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -826,6 +826,7 @@ class Actor : Thinker native native bool CheckMeleeRange(double range = -1); native bool TriggerPainChance(Name mod, bool forcedPain = false); native virtual int DamageMobj(Actor inflictor, Actor source, int damage, Name mod, int flags = 0, double angle = 0); + native virtual bool ReactToDamage(Actor inflictor, Actor source, int damage, Name mod, int flags, int originaldamage); native void PoisonMobj (Actor inflictor, Actor source, int damage, int duration, int period, Name type); native double AimLineAttack(double angle, double distance, out FTranslatedLineTarget pLineTarget = null, double vrange = 0., int flags = 0, Actor target = null, Actor friender = null); native Actor, int LineAttack(double angle, double distance, double pitch, int damage, Name damageType, class pufftype, int flags = 0, out FTranslatedLineTarget victim = null, double offsetz = 0., double offsetforward = 0., double offsetside = 0.); From 7dfb5ff70a4b2a65ceccf605e894fa8702a1f4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 18 Jun 2025 02:10:36 -0300 Subject: [PATCH 211/384] fix unsigned comparison in keysections.cpp --- src/gamedata/keysections.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gamedata/keysections.cpp b/src/gamedata/keysections.cpp index 763d5331f..e96a3042b 100644 --- a/src/gamedata/keysections.cpp +++ b/src/gamedata/keysections.cpp @@ -175,7 +175,7 @@ void D_LoadWadSettings () while (conf < eof) { - size_t linePos = 0; + ptrdiff_t linePos = 0; // Fetch a line to execute command.Clear(); From f4eebd1ceddb6a3fc7e2de80140fb5c7430bf9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Wed, 18 Jun 2025 03:43:30 -0300 Subject: [PATCH 212/384] CallVM API, plus multi-return and vector support --- src/common/scripting/vm/vm.h | 238 ++++++++++++++++++++++++++-- src/common/scripting/vm/vmframe.cpp | 48 ++++++ src/g_game.cpp | 16 +- src/playsim/p_pspr.cpp | 37 ++--- 4 files changed, 294 insertions(+), 45 deletions(-) diff --git a/src/common/scripting/vm/vm.h b/src/common/scripting/vm/vm.h index d4a6b1fa9..1828799af 100644 --- a/src/common/scripting/vm/vm.h +++ b/src/common/scripting/vm/vm.h @@ -534,13 +534,37 @@ inline int VMCallAction(VMFunction *func, VMValue *params, int numparams, VMRetu return VMCall(func, params, numparams, results, numresults); } +template struct VMArgTypeTrait { typedef T type; static const int ArgCount = 1; }; +template<> struct VMArgTypeTrait { typedef double type; static const int ArgCount = 2; }; +template<> struct VMArgTypeTrait { typedef double type; static const int ArgCount = 3; }; +template<> struct VMArgTypeTrait { typedef double type; static const int ArgCount = 4; }; +template<> struct VMArgTypeTrait { typedef double type; static const int ArgCount = 4; }; + template struct VMReturnTypeTrait { typedef T type; static const int ReturnCount = 1; }; template<> struct VMReturnTypeTrait { typedef void type; static const int ReturnCount = 0; }; +template +struct FirstTemplateValue +{ + using type = T; +}; + + + void VMCheckParamCount(VMFunction* func, int retcount, int argcount); -template -void VMCheckParamCount(VMFunction* func, int argcount) { return VMCheckParamCount(func, VMReturnTypeTrait::ReturnCount, argcount); } +template +void VMCheckParamCount(VMFunction* func, int argcount) +{ + if constexpr (sizeof...(Rets) == 1) + { + return VMCheckParamCount(func, VMReturnTypeTrait::type>::ReturnCount, argcount); + } + else + { + return VMCheckParamCount(func, sizeof...(Rets), argcount); + } +} // The type can't be mapped to ZScript automatically: @@ -551,12 +575,20 @@ template void VMCheckReturn(VMFunction* func, int index) = template<> void VMCheckParam(VMFunction* func, int index); template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); +template<> void VMCheckParam(VMFunction* func, int index); template<> void VMCheckParam(VMFunction* func, int index); template<> void VMCheckParam(VMFunction* func, int index); template<> void VMCheckReturn(VMFunction* func, int index); template<> void VMCheckReturn(VMFunction* func, int index); template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); +template<> void VMCheckReturn(VMFunction* func, int index); template<> void VMCheckReturn(VMFunction* func, int index); template<> void VMCheckReturn(VMFunction* func, int index); template<> void VMCheckReturn(VMFunction* func, int index); @@ -595,26 +627,206 @@ void VMValidateSignatureSingle(VMFunction* func, std::index_sequence) (VMCheckParam>(func, I), ...); } +template +void VMValidateSignatureMulti(VMFunction* func, std::index_sequence, std::index_sequence, Args... args) +{ + VMCheckParamCount(func, sizeof...(Args)); + (VMCheckReturn>(func, IRets), ...); + (VMCheckParam>(func, IArgs), ...); +} + void VMCallCheckResult(VMFunction* func, VMValue* params, int numparams, VMReturn* results, int numresults); -template -typename VMReturnTypeTrait::type VMCallSingle(VMFunction* func, Args... args) -{ - VMValidateSignatureSingle(func, std::make_index_sequence{}); - VMValue params[] = { args... }; - vm_pointer_decay_void resultval; // convert any pointer to void - VMReturn results(&resultval); - VMCallCheckResult(func, params, sizeof...(Args), &results, 1); - return (RetVal)resultval; +struct VMValueMulti +{ + VMValue vals[4]; + int count; + + template + VMValueMulti(T val) + { + vals[0] = val; + count = 1; + } + + VMValueMulti(DVector2 val) + { + vals[0] = val.X; + vals[1] = val.Y; + count = 2; + } + + VMValueMulti(DVector3 val) + { + vals[0] = val.X; + vals[1] = val.Y; + vals[2] = val.Z; + count = 3; + } + + VMValueMulti(DVector4 val) + { + vals[0] = val.X; + vals[1] = val.Y; + vals[2] = val.Z; + vals[3] = val.W; + count = 4; + } + + VMValueMulti(DQuaternion val) + { + vals[0] = val.X; + vals[1] = val.Y; + vals[2] = val.Z; + vals[3] = val.W; + count = 4; + } +}; + +template +constexpr int numArgs() +{ + return (VMArgTypeTrait::ArgCount + ...); } +template +constexpr bool hasVector() +{ + return (VMArgTypeTrait::ArgCount + ...) != sizeof...(Args); +} + + template typename VMReturnTypeTrait::type VMCallVoid(VMFunction* func, Args... args) { VMValidateSignatureSingle(func, std::make_index_sequence{}); - VMValue params[] = { args... }; - VMCallCheckResult(func, params, sizeof...(Args), nullptr, 0); + if constexpr(hasVector()) + { + VMValueMulti arglist[] = { args... }; + + constexpr int argCount = numArgs(); + + VMValue params[argCount]; + + for(int i = 0, j = 0; i < sizeof...(Args); i++) + { + for(int k = 0; k < arglist[i].count; k++, j++) + { + params[j] = arglist[i].vals[k]; + } + } + + VMCallCheckResult(func, params, argCount, nullptr, 0); + } + else + { + VMValue params[] = { args... }; + VMCallCheckResult(func, params, sizeof...(Args), nullptr, 0); + } +} + +template +typename VMReturnTypeTrait::type VMCallSingle(VMFunction* func, Args... args) +{ + VMValidateSignatureSingle(func, std::make_index_sequence{}); + if constexpr(hasVector()) + { + VMValueMulti arglist[] = { args... }; + + constexpr int argCount = numArgs(); + + VMValue params[argCount]; + + for(int i = 0, j = 0; i < sizeof...(Args); i++) + { + for(int k = 0; k < arglist[i].count; k++, j++) + { + params[j] = arglist[i].vals[k]; + } + } + + vm_pointer_decay_void resultval; // convert any pointer to void + VMReturn results(&resultval); + VMCallCheckResult(func, params, argCount, &results, 1); + return (RetVal)resultval; + } + else + { + VMValue params[] = { args... }; + + vm_pointer_decay_void resultval; // convert any pointer to void + VMReturn results(&resultval); + VMCallCheckResult(func, params, sizeof...(Args), &results, 1); + return (RetVal)resultval; + } +} + +template +std::tuple::type...> VMCallMultiImpl(VMFunction* func, std::index_sequence retsSeq, Args... args) +{ + VMValidateSignatureMulti(func, retsSeq, std::make_index_sequence{}, std::forward(args)...); + if constexpr(hasVector()) + { + VMValueMulti arglist[] = { args... }; + + constexpr int argCount = numArgs(); + + VMValue params[argCount]; + + for(int i = 0, j = 0; i < sizeof...(Args); i++) + { + for(int k = 0; k < arglist[i].count; k++, j++) + { + params[j] = arglist[i].vals[k]; + } + } + + std::tuple>::type...> resultval; // convert any pointer to void + VMReturn results[sizeof...(Rets)] { &std::get(resultval)... }; + VMCallCheckResult(func, params, argCount, results, sizeof...(Rets)); + return (std::tuple::type...>)resultval; + } + else + { + VMValue params[] = { args... }; + + std::tuple>::type...> resultval; // convert any pointer to void + VMReturn results[sizeof...(Rets)] { &std::get(resultval)... }; + VMCallCheckResult(func, params, sizeof...(Args), results, sizeof...(Rets)); + return (std::tuple::type...>)resultval; + } +} + +template +std::tuple::type...> VMCallMulti(VMFunction* func, Args... args) +{ + return VMCallMultiImpl(func, std::make_index_sequence{}, std::forward(args)...); +} + +template +using MultiVMReturn = std::conditional< + (sizeof...(Rets) > 1), + std::tuple::type...>, + typename VMReturnTypeTrait::type>::type + >; + +template +typename MultiVMReturn::type CallVM(VMFunction* func, Args... args) +{ + static_assert(sizeof...(Rets) > 0, "missing return type in VMCall"); + if constexpr(sizeof...(Rets) == 1 && std::is_same_v::type, void>) + { + return VMCallVoid(func, std::forward(args)...); + } + else if constexpr(sizeof...(Rets) == 1) + { + return VMCallSingle(func, std::forward(args)...); + } + else + { + return VMCallMulti(func, std::forward(args)...); + } } diff --git a/src/common/scripting/vm/vmframe.cpp b/src/common/scripting/vm/vmframe.cpp index 40b3dbef8..c72fc2a7e 100644 --- a/src/common/scripting/vm/vmframe.cpp +++ b/src/common/scripting/vm/vmframe.cpp @@ -575,6 +575,30 @@ template<> void VMCheckParam(VMFunction* func, int index) I_FatalError("%s argument %d is not a double", func->PrintableName, index); } +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index] != TypeVector2) + I_FatalError("%s argument %d is not a vector2", func->PrintableName, index); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index] != TypeVector3) + I_FatalError("%s argument %d is not a vector3", func->PrintableName, index); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index] != TypeVector4) + I_FatalError("%s argument %d is not a vector4", func->PrintableName, index); +} + +template<> void VMCheckParam(VMFunction* func, int index) +{ + if (func->Proto->ArgumentTypes[index] != TypeQuaternion) + I_FatalError("%s argument %d is not a quat", func->PrintableName, index); +} + template<> void VMCheckParam(VMFunction* func, int index) { if (func->Proto->ArgumentTypes[index] != TypeString) @@ -603,6 +627,30 @@ template<> void VMCheckReturn(VMFunction* func, int index) I_FatalError("%s return value %d is not a double", func->PrintableName, index); } +template<> void VMCheckReturn(VMFunction* func, int index) +{ + if (func->Proto->ReturnTypes[index] != TypeVector2) + I_FatalError("%s return value %d is not a vector2", func->PrintableName, index); +} + +template<> void VMCheckReturn(VMFunction* func, int index) +{ + if (func->Proto->ReturnTypes[index] != TypeVector3) + I_FatalError("%s return value %d is not a vector3", func->PrintableName, index); +} + +template<> void VMCheckReturn(VMFunction* func, int index) +{ + if (func->Proto->ReturnTypes[index] != TypeVector4) + I_FatalError("%s return value %d is not a vector4", func->PrintableName, index); +} + +template<> void VMCheckReturn(VMFunction* func, int index) +{ + if (func->Proto->ReturnTypes[index] != TypeQuaternion) + I_FatalError("%s return value %d is not a quat", func->PrintableName, index); +} + template<> void VMCheckReturn(VMFunction* func, int index) { if (func->Proto->ReturnTypes[index] != TypeString) diff --git a/src/g_game.cpp b/src/g_game.cpp index 8e86d86db..703cac8bb 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -317,7 +317,7 @@ CCMD (slot) // Needs to be redone IFVIRTUALPTRNAME(mo, NAME_PlayerPawn, PickWeapon) { - SendItemUse = VMCallSingle(func, mo, slot, (int)!(dmflags2 & DF2_DONTCHECKAMMO)); + SendItemUse = CallVM(func, mo, slot, (int)!(dmflags2 & DF2_DONTCHECKAMMO)); } } @@ -373,7 +373,7 @@ CCMD (weapnext) // Needs to be redone IFVIRTUALPTRNAME(mo, NAME_PlayerPawn, PickNextWeapon) { - SendItemUse = VMCallSingle(func, mo); + SendItemUse = CallVM(func, mo); } } @@ -398,7 +398,7 @@ CCMD (weapprev) // Needs to be redone IFVIRTUALPTRNAME(mo, NAME_PlayerPawn, PickPrevWeapon) { - SendItemUse = VMCallSingle(func, mo); + SendItemUse = CallVM(func, mo); } } @@ -437,7 +437,7 @@ CCMD (invnext) { IFVM(PlayerPawn, InvNext) { - VMCallVoid(func, players[consoleplayer].mo); + CallVM(func, players[consoleplayer].mo); } } } @@ -448,7 +448,7 @@ CCMD(invprev) { IFVM(PlayerPawn, InvPrev) { - VMCallVoid(func, players[consoleplayer].mo); + CallVM(func, players[consoleplayer].mo); } } } @@ -523,7 +523,7 @@ CCMD (useflechette) if (players[consoleplayer].mo == nullptr) return; IFVIRTUALPTRNAME(players[consoleplayer].mo, NAME_PlayerPawn, GetFlechetteItem) { - AActor * cls = VMCallSingle(func, players[consoleplayer].mo); + AActor * cls = CallVM(func, players[consoleplayer].mo); if (cls != nullptr) SendItemUse = cls; } @@ -1289,7 +1289,7 @@ void G_PlayerFinishLevel (int player, EFinishLevelType mode, int flags) { IFVM(PlayerPawn, PlayerFinishLevel) { - VMCallVoid(func, players[player].mo, (int)mode, flags); + CallVM(func, players[player].mo, (int)mode, flags); } } @@ -1362,7 +1362,7 @@ void FLevelLocals::PlayerReborn (int player) IFVIRTUALPTRNAME(actor, NAME_PlayerPawn, GiveDefaultInventory) { - VMCallVoid(func, actor); + CallVM(func, actor); } p->ReadyWeapon = p->PendingWeapon; } diff --git a/src/playsim/p_pspr.cpp b/src/playsim/p_pspr.cpp index df09b58c3..54ae4b6cf 100644 --- a/src/playsim/p_pspr.cpp +++ b/src/playsim/p_pspr.cpp @@ -637,19 +637,16 @@ void P_BobWeapon (player_t *player, float *x, float *y, double ticfrac) { IFVIRTUALPTRNAME(player->mo, NAME_PlayerPawn, BobWeapon) { - VMValue param[] = { player->mo, ticfrac }; - DVector2 result; - VMReturn ret(&result); - VMCall(func, param, 2, &ret, 1); + DVector2 result = CallVM(func, player->mo, ticfrac); - auto inv = player->mo->Inventory; + auto inv = player->mo->Inventory.Get(); while(inv != nullptr && !(inv->ObjectFlags & OF_EuthanizeMe)) // same loop as ModifyDamage, except it actually checks if it's overriden before calling { - auto nextinv = inv->Inventory; + auto nextinv = inv->Inventory.Get(); IFOVERRIDENVIRTUALPTRNAME(inv, NAME_Inventory, ModifyBob) { - VMValue param[] = { (DObject*)inv, result.X, result.Y, ticfrac }; - VMCall(func, param, 4, &ret, 1); + DVector2 r2 = CallVM(func, inv, result, ticfrac); + result = r2; } inv = nextinv; } @@ -665,31 +662,23 @@ void P_BobWeapon3D (player_t *player, FVector3 *translation, FVector3 *rotation, { IFVIRTUALPTRNAME(player->mo, NAME_PlayerPawn, BobWeapon3D) { - VMValue param[] = { player->mo, ticfrac }; - DVector3 t, r; - VMReturn returns[2]; - returns[0].Vec3At(&t); - returns[1].Vec3At(&r); - VMCall(func, param, 2, returns, 2); + auto [t, r] = CallVM(func, player->mo, ticfrac); - auto inv = player->mo->Inventory; + auto inv = player->mo->Inventory.Get(); while(inv != nullptr && !(inv->ObjectFlags & OF_EuthanizeMe)) { - auto nextinv = inv->Inventory; + auto nextinv = inv->Inventory.Get(); IFOVERRIDENVIRTUALPTRNAME(inv, NAME_Inventory, ModifyBob3D) { - VMValue param[] = { (DObject*)inv, t.X, t.Y, t.Z, r.X, r.Y, r.Z, ticfrac }; - VMCall(func, param, 8, returns, 2); + auto [t2, r2] = CallVM(func, inv, t, r, ticfrac); + t = t2; + r = r2; } inv = nextinv; } - translation->X = (float)t.X; - translation->Y = (float)t.Y; - translation->Z = (float)t.Z; - rotation->X = (float)r.X; - rotation->Y = (float)r.Y; - rotation->Z = (float)r.Z; + *translation = FVector3(t); + *rotation = FVector3(r); return; } *translation = *rotation = {}; From 885c1d2920d174f3b277f87ce755e43b1f8657fd Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 14 Jun 2025 08:58:13 -0400 Subject: [PATCH 213/384] Update to latest ZWidget version This adds new features (such as themes) alongside fixing numerous bugs. This should be kept up-to-date with upstream more often and changes to it should be PR'd back to its main repo. --- libraries/ZWidget/CMakeLists.txt | 153 ++- .../ZWidget/include/zwidget/core/canvas.h | 127 +- libraries/ZWidget/include/zwidget/core/rect.h | 2 + .../ZWidget/include/zwidget/core/theme.h | 81 ++ .../ZWidget/include/zwidget/core/widget.h | 109 +- .../zwidget/systemdialogs/open_file_dialog.h | 55 + .../systemdialogs/open_folder_dialog.h | 30 + .../zwidget/systemdialogs/save_file_dialog.h | 46 + .../widgets/checkboxlabel/checkboxlabel.h | 7 +- .../zwidget/widgets/imagebox/imagebox.h | 10 + .../zwidget/widgets/lineedit/lineedit.h | 13 +- .../zwidget/widgets/listview/listview.h | 9 +- .../zwidget/widgets/mainwindow/mainwindow.h | 8 +- .../include/zwidget/widgets/menubar/menubar.h | 107 ++ .../zwidget/widgets/pushbutton/pushbutton.h | 11 +- .../zwidget/widgets/scrollbar/scrollbar.h | 4 +- .../zwidget/widgets/tabwidget/tabwidget.h | 10 +- .../zwidget/widgets/textedit/textedit.h | 11 +- .../zwidget/widgets/textlabel/textlabel.h | 2 +- .../include/zwidget/widgets/toolbar/toolbar.h | 20 + .../zwidget/widgets/toolbar/toolbarbutton.h | 22 +- .../include/zwidget/window/sdl2nativehandle.h | 12 + .../zwidget/window/waylandnativehandle.h | 10 + .../zwidget/window/win32nativehandle.h | 9 + .../ZWidget/include/zwidget/window/window.h | 238 ++-- .../include/zwidget/window/x11nativehandle.h | 13 + libraries/ZWidget/src/core/canvas.cpp | 860 ++++++++---- .../ZWidget/src/core/nanosvg/nanosvgrast.h | 2 +- libraries/ZWidget/src/core/pathfill.cpp | 4 +- libraries/ZWidget/src/core/theme.cpp | 383 ++++++ libraries/ZWidget/src/core/timer.cpp | 2 + libraries/ZWidget/src/core/truetypefont.cpp | 34 +- libraries/ZWidget/src/core/truetypefont.h | 4 +- libraries/ZWidget/src/core/widget.cpp | 302 ++++- .../src/systemdialogs/open_file_dialog.cpp | 12 + .../src/systemdialogs/open_folder_dialog.cpp | 12 + .../src/systemdialogs/save_file_dialog.cpp | 12 + .../widgets/checkboxlabel/checkboxlabel.cpp | 28 +- .../ZWidget/src/widgets/imagebox/imagebox.cpp | 44 +- .../ZWidget/src/widgets/lineedit/lineedit.cpp | 87 +- .../ZWidget/src/widgets/listview/listview.cpp | 45 +- .../src/widgets/mainwindow/mainwindow.cpp | 12 +- .../ZWidget/src/widgets/menubar/menubar.cpp | 378 +++++- .../src/widgets/pushbutton/pushbutton.cpp | 55 +- .../src/widgets/scrollbar/scrollbar.cpp | 13 +- .../src/widgets/statusbar/statusbar.cpp | 4 +- .../src/widgets/tabwidget/tabwidget.cpp | 55 +- .../ZWidget/src/widgets/textedit/textedit.cpp | 98 +- .../src/widgets/textlabel/textlabel.cpp | 2 +- .../ZWidget/src/widgets/toolbar/toolbar.cpp | 53 + .../src/widgets/toolbar/toolbarbutton.cpp | 102 +- .../src/window/dbus/dbus_open_file_dialog.cpp | 288 ++++ .../src/window/dbus/dbus_open_file_dialog.h | 38 + .../window/dbus/dbus_open_folder_dialog.cpp | 26 + .../src/window/dbus/dbus_open_folder_dialog.h | 21 + .../src/window/dbus/dbus_save_file_dialog.cpp | 54 + .../src/window/dbus/dbus_save_file_dialog.h | 37 + .../src/window/sdl2/sdl2_display_backend.cpp | 38 + .../src/window/sdl2/sdl2_display_backend.h | 19 + .../src/window/sdl2/sdl2_display_window.cpp | 772 +++++++++++ ...2displaywindow.h => sdl2_display_window.h} | 25 +- .../src/window/sdl2/sdl2displaywindow.cpp | 675 ---------- .../src/window/stub/stub_open_file_dialog.cpp | 64 + .../src/window/stub/stub_open_file_dialog.h | 41 + .../window/stub/stub_open_folder_dialog.cpp | 26 + .../src/window/stub/stub_open_folder_dialog.h | 23 + .../src/window/stub/stub_save_file_dialog.cpp | 54 + .../src/window/stub/stub_save_file_dialog.h | 39 + .../wayland/wayland_display_backend.cpp | 1150 ++++++++++++++++ .../window/wayland/wayland_display_backend.h | 168 +++ .../window/wayland/wayland_display_window.cpp | 472 +++++++ .../window/wayland/wayland_display_window.h | 198 +++ .../wl_fractional_scaling_protocol.cpp | 205 +++ .../wl_fractional_scaling_protocol.hpp | 126 ++ .../window/win32/win32_display_backend.cpp | 56 + .../src/window/win32/win32_display_backend.h | 23 + .../src/window/win32/win32_display_window.cpp | 788 +++++++++++ .../{win32window.h => win32_display_window.h} | 34 +- .../window/win32/win32_open_file_dialog.cpp | 227 ++++ .../src/window/win32/win32_open_file_dialog.h | 44 + .../window/win32/win32_open_folder_dialog.cpp | 111 ++ .../window/win32/win32_open_folder_dialog.h | 26 + .../window/win32/win32_save_file_dialog.cpp | 174 +++ .../src/window/win32/win32_save_file_dialog.h | 42 + .../ZWidget/src/window/win32/win32_util.h | 60 + .../ZWidget/src/window/win32/win32window.cpp | 702 ---------- libraries/ZWidget/src/window/window.cpp | 199 ++- .../src/window/x11/x11_display_backend.cpp | 70 + .../src/window/x11/x11_display_backend.h | 25 + .../src/window/x11/x11_display_window.cpp | 1184 +++++++++++++++++ .../src/window/x11/x11_display_window.h | 142 ++ .../ZWidget/src/window/ztimer/ztimer.cpp | 73 + libraries/ZWidget/src/window/ztimer/ztimer.h | 41 + src/common/widgets/errorwindow.cpp | 82 +- src/common/widgets/errorwindow.h | 11 +- src/common/widgets/widgetresourcedata.cpp | 13 +- src/d_main.cpp | 10 + src/launcher/launcherwindow.cpp | 5 +- src/launcher/playgamepage.cpp | 10 +- src/launcher/playgamepage.h | 1 + src/launcher/settingspage.h | 2 +- 101 files changed, 10194 insertions(+), 2242 deletions(-) create mode 100644 libraries/ZWidget/include/zwidget/core/theme.h create mode 100644 libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h create mode 100644 libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h create mode 100644 libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h create mode 100644 libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h create mode 100644 libraries/ZWidget/include/zwidget/window/waylandnativehandle.h create mode 100644 libraries/ZWidget/include/zwidget/window/win32nativehandle.h create mode 100644 libraries/ZWidget/include/zwidget/window/x11nativehandle.h create mode 100644 libraries/ZWidget/src/core/theme.cpp create mode 100644 libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp create mode 100644 libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp create mode 100644 libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h create mode 100644 libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp create mode 100644 libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h create mode 100644 libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h create mode 100644 libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp create mode 100644 libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h create mode 100644 libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp rename libraries/ZWidget/src/window/sdl2/{sdl2displaywindow.h => sdl2_display_window.h} (76%) delete mode 100644 libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp create mode 100644 libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/stub/stub_open_file_dialog.h create mode 100644 libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp create mode 100644 libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h create mode 100644 libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/stub/stub_save_file_dialog.h create mode 100644 libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp create mode 100644 libraries/ZWidget/src/window/wayland/wayland_display_backend.h create mode 100644 libraries/ZWidget/src/window/wayland/wayland_display_window.cpp create mode 100644 libraries/ZWidget/src/window/wayland/wayland_display_window.h create mode 100644 libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp create mode 100644 libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp create mode 100644 libraries/ZWidget/src/window/win32/win32_display_backend.cpp create mode 100644 libraries/ZWidget/src/window/win32/win32_display_backend.h create mode 100644 libraries/ZWidget/src/window/win32/win32_display_window.cpp rename libraries/ZWidget/src/window/win32/{win32window.h => win32_display_window.h} (70%) create mode 100644 libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/win32/win32_open_file_dialog.h create mode 100644 libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp create mode 100644 libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h create mode 100644 libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp create mode 100644 libraries/ZWidget/src/window/win32/win32_save_file_dialog.h create mode 100644 libraries/ZWidget/src/window/win32/win32_util.h delete mode 100644 libraries/ZWidget/src/window/win32/win32window.cpp create mode 100644 libraries/ZWidget/src/window/x11/x11_display_backend.cpp create mode 100644 libraries/ZWidget/src/window/x11/x11_display_backend.h create mode 100644 libraries/ZWidget/src/window/x11/x11_display_window.cpp create mode 100644 libraries/ZWidget/src/window/x11/x11_display_window.h create mode 100644 libraries/ZWidget/src/window/ztimer/ztimer.cpp create mode 100644 libraries/ZWidget/src/window/ztimer/ztimer.h diff --git a/libraries/ZWidget/CMakeLists.txt b/libraries/ZWidget/CMakeLists.txt index 329caf96e..d65e5ccea 100644 --- a/libraries/ZWidget/CMakeLists.txt +++ b/libraries/ZWidget/CMakeLists.txt @@ -1,6 +1,22 @@ cmake_minimum_required(VERSION 3.11) project(zwidget) +if (UNIX AND NOT APPLE) + include(FindPkgConfig) + pkg_check_modules(DBUS dbus-1) + if (!DBUS_FOUND) + pkg_check_modules(DBUS REQUIRED dbus) # Fedora Linux looks for dbus instead + endif() + pkg_check_modules(WAYLANDPP + wayland-client + wayland-client++ + wayland-client-extra++ + wayland-client-unstable++ + wayland-cursor++ + xkbcommon>=0.5.0 + ) +endif() + set(ZWIDGET_SOURCES src/core/canvas.cpp src/core/font.cpp @@ -8,6 +24,7 @@ set(ZWIDGET_SOURCES src/core/span_layout.cpp src/core/timer.cpp src/core/widget.cpp + src/core/theme.cpp src/core/utf8reader.cpp src/core/pathfill.cpp src/core/truetypefont.cpp @@ -32,6 +49,17 @@ set(ZWIDGET_SOURCES src/widgets/listview/listview.cpp src/widgets/tabwidget/tabwidget.cpp src/window/window.cpp + src/window/stub/stub_open_folder_dialog.cpp + src/window/stub/stub_open_folder_dialog.h + src/window/stub/stub_open_file_dialog.cpp + src/window/stub/stub_open_file_dialog.h + src/window/stub/stub_save_file_dialog.cpp + src/window/stub/stub_save_file_dialog.h + src/window/ztimer/ztimer.h + src/window/ztimer/ztimer.cpp + src/systemdialogs/open_folder_dialog.cpp + src/systemdialogs/open_file_dialog.cpp + src/systemdialogs/save_file_dialog.cpp ) set(ZWIDGET_INCLUDES @@ -44,6 +72,7 @@ set(ZWIDGET_INCLUDES include/zwidget/core/span_layout.h include/zwidget/core/timer.h include/zwidget/core/widget.h + include/zwidget/core/theme.h include/zwidget/core/utf8reader.h include/zwidget/core/resourcedata.h include/zwidget/widgets/lineedit/lineedit.h @@ -61,19 +90,62 @@ set(ZWIDGET_INCLUDES include/zwidget/widgets/listview/listview.h include/zwidget/widgets/tabwidget/tabwidget.h include/zwidget/window/window.h + include/zwidget/window/x11nativehandle.h + include/zwidget/window/waylandnativehandle.h + include/zwidget/window/win32nativehandle.h + include/zwidget/window/sdl2nativehandle.h + include/zwidget/systemdialogs/open_folder_dialog.h + include/zwidget/systemdialogs/open_file_dialog.h + include/zwidget/systemdialogs/save_file_dialog.h ) set(ZWIDGET_WIN32_SOURCES - src/window/win32/win32window.cpp - src/window/win32/win32window.h + src/window/win32/win32_display_backend.cpp + src/window/win32/win32_display_backend.h + src/window/win32/win32_display_window.cpp + src/window/win32/win32_display_window.h + src/window/win32/win32_open_folder_dialog.cpp + src/window/win32/win32_open_folder_dialog.h + src/window/win32/win32_open_file_dialog.cpp + src/window/win32/win32_open_file_dialog.h + src/window/win32/win32_save_file_dialog.cpp + src/window/win32/win32_save_file_dialog.h + src/window/win32/win32_util.h +) + +set(ZWIDGET_DBUS_SOURCES + src/window/dbus/dbus_open_folder_dialog.cpp + src/window/dbus/dbus_open_folder_dialog.h + src/window/dbus/dbus_open_file_dialog.cpp + src/window/dbus/dbus_open_file_dialog.h + src/window/dbus/dbus_save_file_dialog.cpp + src/window/dbus/dbus_save_file_dialog.h ) set(ZWIDGET_COCOA_SOURCES ) set(ZWIDGET_SDL2_SOURCES - src/window/sdl2/sdl2displaywindow.cpp - src/window/sdl2/sdl2displaywindow.h + src/window/sdl2/sdl2_display_backend.cpp + src/window/sdl2/sdl2_display_backend.h + src/window/sdl2/sdl2_display_window.cpp + src/window/sdl2/sdl2_display_window.h +) + +set(ZWIDGET_X11_SOURCES + src/window/x11/x11_display_backend.cpp + src/window/x11/x11_display_backend.h + src/window/x11/x11_display_window.cpp + src/window/x11/x11_display_window.h +) + +set(ZWIDGET_WAYLAND_SOURCES + src/window/wayland/wayland_display_backend.cpp + src/window/wayland/wayland_display_backend.h + src/window/wayland/wayland_display_window.cpp + src/window/wayland/wayland_display_window.h + src/window/wayland/wl_fractional_scaling_protocol.cpp + src/window/wayland/wl_fractional_scaling_protocol.hpp ) source_group("src" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/.+") @@ -95,6 +167,13 @@ source_group("src\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_S source_group("src\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/listview/.+") source_group("src\\widgets\\tabwidget" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/tabwidget/.+") source_group("src\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/.+") +source_group("src\\window\\stub" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/stub/.+") +source_group("src\\window\\win32" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/win32/.+") +source_group("src\\window\\sdl2" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/sdl2/.+") +source_group("src\\window\\x11" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/x11/.+") +source_group("src\\window\\wayland" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/wayland/.+") +source_group("src\\window\\dbus" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/dbus/.+") +source_group("src\\systemdialogs" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/systemdialogs/.+") source_group("include" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/.+") source_group("include\\core" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/core/.+") source_group("include\\widgets" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/.+") @@ -112,42 +191,62 @@ source_group("include\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRE source_group("include\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/listview/.+") source_group("include\\widgets\\tabwidget" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/widgets/tabwidget/.+") source_group("include\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/.+") -source_group("include\\window\\win32" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/win32/.+") -source_group("include\\window\\sdl2" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/window/sdl2/.+") +source_group("include\\systemdialogs" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/include/zwidget/systemdialogs/.+") -include_directories(include include/zwidget src) +# Include directory to external projects using zwidget +include_directories(include) + +# Internal include dirs for building zwidget +set(ZWIDGET_INCLUDE_DIRS include/zwidget src) + +set(ZWIDGET_COMPILE_OPTIONS) if(WIN32) set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_WIN32_SOURCES}) - add_definitions(-DUNICODE -D_UNICODE) - if(MINGW) - add_definitions(-DMINGW) + set(ZWIDGET_DEFINES -DUNICODE -D_UNICODE) + set(ZWIDGET_LINK_OPTIONS) + if(MSVC) + # Use all cores for compilation + set(ZWIDGET_COMPILE_OPTIONS ${ZWIDGET_COMPILE_OPTIONS} /MP) + + # Ignore specific warnings + #set(ZWIDGET_COMPILE_OPTIONS ${ZWIDGET_COMPILE_OPTIONS} /wd4244 /wd4267 /wd4005 /wd4018) + + # Don't slow down std containers in debug builds + set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES}) + + # Ignore warning about legacy CRT functions + #set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -D_CRT_SECURE_NO_WARNINGS) endif() elseif(APPLE) set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_COCOA_SOURCES}) set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) - add_definitions(-DUNIX -D_UNIX) - add_link_options(-pthread) + set(ZWIDGET_DEFINES -DUNIX -D_UNIX) + set(ZWIDGET_LINK_OPTIONS -pthread) else() - set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES}) - if(NOT HAIKU) - set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl) - else() - set(ZWIDGET_LIBS ${CMAKE_DL_LIBS}) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_SDL2_SOURCES} ${ZWIDGET_X11_SOURCES}) + set(ZWIDGET_LIBS ${CMAKE_DL_LIBS} -ldl -lX11) + set(ZWIDGET_DEFINES -DUNIX -D_UNIX -DUSE_SDL2 -DUSE_X11) + set(ZWIDGET_LINK_OPTIONS -pthread) + if (DBUS_FOUND) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_DBUS_SOURCES}) + set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${DBUS_INCLUDE_DIRS}) + set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${DBUS_LDFLAGS}) + set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_DBUS) + endif() + if (WAYLANDPP_FOUND) + set(ZWIDGET_SOURCES ${ZWIDGET_SOURCES} ${ZWIDGET_WAYLAND_SOURCES}) + set(ZWIDGET_INCLUDE_DIRS ${ZWIDGET_INCLUDE_DIRS} ${WAYLANDPP_INCLUDE_DIRS}) + set(ZWIDGET_LIBS ${ZWIDGET_LIBS} ${WAYLANDPP_LDFLAGS}) + set(ZWIDGET_DEFINES ${ZWIDGET_DEFINES} -DUSE_WAYLAND) endif() - add_definitions(-DUNIX -D_UNIX) - add_link_options(-pthread) -endif() - -if(MSVC) - # Use all cores for compilation - set(CMAKE_CXX_FLAGS "/MP ${CMAKE_CXX_FLAGS}") - - # Ignore warnings in third party code - #set_source_files_properties(${ZWIDGET_SOURCES} PROPERTIES COMPILE_FLAGS "/wd4244 /wd4267 /wd4005 /wd4018 -D_CRT_SECURE_NO_WARNINGS") endif() add_library(zwidget STATIC ${ZWIDGET_SOURCES} ${ZWIDGET_INCLUDES}) +target_compile_options(zwidget PRIVATE ${ZWIDGET_COMPILE_OPTIONS}) +target_compile_definitions(zwidget PRIVATE ${ZWIDGET_DEFINES}) +target_include_directories(zwidget PRIVATE ${ZWIDGET_INCLUDE_DIRS}) +target_link_options(zwidget PRIVATE ${ZWIDGET_LINK_OPTIONS}) target_link_libraries(zwidget ${ZWIDGET_LIBS}) set_target_properties(zwidget PROPERTIES CXX_STANDARD 17) diff --git a/libraries/ZWidget/include/zwidget/core/canvas.h b/libraries/ZWidget/include/zwidget/core/canvas.h index a546e62cc..c3ae7fea8 100644 --- a/libraries/ZWidget/include/zwidget/core/canvas.h +++ b/libraries/ZWidget/include/zwidget/core/canvas.h @@ -2,14 +2,26 @@ #include #include +#include +#include "image.h" +#include "rect.h" +#include class Font; -class Image; class Point; class Rect; class Colorf; class DisplayWindow; -struct VerticalTextPosition; +class CanvasFontGroup; + +class CanvasTexture +{ +public: + virtual ~CanvasTexture() = default; + + int Width = 0; + int Height = 0; +}; class FontMetrics { @@ -20,44 +32,85 @@ public: double height = 0.0; }; -class Canvas -{ -public: - static std::unique_ptr create(DisplayWindow* window); - - virtual ~Canvas() = default; - - virtual void begin(const Colorf& color) = 0; - virtual void end() = 0; - - virtual void begin3d() = 0; - virtual void end3d() = 0; - - virtual Point getOrigin() = 0; - virtual void setOrigin(const Point& origin) = 0; - - virtual void pushClip(const Rect& box) = 0; - virtual void popClip() = 0; - - virtual void fillRect(const Rect& box, const Colorf& color) = 0; - virtual void line(const Point& p0, const Point& p1, const Colorf& color) = 0; - - virtual void drawText(const Point& pos, const Colorf& color, const std::string& text) = 0; - virtual Rect measureText(const std::string& text) = 0; - virtual VerticalTextPosition verticalTextAlign() = 0; - - virtual void drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color) = 0; - virtual void drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color) = 0; - virtual Rect measureText(const std::shared_ptr& font, const std::string& text) = 0; - virtual FontMetrics getFontMetrics(const std::shared_ptr& font) = 0; - virtual int getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint) = 0; - - virtual void drawImage(const std::shared_ptr& image, const Point& pos) = 0; -}; - struct VerticalTextPosition { double top = 0.0; double baseline = 0.0; double bottom = 0.0; }; + +class Canvas +{ +public: + static std::unique_ptr create(); + + Canvas(); + virtual ~Canvas(); + + virtual void attach(DisplayWindow* window); + virtual void detach(); + + virtual void begin(const Colorf& color); + virtual void end() { } + + virtual void begin3d() { } + virtual void end3d() { } + + Point getOrigin(); + void setOrigin(const Point& origin); + + void pushClip(const Rect& box); + void popClip(); + + void fillRect(const Rect& box, const Colorf& color); + void line(const Point& p0, const Point& p1, const Colorf& color); + + void drawText(const Point& pos, const Colorf& color, const std::string& text); + Rect measureText(const std::string& text); + VerticalTextPosition verticalTextAlign(); + + void drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color); + void drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color); + Rect measureText(const std::shared_ptr& font, const std::string& text); + FontMetrics getFontMetrics(const std::shared_ptr& font); + int getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint); + + void drawImage(const std::shared_ptr& image, const Point& pos); + void drawImage(const std::shared_ptr& image, const Rect& box); + +protected: + virtual std::unique_ptr createTexture(int width, int height, const void* pixels, ImageFormat format = ImageFormat::B8G8R8A8) = 0; + virtual void drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color) = 0; + virtual void fillTile(float x, float y, float width, float height, Colorf color) = 0; + virtual void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) = 0; + virtual void drawGlyph(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) = 0; + + int getClipMinX() const; + int getClipMinY() const; + int getClipMaxX() const; + int getClipMaxY() const; + + template + static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } + + DisplayWindow* window = nullptr; + int width = 0; + int height = 0; + double uiscale = 1.0f; + + std::unique_ptr whiteTexture; + +private: + void setLanguage(const char* lang) { language = lang; } + void drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color); + + std::unique_ptr font; + + Point origin; + std::vector clipStack; + + std::unordered_map, std::unique_ptr> imageTextures; + std::string language; + + friend class CanvasFont; +}; diff --git a/libraries/ZWidget/include/zwidget/core/rect.h b/libraries/ZWidget/include/zwidget/core/rect.h index ad2a4e09c..3b655cd2e 100644 --- a/libraries/ZWidget/include/zwidget/core/rect.h +++ b/libraries/ZWidget/include/zwidget/core/rect.h @@ -1,5 +1,7 @@ #pragma once +#include + class Point { public: diff --git a/libraries/ZWidget/include/zwidget/core/theme.h b/libraries/ZWidget/include/zwidget/core/theme.h new file mode 100644 index 000000000..5a5271bc3 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/core/theme.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include "rect.h" +#include "colorf.h" + +class Widget; +class Canvas; + +class WidgetStyle +{ +public: + WidgetStyle(WidgetStyle* parentStyle = nullptr) : ParentStyle(parentStyle) { } + virtual ~WidgetStyle() = default; + virtual void Paint(Widget* widget, Canvas* canvas, Size size) = 0; + + void SetBool(const std::string& state, const std::string& propertyName, bool value); + void SetInt(const std::string& state, const std::string& propertyName, int value); + void SetDouble(const std::string& state, const std::string& propertyName, double value); + void SetString(const std::string& state, const std::string& propertyName, const std::string& value); + void SetColor(const std::string& state, const std::string& propertyName, const Colorf& value); + + void SetBool(const std::string& propertyName, bool value) { SetBool(std::string(), propertyName, value); } + void SetInt(const std::string& propertyName, int value) { SetInt(std::string(), propertyName, value); } + void SetDouble(const std::string& propertyName, double value) { SetDouble(std::string(), propertyName, value); } + void SetString(const std::string& propertyName, const std::string& value) { SetString(std::string(), propertyName, value); } + void SetColor(const std::string& propertyName, const Colorf& value) { SetColor(std::string(), propertyName, value); } + +private: + // Note: do not call these directly. Use widget->GetStyleXX instead since a widget may explicitly override a class style + bool GetBool(const std::string& state, const std::string& propertyName) const; + int GetInt(const std::string& state, const std::string& propertyName) const; + double GetDouble(const std::string& state, const std::string& propertyName) const; + std::string GetString(const std::string& state, const std::string& propertyName) const; + Colorf GetColor(const std::string& state, const std::string& propertyName) const; + + WidgetStyle* ParentStyle = nullptr; + typedef std::variant PropertyVariant; + std::unordered_map> StyleProperties; + + const PropertyVariant* FindProperty(const std::string& state, const std::string& propertyName) const; + + friend class Widget; +}; + +class BasicWidgetStyle : public WidgetStyle +{ +public: + using WidgetStyle::WidgetStyle; + void Paint(Widget* widget, Canvas* canvas, Size size) override; +}; + +class WidgetTheme +{ +public: + virtual ~WidgetTheme() = default; + + WidgetStyle* RegisterStyle(std::unique_ptr widgetStyle, const std::string& widgetClass); + WidgetStyle* GetStyle(const std::string& widgetClass); + + static void SetTheme(std::unique_ptr theme); + static WidgetTheme* GetTheme(); + +private: + std::unordered_map> Styles; +}; + +class DarkWidgetTheme : public WidgetTheme +{ +public: + DarkWidgetTheme(); +}; + +class LightWidgetTheme : public WidgetTheme +{ +public: + LightWidgetTheme(); +}; diff --git a/libraries/ZWidget/include/zwidget/core/widget.h b/libraries/ZWidget/include/zwidget/core/widget.h index 34faa14e9..6d87f72a0 100644 --- a/libraries/ZWidget/include/zwidget/core/widget.h +++ b/libraries/ZWidget/include/zwidget/core/widget.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include "canvas.h" #include "rect.h" #include "colorf.h" @@ -20,7 +22,7 @@ enum class WidgetType class Widget : DisplayWindowHost { public: - Widget(Widget* parent = nullptr, WidgetType type = WidgetType::Child); + Widget(Widget* parent = nullptr, WidgetType type = WidgetType::Child, RenderAPI api = RenderAPI::Unspecified); virtual ~Widget(); void SetParent(Widget* parent); @@ -39,16 +41,45 @@ public: // Widget noncontent area void SetNoncontentSizes(double left, double top, double right, double bottom); - double GetNoncontentLeft() const { return Noncontent.Left; } - double GetNoncontentTop() const { return Noncontent.Top; } - double GetNoncontentRight() const { return Noncontent.Right; } - double GetNoncontentBottom() const { return Noncontent.Bottom; } + double GetNoncontentLeft() const { return GridFitSize(GetStyleDouble("noncontent-left")); } + double GetNoncontentTop() const { return GridFitSize(GetStyleDouble("noncontent-top")); } + double GetNoncontentRight() const { return GridFitSize(GetStyleDouble("noncontent-right")); } + double GetNoncontentBottom() const { return GridFitSize(GetStyleDouble("noncontent-bottom")); } + + // Get the DPI scale factor for the window the widget is located on + double GetDpiScale() const; + + // Align point to the nearest physical screen pixel + double GridFitPoint(double p) const; + Point GridFitPoint(double x, double y) const { return GridFitPoint(Point(x, y)); } + Point GridFitPoint(Point p) const { return Point(GridFitPoint(p.x), GridFitPoint(p.y)); } + + // Convert size to exactly covering physical screen pixels + double GridFitSize(double s) const; + Size GridFitSize(double w, double h) const { return GridFitSize(Size(w, h)); } + Size GridFitSize(Size s) const { return Size(GridFitSize(s.width), GridFitSize(s.height)); } // Widget frame box Rect GetFrameGeometry() const; void SetFrameGeometry(const Rect& geometry); void SetFrameGeometry(double x, double y, double width, double height) { SetFrameGeometry(Rect::xywh(x, y, width, height)); } + // Style properties + void SetStyleClass(const std::string& styleClass); + const std::string& GetStyleClass() const { return StyleClass; } + void SetStyleState(const std::string& state); + const std::string& GetStyleState() const { return StyleState; } + void SetStyleBool(const std::string& propertyName, bool value); + void SetStyleInt(const std::string& propertyName, int value); + void SetStyleDouble(const std::string& propertyName, double value); + void SetStyleString(const std::string& propertyName, const std::string& value); + void SetStyleColor(const std::string& propertyName, const Colorf& value); + bool GetStyleBool(const std::string& propertyName) const; + int GetStyleInt(const std::string& propertyName) const; + double GetStyleDouble(const std::string& propertyName) const; + std::string GetStyleString(const std::string& propertyName) const; + Colorf GetStyleColor(const std::string& propertyName) const; + void SetWindowBackground(const Colorf& color); void SetWindowBorderColor(const Colorf& color); void SetWindowCaptionColor(const Colorf& color); @@ -73,6 +104,7 @@ public: bool IsEnabled(); bool IsVisible(); bool IsHidden(); + bool IsFullscreen(); void SetFocus(); void SetEnabled(bool value); @@ -82,19 +114,26 @@ public: void LockCursor(); void UnlockCursor(); void SetCursor(StandardCursor cursor); - void CaptureMouse(); - void ReleaseMouseCapture(); - bool GetKeyState(EInputKey key); + void SetPointerCapture(); + void ReleasePointerCapture(); + + void SetModalCapture(); + void ReleaseModalCapture(); + + bool GetKeyState(InputKey key); std::string GetClipboardText(); void SetClipboardText(const std::string& text); - Widget* Window(); + Widget* Window() const; Canvas* GetCanvas() const; Widget* ChildAt(double x, double y) { return ChildAt(Point(x, y)); } Widget* ChildAt(const Point& pos); + bool IsParent(const Widget* w) const; + bool IsChild(const Widget* w) const; + Widget* Parent() const { return ParentObj; } Widget* PrevSibling() const { return PrevSiblingObj; } Widget* NextSibling() const { return NextSiblingObj; } @@ -111,19 +150,28 @@ public: static Size GetScreenSize(); + void SetCanvas(std::unique_ptr canvas); + void* GetNativeHandle(); + int GetNativePixelWidth(); + int GetNativePixelHeight(); + + // Vulkan support: + std::vector GetVulkanInstanceExtensions() { return Window()->DispWindow->GetVulkanInstanceExtensions(); } + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) { return Window()->DispWindow->CreateVulkanSurface(instance); } + protected: - virtual void OnPaintFrame(Canvas* canvas) { } + virtual void OnPaintFrame(Canvas* canvas); virtual void OnPaint(Canvas* canvas) { } - virtual bool OnMouseDown(const Point& pos, int key) { return false; } - virtual bool OnMouseDoubleclick(const Point& pos, int key) { return false; } - virtual bool OnMouseUp(const Point& pos, int key) { return false; } - virtual bool OnMouseWheel(const Point& pos, EInputKey key) { return false; } + virtual bool OnMouseDown(const Point& pos, InputKey key) { return false; } + virtual bool OnMouseDoubleclick(const Point& pos, InputKey key) { return false; } + virtual bool OnMouseUp(const Point& pos, InputKey key) { return false; } + virtual bool OnMouseWheel(const Point& pos, InputKey key) { return false; } virtual void OnMouseMove(const Point& pos) { } virtual void OnMouseLeave() { } virtual void OnRawMouseMove(int dx, int dy) { } virtual void OnKeyChar(std::string chars) { } - virtual void OnKeyDown(EInputKey key) { } - virtual void OnKeyUp(EInputKey key) { } + virtual void OnKeyDown(InputKey key) { } + virtual void OnKeyUp(InputKey key) { } virtual void OnGeometryChanged() { } virtual void OnClose() { delete this; } virtual void OnSetFocus() { } @@ -138,14 +186,15 @@ private: // DisplayWindowHost void OnWindowPaint() override; void OnWindowMouseMove(const Point& pos) override; - void OnWindowMouseDown(const Point& pos, EInputKey key) override; - void OnWindowMouseDoubleclick(const Point& pos, EInputKey key) override; - void OnWindowMouseUp(const Point& pos, EInputKey key) override; - void OnWindowMouseWheel(const Point& pos, EInputKey key) override; + void OnWindowMouseLeave() override; + void OnWindowMouseDown(const Point& pos, InputKey key) override; + void OnWindowMouseDoubleclick(const Point& pos, InputKey key) override; + void OnWindowMouseUp(const Point& pos, InputKey key) override; + void OnWindowMouseWheel(const Point& pos, InputKey key) override; void OnWindowRawMouseMove(int dx, int dy) override; void OnWindowKeyChar(std::string chars) override; - void OnWindowKeyDown(EInputKey key) override; - void OnWindowKeyUp(EInputKey key) override; + void OnWindowKeyDown(InputKey key) override; + void OnWindowKeyUp(InputKey key) override; void OnWindowGeometryChanged() override; void OnWindowClose() override; void OnWindowActivated() override; @@ -167,14 +216,6 @@ private: Colorf WindowBackground = Colorf::fromRgba8(240, 240, 240); - struct - { - double Left = 0.0; - double Top = 0.0; - double Right = 0.0; - double Bottom = 0.0; - } Noncontent; - std::string WindowTitle; std::unique_ptr DispWindow; std::unique_ptr DispCanvas; @@ -185,8 +226,16 @@ private: StandardCursor CurrentCursor = StandardCursor::arrow; + std::string StyleClass = "widget"; + std::string StyleState; + typedef std::variant PropertyVariant; + std::unordered_map StyleProperties; + Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete; friend class Timer; + friend class OpenFileDialog; + friend class OpenFolderDialog; + friend class SaveFileDialog; }; diff --git a/libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h b/libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h new file mode 100644 index 000000000..29ede966e --- /dev/null +++ b/libraries/ZWidget/include/zwidget/systemdialogs/open_file_dialog.h @@ -0,0 +1,55 @@ + +#pragma once + +#include +#include +#include + +class Widget; + +/// \brief Displays the system open file dialog. +class OpenFileDialog +{ +public: + /// \brief Constructs an open file dialog. + static std::unique_ptr Create(Widget* owner); + + virtual ~OpenFileDialog() = default; + + /// \brief Get the full path of the file selected. + /// + /// If multiple files are selected, this returns the first file. + virtual std::string Filename() const = 0; + + /// \brief Gets an array that contains one file name for each selected file. + virtual std::vector Filenames() const = 0; + + /// \brief Sets if multiple files can be selected or not. + /// \param multiselect = When true, multiple items can be selected. + virtual void SetMultiSelect(bool multiselect) = 0; + + /// \brief Sets a string containing the full path of the file selected. + virtual void SetFilename(const std::string &filename) = 0; + + /// \brief Sets the default extension to use. + virtual void SetDefaultExtension(const std::string& extension) = 0; + + /// \brief Add a filter that determines what types of files are displayed. + virtual void AddFilter(const std::string &filter_description, const std::string &filter_extension) = 0; + + /// \brief Clears all filters. + virtual void ClearFilters() = 0; + + /// \brief Sets a default filter, on a 0-based index. + virtual void SetFilterIndex(int filter_index) = 0; + + /// \brief Sets the initial directory that is displayed. + virtual void SetInitialDirectory(const std::string &path) = 0; + + /// \brief Sets the text that appears in the title bar. + virtual void SetTitle(const std::string &title) = 0; + + /// \brief Shows the file dialog. + /// \return true if the user clicks the OK button of the dialog that is displayed, false otherwise. + virtual bool Show() = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h b/libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h new file mode 100644 index 000000000..9727f353b --- /dev/null +++ b/libraries/ZWidget/include/zwidget/systemdialogs/open_folder_dialog.h @@ -0,0 +1,30 @@ + +#pragma once + +#include +#include + +class Widget; + +/// \brief Displays the system folder browsing dialog +class OpenFolderDialog +{ +public: + /// \brief Constructs an browse folder dialog. + static std::unique_ptr Create(Widget*owner); + + virtual ~OpenFolderDialog() = default; + + /// \brief Get the full path of the directory selected. + virtual std::string SelectedPath() const = 0; + + /// \brief Sets the initial directory that is displayed. + virtual void SetInitialDirectory(const std::string& path) = 0; + + /// \brief Sets the text that appears in the title bar. + virtual void SetTitle(const std::string& title) = 0; + + /// \brief Shows the file dialog. + /// \return true if the user clicks the OK button of the dialog that is displayed, false otherwise. + virtual bool Show() = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h b/libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h new file mode 100644 index 000000000..ce10a127f --- /dev/null +++ b/libraries/ZWidget/include/zwidget/systemdialogs/save_file_dialog.h @@ -0,0 +1,46 @@ + +#pragma once + +#include +#include +#include + +class Widget; + +/// \brief Displays the system save file dialog. +class SaveFileDialog +{ +public: + /// \brief Constructs a save file dialog. + static std::unique_ptr Create(Widget *owner); + + virtual ~SaveFileDialog() = default; + + /// \brief Get the full path of the file selected. + virtual std::string Filename() const = 0; + + /// \brief Sets a string containing the full path of the file selected. + virtual void SetFilename(const std::string &filename) = 0; + + /// \brief Sets the default extension to use. + virtual void SetDefaultExtension(const std::string& extension) = 0; + + /// \brief Add a filter that determines what types of files are displayed. + virtual void AddFilter(const std::string &filter_description, const std::string &filter_extension) = 0; + + /// \brief Clears all filters. + virtual void ClearFilters() = 0; + + /// \brief Sets a default filter, on a 0-based index. + virtual void SetFilterIndex(int filter_index) = 0; + + /// \brief Sets the initial directory that is displayed. + virtual void SetInitialDirectory(const std::string &path) = 0; + + /// \brief Sets the text that appears in the title bar. + virtual void SetTitle(const std::string &title) = 0; + + /// \brief Shows the file dialog. + /// \return true if the user clicks the OK button of the dialog that is displayed, false otherwise. + virtual bool Show() = 0; +}; diff --git a/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h b/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h index c3f3fb465..21ce0b697 100644 --- a/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h +++ b/libraries/ZWidget/include/zwidget/widgets/checkboxlabel/checkboxlabel.h @@ -21,15 +21,14 @@ public: protected: void OnPaint(Canvas* canvas) override; - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseUp(const Point& pos, int key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; void OnMouseLeave() override; - void OnKeyUp(EInputKey key) override; + void OnKeyUp(InputKey key) override; private: std::string text; bool checked = false; bool radiostyle = false; bool mouseDownActive = false; - }; diff --git a/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h b/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h index e3a724381..7cae08c24 100644 --- a/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h +++ b/libraries/ZWidget/include/zwidget/widgets/imagebox/imagebox.h @@ -4,13 +4,22 @@ #include "../../core/widget.h" #include "../../core/image.h" +enum ImageBoxMode +{ + Center, + Contain, + Cover +}; + class ImageBox : public Widget { public: ImageBox(Widget* parent); void SetImage(std::shared_ptr newImage); + void SetImageMode(ImageBoxMode mode); + double GetPreferredWidth() const; double GetPreferredHeight() const; protected: @@ -18,4 +27,5 @@ protected: private: std::shared_ptr image; + ImageBoxMode mode = ImageBoxMode::Center; }; diff --git a/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h b/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h index 526d9eee9..8c5dcfa89 100644 --- a/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h +++ b/libraries/ZWidget/include/zwidget/widgets/lineedit/lineedit.h @@ -59,7 +59,7 @@ public: void SetInputMask(const std::string& mask); void SetDecimalCharacter(const std::string& decimal_char); - std::function FuncIgnoreKeyDown; + std::function FuncIgnoreKeyDown; std::function FuncFilterKeyChar; std::function FuncBeforeEditChanged; std::function FuncAfterEditChanged; @@ -69,15 +69,14 @@ public: std::function FuncEnterPressed; protected: - void OnPaintFrame(Canvas* canvas) override; void OnPaint(Canvas* canvas) override; void OnMouseMove(const Point& pos) override; - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseDoubleclick(const Point& pos, int key) override; - bool OnMouseUp(const Point& pos, int key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseDoubleclick(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; void OnKeyChar(std::string chars) override; - void OnKeyDown(EInputKey key) override; - void OnKeyUp(EInputKey key) override; + void OnKeyDown(InputKey key) override; + void OnKeyUp(InputKey key) override; void OnGeometryChanged() override; void OnEnableChanged() override; void OnSetFocus() override; diff --git a/libraries/ZWidget/include/zwidget/widgets/listview/listview.h b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h index 550cc07f9..433119759 100644 --- a/libraries/ZWidget/include/zwidget/widgets/listview/listview.h +++ b/libraries/ZWidget/include/zwidget/widgets/listview/listview.h @@ -29,11 +29,10 @@ public: protected: void OnPaint(Canvas* canvas) override; - void OnPaintFrame(Canvas* canvas) override; - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseDoubleclick(const Point& pos, int key) override; - bool OnMouseWheel(const Point& pos, EInputKey key) override; - void OnKeyDown(EInputKey key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseDoubleclick(const Point& pos, InputKey key) override; + bool OnMouseWheel(const Point& pos, InputKey key) override; + void OnKeyDown(InputKey key) override; void OnGeometryChanged() override; void OnScrollbarScroll(); diff --git a/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h b/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h index abba23d6c..13498e56d 100644 --- a/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h +++ b/libraries/ZWidget/include/zwidget/widgets/mainwindow/mainwindow.h @@ -10,11 +10,12 @@ class Statusbar; class MainWindow : public Widget { public: - MainWindow(); + MainWindow(RenderAPI api = RenderAPI::Unspecified); ~MainWindow(); Menubar* GetMenubar() const { return MenubarWidget; } - Toolbar* GetToolbar() const { return ToolbarWidget; } + Toolbar* GetTopToolbar() const { return TopToolbarWidget; } + Toolbar* GetLeftToolbar() const { return LeftToolbarWidget; } Statusbar* GetStatusbar() const { return StatusbarWidget; } Widget* GetCentralWidget() const { return CentralWidget; } @@ -25,7 +26,8 @@ protected: private: Menubar* MenubarWidget = nullptr; - Toolbar* ToolbarWidget = nullptr; + Toolbar* TopToolbarWidget = nullptr; + Toolbar* LeftToolbarWidget = nullptr; Widget* CentralWidget = nullptr; Statusbar* StatusbarWidget = nullptr; }; diff --git a/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h b/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h index 9c13283cc..1881111f4 100644 --- a/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h +++ b/libraries/ZWidget/include/zwidget/widgets/menubar/menubar.h @@ -2,6 +2,14 @@ #pragma once #include "../../core/widget.h" +#include "../textlabel/textlabel.h" +#include "../imagebox/imagebox.h" +#include + +class Menu; +class MenubarItem; +class MenuItem; +class MenuItemSeparator; class Menubar : public Widget { @@ -9,6 +17,105 @@ public: Menubar(Widget* parent); ~Menubar(); + MenubarItem* AddItem(std::string text, std::function onOpen, bool alignRight = false); + +protected: + void OnGeometryChanged() override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnKeyDown(InputKey key) override; + +private: + void ShowMenu(MenubarItem* item); + void CloseMenu(); + + MenubarItem* GetMenubarItemAt(const Point& pos); + int GetItemIndex(MenubarItem* item); + + std::vector menuItems; + int currentMenubarItem = -1; + Menu* openMenu = nullptr; + bool modalMode = false; + + friend class MenubarItem; +}; + +class MenubarItem : public Widget +{ +public: + MenubarItem(Menubar* menubar, std::string text, bool alignRight); + + void SetOpenCallback(std::function callback) { onOpen = std::move(callback); } + const std::function& GetOpenCallback() const { return onOpen; } + + double GetPreferredWidth() const; + + bool AlignRight = false; + +protected: + void OnPaint(Canvas* canvas) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + +private: + Menubar* menubar = nullptr; + std::function onOpen; + std::string text; +}; + +class Menu : public Widget +{ +public: + Menu(Widget* parent); + + void SetLeftPosition(const Point& globalPos); + void SetRightPosition(const Point& globalPos); + MenuItem* AddItem(std::shared_ptr icon, std::string text, std::function onClick = {}); + MenuItemSeparator* AddSeparator(); + + double GetPreferredWidth() const; + double GetPreferredHeight() const; + + void SetSelected(MenuItem* item); + +protected: + void OnGeometryChanged() override; + + std::function onCloseMenu; + MenuItem* selectedItem = nullptr; + + friend class Menubar; +}; + +class MenuItem : public Widget +{ +public: + MenuItem(Menu* menu, std::function onClick); + + void Click(); + + Menu* menu = nullptr; + ImageBox* icon = nullptr; + TextLabel* text = nullptr; + +protected: + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + void OnGeometryChanged() override; + +private: + std::function onClick; +}; + +class MenuItemSeparator : public Widget +{ +public: + MenuItemSeparator(Widget* parent); + protected: void OnPaint(Canvas* canvas) override; }; diff --git a/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h b/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h index b9b6c9654..d3fe0295b 100644 --- a/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h +++ b/libraries/ZWidget/include/zwidget/widgets/pushbutton/pushbutton.h @@ -19,17 +19,14 @@ public: std::function OnClick; protected: - void OnPaintFrame(Canvas* canvas) override; void OnPaint(Canvas* canvas) override; - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseUp(const Point& pos, int key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; void OnMouseMove(const Point& pos) override; void OnMouseLeave() override; - void OnKeyDown(EInputKey key) override; - void OnKeyUp(EInputKey key) override; + void OnKeyDown(InputKey key) override; + void OnKeyUp(InputKey key) override; private: std::string text; - bool buttonDown = false; - bool hot = false; }; diff --git a/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h b/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h index 5ec95c841..6e169b1fe 100644 --- a/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h +++ b/libraries/ZWidget/include/zwidget/widgets/scrollbar/scrollbar.h @@ -47,8 +47,8 @@ public: std::function FuncScrollEnd; protected: - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseUp(const Point& pos, int key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; void OnMouseMove(const Point& pos) override; void OnMouseLeave() override; void OnPaint(Canvas* canvas) override; diff --git a/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h b/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h index 242094024..00bb3d890 100644 --- a/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h +++ b/libraries/ZWidget/include/zwidget/widgets/tabwidget/tabwidget.h @@ -2,9 +2,9 @@ #pragma once #include "../../core/widget.h" -#include #include #include +#include class TabBar; class TabBarTab; @@ -37,7 +37,6 @@ public: std::function OnCurrentChanged; protected: - void OnPaintFrame(Canvas* canvas) override; void OnGeometryChanged() override; private: @@ -67,7 +66,6 @@ public: std::function OnCurrentChanged; protected: - void OnPaintFrame(Canvas* canvas) override; void OnGeometryChanged() override; private: @@ -92,10 +90,9 @@ public: std::function OnClick; protected: - void OnPaintFrame(Canvas* canvas) override; void OnGeometryChanged() override; - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseUp(const Point& pos, int key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; void OnMouseMove(const Point& pos) override; void OnMouseLeave() override; @@ -116,7 +113,6 @@ public: Widget* GetCurrentWidget() const { return CurrentWidget; } protected: - void OnPaintFrame(Canvas* canvas) override; void OnGeometryChanged() override; private: diff --git a/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h b/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h index ed80eec36..f392f85d6 100644 --- a/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h +++ b/libraries/ZWidget/include/zwidget/widgets/textedit/textedit.h @@ -53,15 +53,14 @@ public: std::function FuncEnterPressed; protected: - void OnPaintFrame(Canvas* canvas) override; void OnPaint(Canvas* canvas) override; void OnMouseMove(const Point& pos) override; - bool OnMouseDown(const Point& pos, int key) override; - bool OnMouseDoubleclick(const Point& pos, int key) override; - bool OnMouseUp(const Point& pos, int key) override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseDoubleclick(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; void OnKeyChar(std::string chars) override; - void OnKeyDown(EInputKey key) override; - void OnKeyUp(EInputKey key) override; + void OnKeyDown(InputKey key) override; + void OnKeyUp(InputKey key) override; void OnGeometryChanged() override; void OnEnableChanged() override; void OnSetFocus() override; diff --git a/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h b/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h index 45515dfcb..18cfcbf5f 100644 --- a/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h +++ b/libraries/ZWidget/include/zwidget/widgets/textlabel/textlabel.h @@ -3,7 +3,7 @@ #include "../../core/widget.h" -enum TextLabelAlignment +enum class TextLabelAlignment { Left, Center, diff --git a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h index ca0a89c0d..fac66b97d 100644 --- a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h +++ b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbar.h @@ -3,9 +3,29 @@ #include "../../core/widget.h" +enum class ToolbarDirection +{ + Horizontal, + Vertical +}; + +class ToolbarButton; + class Toolbar : public Widget { public: Toolbar(Widget* parent); ~Toolbar(); + + void SetDirection(ToolbarDirection direction); + ToolbarDirection GetDirection(ToolbarDirection) const { return direction; } + + ToolbarButton* AddButton(std::string icon, std::string text, std::function onClicked = {}); + +protected: + void OnGeometryChanged() override; + +private: + ToolbarDirection direction = ToolbarDirection::Horizontal; + std::vector buttons; }; diff --git a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h index 933a6517e..f6e1557a1 100644 --- a/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h +++ b/libraries/ZWidget/include/zwidget/widgets/toolbar/toolbarbutton.h @@ -2,6 +2,8 @@ #pragma once #include "../../core/widget.h" +#include "../textlabel/textlabel.h" +#include "../imagebox/imagebox.h" class ToolbarButton : public Widget { @@ -9,6 +11,24 @@ public: ToolbarButton(Widget* parent); ~ToolbarButton(); + void SetIcon(std::string icon); + void SetText(std::string text); + + void Click(); + + double GetPreferredWidth(); + double GetPreferredHeight(); + + std::function OnClick; + protected: - void OnPaint(Canvas* canvas) override; + void OnMouseMove(const Point& pos) override; + void OnMouseLeave() override; + bool OnMouseDown(const Point& pos, InputKey key) override; + bool OnMouseUp(const Point& pos, InputKey key) override; + void OnGeometryChanged() override; + +private: + ImageBox* image = nullptr; + TextLabel* label = nullptr; }; diff --git a/libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h b/libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h new file mode 100644 index 000000000..9e396a80b --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/sdl2nativehandle.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +struct SDL_Window; + +class SDL2NativeHandle +{ +public: + SDL_Window* window = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/window/waylandnativehandle.h b/libraries/ZWidget/include/zwidget/window/waylandnativehandle.h new file mode 100644 index 000000000..b7358d782 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/waylandnativehandle.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class WaylandNativeHandle +{ +public: + wl_display* display = nullptr; + wl_surface* surface = nullptr; +}; diff --git a/libraries/ZWidget/include/zwidget/window/win32nativehandle.h b/libraries/ZWidget/include/zwidget/window/win32nativehandle.h new file mode 100644 index 000000000..979ac63a1 --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/win32nativehandle.h @@ -0,0 +1,9 @@ +#pragma once + +typedef struct HWND__* HWND; + +class Win32NativeHandle +{ +public: + HWND hwnd = {}; +}; diff --git a/libraries/ZWidget/include/zwidget/window/window.h b/libraries/ZWidget/include/zwidget/window/window.h index 4cdb748d8..ad59f1f02 100644 --- a/libraries/ZWidget/include/zwidget/window/window.h +++ b/libraries/ZWidget/include/zwidget/window/window.h @@ -1,12 +1,31 @@ #pragma once -#include #include #include #include +#include +#include #include "../core/rect.h" -class Engine; +#ifndef VULKAN_H_ + +#define VK_DEFINE_HANDLE(object) typedef struct object##_T* object; + +#if defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef struct object##_T *object; +#else +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; +#endif + +VK_DEFINE_HANDLE(VkInstance) +VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkSurfaceKHR) + +#endif + +class Widget; +class OpenFileDialog; +class SaveFileDialog; +class OpenFolderDialog; enum class StandardCursor { @@ -25,91 +44,83 @@ enum class StandardCursor wait }; -enum EInputKey +enum class InputKey : uint32_t { - IK_None, IK_LeftMouse, IK_RightMouse, IK_Cancel, - IK_MiddleMouse, IK_Unknown05, IK_Unknown06, IK_Unknown07, - IK_Backspace, IK_Tab, IK_Unknown0A, IK_Unknown0B, - IK_Unknown0C, IK_Enter, IK_Unknown0E, IK_Unknown0F, - IK_Shift, IK_Ctrl, IK_Alt, IK_Pause, - IK_CapsLock, IK_Unknown15, IK_Unknown16, IK_Unknown17, - IK_Unknown18, IK_Unknown19, IK_Unknown1A, IK_Escape, - IK_Unknown1C, IK_Unknown1D, IK_Unknown1E, IK_Unknown1F, - IK_Space, IK_PageUp, IK_PageDown, IK_End, - IK_Home, IK_Left, IK_Up, IK_Right, - IK_Down, IK_Select, IK_Print, IK_Execute, - IK_PrintScrn, IK_Insert, IK_Delete, IK_Help, - IK_0, IK_1, IK_2, IK_3, - IK_4, IK_5, IK_6, IK_7, - IK_8, IK_9, IK_Unknown3A, IK_Unknown3B, - IK_Unknown3C, IK_Unknown3D, IK_Unknown3E, IK_Unknown3F, - IK_Unknown40, IK_A, IK_B, IK_C, - IK_D, IK_E, IK_F, IK_G, - IK_H, IK_I, IK_J, IK_K, - IK_L, IK_M, IK_N, IK_O, - IK_P, IK_Q, IK_R, IK_S, - IK_T, IK_U, IK_V, IK_W, - IK_X, IK_Y, IK_Z, IK_Unknown5B, - IK_Unknown5C, IK_Unknown5D, IK_Unknown5E, IK_Unknown5F, - IK_NumPad0, IK_NumPad1, IK_NumPad2, IK_NumPad3, - IK_NumPad4, IK_NumPad5, IK_NumPad6, IK_NumPad7, - IK_NumPad8, IK_NumPad9, IK_GreyStar, IK_GreyPlus, - IK_Separator, IK_GreyMinus, IK_NumPadPeriod, IK_GreySlash, - IK_F1, IK_F2, IK_F3, IK_F4, - IK_F5, IK_F6, IK_F7, IK_F8, - IK_F9, IK_F10, IK_F11, IK_F12, - IK_F13, IK_F14, IK_F15, IK_F16, - IK_F17, IK_F18, IK_F19, IK_F20, - IK_F21, IK_F22, IK_F23, IK_F24, - IK_Unknown88, IK_Unknown89, IK_Unknown8A, IK_Unknown8B, - IK_Unknown8C, IK_Unknown8D, IK_Unknown8E, IK_Unknown8F, - IK_NumLock, IK_ScrollLock, IK_Unknown92, IK_Unknown93, - IK_Unknown94, IK_Unknown95, IK_Unknown96, IK_Unknown97, - IK_Unknown98, IK_Unknown99, IK_Unknown9A, IK_Unknown9B, - IK_Unknown9C, IK_Unknown9D, IK_Unknown9E, IK_Unknown9F, - IK_LShift, IK_RShift, IK_LControl, IK_RControl, - IK_UnknownA4, IK_UnknownA5, IK_UnknownA6, IK_UnknownA7, - IK_UnknownA8, IK_UnknownA9, IK_UnknownAA, IK_UnknownAB, - IK_UnknownAC, IK_UnknownAD, IK_UnknownAE, IK_UnknownAF, - IK_UnknownB0, IK_UnknownB1, IK_UnknownB2, IK_UnknownB3, - IK_UnknownB4, IK_UnknownB5, IK_UnknownB6, IK_UnknownB7, - IK_UnknownB8, IK_UnknownB9, IK_Semicolon, IK_Equals, - IK_Comma, IK_Minus, IK_Period, IK_Slash, - IK_Tilde, IK_UnknownC1, IK_UnknownC2, IK_UnknownC3, - IK_UnknownC4, IK_UnknownC5, IK_UnknownC6, IK_UnknownC7, - IK_Joy1, IK_Joy2, IK_Joy3, IK_Joy4, - IK_Joy5, IK_Joy6, IK_Joy7, IK_Joy8, - IK_Joy9, IK_Joy10, IK_Joy11, IK_Joy12, - IK_Joy13, IK_Joy14, IK_Joy15, IK_Joy16, - IK_UnknownD8, IK_UnknownD9, IK_UnknownDA, IK_LeftBracket, - IK_Backslash, IK_RightBracket, IK_SingleQuote, IK_UnknownDF, - IK_JoyX, IK_JoyY, IK_JoyZ, IK_JoyR, - IK_MouseX, IK_MouseY, IK_MouseZ, IK_MouseW, - IK_JoyU, IK_JoyV, IK_UnknownEA, IK_UnknownEB, - IK_MouseWheelUp, IK_MouseWheelDown, IK_Unknown10E, IK_Unknown10F, - IK_JoyPovUp, IK_JoyPovDown, IK_JoyPovLeft, IK_JoyPovRight, - IK_UnknownF4, IK_UnknownF5, IK_Attn, IK_CrSel, - IK_ExSel, IK_ErEof, IK_Play, IK_Zoom, - IK_NoName, IK_PA1, IK_OEMClear + None, LeftMouse, RightMouse, Cancel, + MiddleMouse, Unknown05, Unknown06, Unknown07, + Backspace, Tab, Unknown0A, Unknown0B, + Unknown0C, Enter, Unknown0E, Unknown0F, + Shift, Ctrl, Alt, Pause, + CapsLock, Unknown15, Unknown16, Unknown17, + Unknown18, Unknown19, Unknown1A, Escape, + Unknown1C, Unknown1D, Unknown1E, Unknown1F, + Space, PageUp, PageDown, End, + Home, Left, Up, Right, + Down, Select, Print, Execute, + PrintScrn, Insert, Delete, Help, + _0, _1, _2, _3, + _4, _5, _6, _7, + _8, _9, Unknown3A, Unknown3B, + Unknown3C, Unknown3D, Unknown3E, Unknown3F, + Unknown40, A, B, C, + D, E, F, G, + H, I, J, K, + L, M, N, O, + P, Q, R, S, + T, U, V, W, + X, Y, Z, Unknown5B, + Unknown5C, Unknown5D, Unknown5E, Unknown5F, + NumPad0, NumPad1, NumPad2, NumPad3, + NumPad4, NumPad5, NumPad6, NumPad7, + NumPad8, NumPad9, GreyStar, GreyPlus, + Separator, GreyMinus, NumPadPeriod, GreySlash, + F1, F2, F3, F4, + F5, F6, F7, F8, + F9, F10, F11, F12, + F13, F14, F15, F16, + F17, F18, F19, F20, + F21, F22, F23, F24, + Unknown88, Unknown89, Unknown8A, Unknown8B, + Unknown8C, Unknown8D, Unknown8E, Unknown8F, + NumLock, ScrollLock, Unknown92, Unknown93, + Unknown94, Unknown95, Unknown96, Unknown97, + Unknown98, Unknown99, Unknown9A, Unknown9B, + Unknown9C, Unknown9D, Unknown9E, Unknown9F, + LShift, RShift, LControl, RControl, + UnknownA4, UnknownA5, UnknownA6, UnknownA7, + UnknownA8, UnknownA9, UnknownAA, UnknownAB, + UnknownAC, UnknownAD, UnknownAE, UnknownAF, + UnknownB0, UnknownB1, UnknownB2, UnknownB3, + UnknownB4, UnknownB5, UnknownB6, UnknownB7, + UnknownB8, UnknownB9, Semicolon, Equals, + Comma, Minus, Period, Slash, + Tilde, UnknownC1, UnknownC2, UnknownC3, + UnknownC4, UnknownC5, UnknownC6, UnknownC7, + Joy1, Joy2, Joy3, Joy4, + Joy5, Joy6, Joy7, Joy8, + Joy9, Joy10, Joy11, Joy12, + Joy13, Joy14, Joy15, Joy16, + UnknownD8, UnknownD9, UnknownDA, LeftBracket, + Backslash, RightBracket, SingleQuote, UnknownDF, + JoyX, JoyY, JoyZ, JoyR, + MouseX, MouseY, MouseZ, MouseW, + JoyU, JoyV, UnknownEA, UnknownEB, + MouseWheelUp, MouseWheelDown, Unknown10E, Unknown10F, + JoyPovUp, JoyPovDown, JoyPovLeft, JoyPovRight, + UnknownF4, UnknownF5, Attn, CrSel, + ExSel, ErEof, Play, Zoom, + NoName, PA1, OEMClear }; -enum EInputType +enum class RenderAPI { - IST_None, - IST_Press, - IST_Hold, - IST_Release, - IST_Axis -}; - -class KeyEvent -{ -public: - EInputKey Key; - bool Ctrl = false; - bool Alt = false; - bool Shift = false; - Point MousePos = Point(0.0, 0.0); + Unspecified, + Bitmap, + Vulkan, + OpenGL, + D3D11, + D3D12, + Metal }; class DisplayWindow; @@ -119,14 +130,15 @@ class DisplayWindowHost public: virtual void OnWindowPaint() = 0; virtual void OnWindowMouseMove(const Point& pos) = 0; - virtual void OnWindowMouseDown(const Point& pos, EInputKey key) = 0; - virtual void OnWindowMouseDoubleclick(const Point& pos, EInputKey key) = 0; - virtual void OnWindowMouseUp(const Point& pos, EInputKey key) = 0; - virtual void OnWindowMouseWheel(const Point& pos, EInputKey key) = 0; + virtual void OnWindowMouseLeave() = 0; + virtual void OnWindowMouseDown(const Point& pos, InputKey key) = 0; + virtual void OnWindowMouseDoubleclick(const Point& pos, InputKey key) = 0; + virtual void OnWindowMouseUp(const Point& pos, InputKey key) = 0; + virtual void OnWindowMouseWheel(const Point& pos, InputKey key) = 0; virtual void OnWindowRawMouseMove(int dx, int dy) = 0; virtual void OnWindowKeyChar(std::string chars) = 0; - virtual void OnWindowKeyDown(EInputKey key) = 0; - virtual void OnWindowKeyUp(EInputKey key) = 0; + virtual void OnWindowKeyDown(InputKey key) = 0; + virtual void OnWindowKeyUp(InputKey key) = 0; virtual void OnWindowGeometryChanged() = 0; virtual void OnWindowClose() = 0; virtual void OnWindowActivated() = 0; @@ -137,7 +149,7 @@ public: class DisplayWindow { public: - static std::unique_ptr Create(DisplayWindowHost* windowHost); + static std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI); static void ProcessEvents(); static void RunLoop(); @@ -158,6 +170,7 @@ public: virtual void ShowMaximized() = 0; virtual void ShowMinimized() = 0; virtual void ShowNormal() = 0; + virtual bool IsWindowFullscreen() = 0; virtual void Hide() = 0; virtual void Activate() = 0; virtual void ShowCursor(bool enable) = 0; @@ -166,7 +179,7 @@ public: virtual void CaptureMouse() = 0; virtual void ReleaseMouseCapture() = 0; virtual void Update() = 0; - virtual bool GetKeyState(EInputKey key) = 0; + virtual bool GetKeyState(InputKey key) = 0; virtual void SetCursor(StandardCursor cursor) = 0; @@ -176,6 +189,9 @@ public: virtual int GetPixelHeight() const = 0; virtual double GetDpiScale() const = 0; + virtual Point MapFromGlobal(const Point& pos) const = 0; + virtual Point MapToGlobal(const Point& pos) const = 0; + virtual void SetBorderColor(uint32_t bgra8) = 0; virtual void SetCaptionColor(uint32_t bgra8) = 0; virtual void SetCaptionTextColor(uint32_t bgra8) = 0; @@ -184,4 +200,44 @@ public: virtual std::string GetClipboardText() = 0; virtual void SetClipboardText(const std::string& text) = 0; + + virtual void* GetNativeHandle() = 0; + + virtual std::vector GetVulkanInstanceExtensions() = 0; + virtual VkSurfaceKHR CreateVulkanSurface(VkInstance instance) = 0; +}; + +class DisplayBackend +{ +public: + static DisplayBackend* Get(); + static void Set(std::unique_ptr instance); + + static std::unique_ptr TryCreateWin32(); + static std::unique_ptr TryCreateSDL2(); + static std::unique_ptr TryCreateX11(); + static std::unique_ptr TryCreateWayland(); + + static std::unique_ptr TryCreateBackend(); + + virtual ~DisplayBackend() = default; + + virtual bool IsWin32() { return false; } + virtual bool IsSDL2() { return false; } + virtual bool IsX11() { return false; } + virtual bool IsWayland() { return false; } + + virtual std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) = 0; + virtual void ProcessEvents() = 0; + virtual void RunLoop() = 0; + virtual void ExitLoop() = 0; + + virtual void* StartTimer(int timeoutMilliseconds, std::function onTimer) = 0; + virtual void StopTimer(void* timerID) = 0; + + virtual Size GetScreenSize() = 0; + + virtual std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner); + virtual std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner); + virtual std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner); }; diff --git a/libraries/ZWidget/include/zwidget/window/x11nativehandle.h b/libraries/ZWidget/include/zwidget/window/x11nativehandle.h new file mode 100644 index 000000000..f78247b2f --- /dev/null +++ b/libraries/ZWidget/include/zwidget/window/x11nativehandle.h @@ -0,0 +1,13 @@ +#pragma once + +typedef struct _XDisplay Display; +typedef unsigned long XID; +typedef unsigned long VisualID; +typedef XID Window; + +class X11NativeHandle +{ +public: + Display* display = nullptr; + Window window = 0; +}; diff --git a/libraries/ZWidget/src/core/canvas.cpp b/libraries/ZWidget/src/core/canvas.cpp index 76c7a16e7..bc72b8f19 100644 --- a/libraries/ZWidget/src/core/canvas.cpp +++ b/libraries/ZWidget/src/core/canvas.cpp @@ -13,13 +13,12 @@ #include #include -class CanvasTexture -{ -public: - int Width = 0; - int Height = 0; - std::vector Data; -}; +#if defined(__SSE2__) || defined(_M_X64) +#include +#define USE_SSE2 +#endif + +//////////////////////////////////////////////////////////////////////////// class CanvasGlyph { @@ -41,78 +40,12 @@ public: class CanvasFont { public: - CanvasFont(const std::string& fontname, double height, std::vector& _data) : fontname(fontname), height(height) - { - auto tdata = std::make_shared(_data); - ttf = std::make_unique(tdata); - textmetrics = ttf->GetTextMetrics(height); - } + CanvasFont(const std::string& fontname, double height, std::vector data); + ~CanvasFont(); - ~CanvasFont() - { - } - - CanvasGlyph* getGlyph(uint32_t utfchar) - { - uint32_t glyphIndex = ttf->GetGlyphIndex(utfchar); - if (glyphIndex == 0) return nullptr; - - auto& glyph = glyphs[glyphIndex]; - if (glyph) - return glyph.get(); - - glyph = std::make_unique(); - - TrueTypeGlyph ttfglyph = ttf->LoadGlyph(glyphIndex, height); - - // Create final subpixel version - int w = ttfglyph.width; - int h = ttfglyph.height; - int destwidth = (w + 2) / 3; - auto texture = std::make_shared(); - texture->Width = destwidth; - texture->Height = h; - texture->Data.resize(destwidth * h); - - uint8_t* grayscale = ttfglyph.grayscale.get(); - uint32_t* dest = (uint32_t*)texture->Data.data(); - for (int y = 0; y < h; y++) - { - uint8_t* sline = grayscale + y * w; - uint32_t* dline = dest + y * destwidth; - for (int x = 0; x < w; x += 3) - { - uint32_t values[5] = - { - x > 0 ? sline[x - 1] : 0U, - sline[x], - x + 1 < w ? sline[x + 1] : 0U, - x + 2 < w ? sline[x + 2] : 0U, - x + 3 < w ? sline[x + 3] : 0U - }; - - uint32_t red = (values[0] + values[1] + values[1] + values[2] + 2) >> 2; - uint32_t green = (values[1] + values[2] + values[2] + values[3] + 2) >> 2; - uint32_t blue = (values[2] + values[3] + values[3] + values[4] + 2) >> 2; - uint32_t alpha = (red | green | blue) ? 255 : 0; - - *(dline++) = (alpha << 24) | (red << 16) | (green << 8) | blue; - } - } - - glyph->u = 0.0; - glyph->v = 0.0; - glyph->uvwidth = destwidth; - glyph->uvheight = h; - glyph->texture = std::move(texture); - - glyph->metrics.advanceWidth = (ttfglyph.advanceWidth + 2) / 3; - glyph->metrics.leftSideBearing = (ttfglyph.leftSideBearing + 2) / 3; - glyph->metrics.yOffset = ttfglyph.yOffset; - - return glyph.get(); - } + CanvasGlyph* getGlyph(Canvas* canvas, uint32_t utfchar); +private: std::unique_ptr ttf; std::string fontname; @@ -120,6 +53,8 @@ public: TrueTypeTextMetrics textmetrics; std::unordered_map> glyphs; + + friend class CanvasFontGroup; }; class CanvasFontGroup @@ -130,143 +65,164 @@ public: std::unique_ptr font; std::string language; }; - CanvasFontGroup(const std::string& fontname, double height) : height(height) - { - auto fontdata = LoadWidgetFontData(fontname); - fonts.resize(fontdata.size()); - for (size_t i = 0; i < fonts.size(); i++) - { - fonts[i].font = std::make_unique(fontname, height, fontdata[i].fontdata); - fonts[i].language = fontdata[i].language; - } - } - CanvasGlyph* getGlyph(uint32_t utfchar, const char* lang = nullptr) - { - for (int i = 0; i < 2; i++) - { - for (auto& fd : fonts) - { - if (i == 1 || lang == nullptr || *lang == 0 || fd.language.empty() || fd.language == lang) - { - auto g = fd.font->getGlyph(utfchar); - if (g) return g; - } - } - } + CanvasFontGroup(const std::string& fontname, double height); - return nullptr; - } - - TrueTypeTextMetrics& GetTextMetrics() - { - return fonts[0].font->textmetrics; - } + CanvasGlyph* getGlyph(Canvas* canvas, uint32_t utfchar, const char* lang = nullptr); + TrueTypeTextMetrics& GetTextMetrics(); double height; std::vector fonts; - }; -class BitmapCanvas : public Canvas +//////////////////////////////////////////////////////////////////////////// + +CanvasFont::CanvasFont(const std::string& fontname, double height, std::vector data) : fontname(fontname), height(height) { -public: - BitmapCanvas(DisplayWindow* window); - ~BitmapCanvas(); + auto tdata = std::make_shared(std::move(data)); + ttf = std::make_unique(tdata); + textmetrics = ttf->GetTextMetrics(height); +} - void begin(const Colorf& color) override; - void end() override; +CanvasFont::~CanvasFont() +{ +} - void begin3d() override; - void end3d() override; +CanvasGlyph* CanvasFont::getGlyph(Canvas* canvas, uint32_t utfchar) +{ + uint32_t glyphIndex = ttf->GetGlyphIndex(utfchar); + if (glyphIndex == 0) return nullptr; - Point getOrigin() override; - void setOrigin(const Point& origin) override; + auto& glyph = glyphs[glyphIndex]; + if (glyph) + return glyph.get(); - void pushClip(const Rect& box) override; - void popClip() override; + glyph = std::make_unique(); - void fillRect(const Rect& box, const Colorf& color) override; - void line(const Point& p0, const Point& p1, const Colorf& color) override; + TrueTypeGlyph ttfglyph = ttf->LoadGlyph(glyphIndex, height); - void drawText(const Point& pos, const Colorf& color, const std::string& text) override; - Rect measureText(const std::string& text) override; - VerticalTextPosition verticalTextAlign() override; + // Create final subpixel version + int w = ttfglyph.width; + int h = ttfglyph.height; + int destwidth = (w + 2) / 3; + auto texture = std::make_shared(); + std::vector data(destwidth * h); - void drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color) override { drawText(pos, color, text); } - void drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color) override { drawText(pos, color, text); } - Rect measureText(const std::shared_ptr& font, const std::string& text) override { return measureText(text); } - FontMetrics getFontMetrics(const std::shared_ptr& font) override + uint8_t* grayscale = ttfglyph.grayscale.get(); + uint32_t* dest = data.data(); + for (int y = 0; y < h; y++) { - VerticalTextPosition vtp = verticalTextAlign(); - FontMetrics metrics; - metrics.external_leading = vtp.top; - metrics.ascent = vtp.baseline - vtp.top; - metrics.descent = vtp.bottom - vtp.baseline; - metrics.height = metrics.ascent + metrics.descent; - return metrics; + uint8_t* sline = grayscale + y * w; + uint32_t* dline = dest + y * destwidth; + for (int x = 0; x < w; x += 3) + { + uint32_t values[5] = + { + x > 0 ? sline[x - 1] : 0U, + sline[x], + x + 1 < w ? sline[x + 1] : 0U, + x + 2 < w ? sline[x + 2] : 0U, + x + 3 < w ? sline[x + 3] : 0U + }; + + uint32_t red = (values[0] + values[1] + values[1] + values[2] + 2) >> 2; + uint32_t green = (values[1] + values[2] + values[2] + values[3] + 2) >> 2; + uint32_t blue = (values[2] + values[3] + values[3] + values[4] + 2) >> 2; + uint32_t alpha = (red | green | blue) ? 255 : 0; + + *(dline++) = (alpha << 24) | (red << 16) | (green << 8) | blue; + } } - int getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint) override { return 0; } - void drawImage(const std::shared_ptr& image, const Point& pos) override; + glyph->u = 0.0; + glyph->v = 0.0; + glyph->uvwidth = destwidth; + glyph->uvheight = h; + glyph->texture = canvas->createTexture(destwidth, h, data.data(), ImageFormat::B8G8R8A8); - void drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color); + glyph->metrics.advanceWidth = (ttfglyph.advanceWidth + 2) / 3; + glyph->metrics.leftSideBearing = (ttfglyph.leftSideBearing + 2) / 3; + glyph->metrics.yOffset = ttfglyph.yOffset; - void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color); - void drawGlyph(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color); + return glyph.get(); +} - int getClipMinX() const; - int getClipMinY() const; - int getClipMaxX() const; - int getClipMaxY() const; +//////////////////////////////////////////////////////////////////////////// - void setLanguage(const char* lang) { language = lang; } - - std::unique_ptr createTexture(int width, int height, const void* pixels, ImageFormat format = ImageFormat::B8G8R8A8); - - template - static T clamp(T val, T minval, T maxval) { return std::max(std::min(val, maxval), minval); } - - DisplayWindow* window = nullptr; - - std::unique_ptr font; - std::unique_ptr whiteTexture; - - Point origin; - double uiscale = 1.0f; - std::vector clipStack; - - int width = 0; - int height = 0; - std::vector pixels; - - std::unordered_map, std::unique_ptr> imageTextures; - std::string language; -}; - -BitmapCanvas::BitmapCanvas(DisplayWindow* window) : window(window) +CanvasFontGroup::CanvasFontGroup(const std::string& fontname, double height) : height(height) { + auto fontdata = LoadWidgetFontData(fontname); + fonts.resize(fontdata.size()); + for (size_t i = 0; i < fonts.size(); i++) + { + fonts[i].font = std::make_unique(fontname, height, fontdata[i].fontdata); + fonts[i].language = fontdata[i].language; + } +} + +CanvasGlyph* CanvasFontGroup::getGlyph(Canvas* canvas, uint32_t utfchar, const char* lang) +{ + for (int i = 0; i < 2; i++) + { + for (auto& fd : fonts) + { + if (i == 1 || lang == nullptr || *lang == 0 || fd.language.empty() || fd.language == lang) + { + auto g = fd.font->getGlyph(canvas, utfchar); + if (g) return g; + } + } + } + + return nullptr; +} + +TrueTypeTextMetrics& CanvasFontGroup::GetTextMetrics() +{ + return fonts[0].font->textmetrics; +} + +//////////////////////////////////////////////////////////////////////////// + +Canvas::Canvas() +{ +} + +Canvas::~Canvas() +{ +} + +void Canvas::attach(DisplayWindow* newWindow) +{ + window = newWindow; uiscale = window->GetDpiScale(); uint32_t white = 0xffffffff; whiteTexture = createTexture(1, 1, &white); font = std::make_unique("NotoSans", 13.0 * uiscale); } -BitmapCanvas::~BitmapCanvas() +void Canvas::detach() { } -Point BitmapCanvas::getOrigin() +void Canvas::begin(const Colorf& color) +{ + uiscale = window->GetDpiScale(); + width = window->GetPixelWidth(); + height = window->GetPixelHeight(); +} + +Point Canvas::getOrigin() { return origin; } -void BitmapCanvas::setOrigin(const Point& newOrigin) +void Canvas::setOrigin(const Point& newOrigin) { origin = newOrigin; } -void BitmapCanvas::pushClip(const Rect& box) +void Canvas::pushClip(const Rect& box) { if (!clipStack.empty()) { @@ -293,17 +249,17 @@ void BitmapCanvas::pushClip(const Rect& box) } } -void BitmapCanvas::popClip() +void Canvas::popClip() { clipStack.pop_back(); } -void BitmapCanvas::fillRect(const Rect& box, const Colorf& color) +void Canvas::fillRect(const Rect& box, const Colorf& color) { - drawTile(whiteTexture.get(), (float)((origin.x + box.x) * uiscale), (float)((origin.y + box.y) * uiscale), (float)(box.width * uiscale), (float)(box.height * uiscale), 0.0, 0.0, 1.0, 1.0, color); + fillTile((float)((origin.x + box.x) * uiscale), (float)((origin.y + box.y) * uiscale), (float)(box.width * uiscale), (float)(box.height * uiscale), color); } -void BitmapCanvas::drawImage(const std::shared_ptr& image, const Point& pos) +void Canvas::drawImage(const std::shared_ptr& image, const Point& pos) { auto& texture = imageTextures[image]; if (!texture) @@ -314,7 +270,18 @@ void BitmapCanvas::drawImage(const std::shared_ptr& image, const Point& p drawTile(texture.get(), (float)((origin.x + pos.x) * uiscale), (float)((origin.y + pos.y) * uiscale), (float)(texture->Width * uiscale), (float)(texture->Height * uiscale), 0.0, 0.0, (float)texture->Width, (float)texture->Height, color); } -void BitmapCanvas::line(const Point& p0, const Point& p1, const Colorf& color) +void Canvas::drawImage(const std::shared_ptr& image, const Rect& box) +{ + auto& texture = imageTextures[image]; + if (!texture) + { + texture = createTexture(image->GetWidth(), image->GetHeight(), image->GetData(), image->GetFormat()); + } + Colorf color(1.0f, 1.0f, 1.0f); + drawTile(texture.get(), (float)((origin.x + box.x) * uiscale), (float)((origin.y + box.y) * uiscale), (float)(box.width * uiscale), (float)(box.height * uiscale), 0.0, 0.0, (float)texture->Width, (float)texture->Height, color); +} + +void Canvas::line(const Point& p0, const Point& p1, const Colorf& color) { double x0 = origin.x + p0.x; double y0 = origin.y + p0.y; @@ -401,7 +368,7 @@ void BitmapCanvas::line(const Point& p0, const Point& p1, const Colorf& color) } } -void BitmapCanvas::drawText(const Point& pos, const Colorf& color, const std::string& text) +void Canvas::drawText(const Point& pos, const Colorf& color, const std::string& text) { double x = std::round((origin.x + pos.x) * uiscale); double y = std::round((origin.y + pos.y) * uiscale); @@ -409,10 +376,10 @@ void BitmapCanvas::drawText(const Point& pos, const Colorf& color, const std::st UTF8Reader reader(text.data(), text.size()); while (!reader.is_end()) { - CanvasGlyph* glyph = font->getGlyph(reader.character(), language.c_str()); + CanvasGlyph* glyph = font->getGlyph(this, reader.character(), language.c_str()); if (!glyph || !glyph->texture) { - glyph = font->getGlyph(32); + glyph = font->getGlyph(this, 32); } if (glyph->texture) @@ -427,7 +394,22 @@ void BitmapCanvas::drawText(const Point& pos, const Colorf& color, const std::st } } -Rect BitmapCanvas::measureText(const std::string& text) +void Canvas::drawText(const std::shared_ptr& font, const Point& pos, const std::string& text, const Colorf& color) +{ + drawText(pos, color, text); +} + +void Canvas::drawTextEllipsis(const std::shared_ptr& font, const Point& pos, const Rect& clipBox, const std::string& text, const Colorf& color) +{ + drawText(pos, color, text); +} + +Rect Canvas::measureText(const std::shared_ptr& font, const std::string& text) +{ + return measureText(text); +} + +Rect Canvas::measureText(const std::string& text) { double x = 0.0; double y = font->GetTextMetrics().ascender - font->GetTextMetrics().descender; @@ -435,10 +417,10 @@ Rect BitmapCanvas::measureText(const std::string& text) UTF8Reader reader(text.data(), text.size()); while (!reader.is_end()) { - CanvasGlyph* glyph = font->getGlyph(reader.character(), language.c_str()); + CanvasGlyph* glyph = font->getGlyph(this, reader.character(), language.c_str()); if (!glyph || !glyph->texture) { - glyph = font->getGlyph(32); + glyph = font->getGlyph(this, 32); } x += std::round(glyph->metrics.advanceWidth); @@ -448,7 +430,23 @@ Rect BitmapCanvas::measureText(const std::string& text) return Rect::xywh(0.0, 0.0, x / uiscale, y / uiscale); } -VerticalTextPosition BitmapCanvas::verticalTextAlign() +FontMetrics Canvas::getFontMetrics(const std::shared_ptr& font) +{ + VerticalTextPosition vtp = verticalTextAlign(); + FontMetrics metrics; + metrics.external_leading = vtp.top; + metrics.ascent = vtp.baseline - vtp.top; + metrics.descent = vtp.bottom - vtp.baseline; + metrics.height = metrics.ascent + metrics.descent; + return metrics; +} + +int Canvas::getCharacterIndex(const std::shared_ptr& font, const std::string& text, const Point& hitPoint) +{ + return 0; +} + +VerticalTextPosition Canvas::verticalTextAlign() { VerticalTextPosition align; align.top = 0.0f; @@ -458,9 +456,70 @@ VerticalTextPosition BitmapCanvas::verticalTextAlign() return align; } +void Canvas::drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color) +{ + if (p0.x == p1.x) + { + fillTile((float)((p0.x - 0.5) * uiscale), (float)(p0.y * uiscale), (float)uiscale, (float)((p1.y - p0.y) * uiscale), color); + } + else if (p0.y == p1.y) + { + fillTile((float)(p0.x * uiscale), (float)((p0.y - 0.5) * uiscale), (float)((p1.x - p0.x) * uiscale), (float)uiscale, color); + } + else + { + drawLineAntialiased((float)(p0.x * uiscale), (float)(p0.y * uiscale), (float)(p1.x * uiscale), (float)(p1.y * uiscale), color); + } +} + +int Canvas::getClipMinX() const +{ + return clipStack.empty() ? 0 : (int)std::round(std::max(clipStack.back().x * uiscale, 0.0)); +} + +int Canvas::getClipMinY() const +{ + return clipStack.empty() ? 0 : (int)std::round(std::max(clipStack.back().y * uiscale, 0.0)); +} + +int Canvas::getClipMaxX() const +{ + return clipStack.empty() ? width : (int)std::round(std::min((clipStack.back().x + clipStack.back().width) * uiscale, (double)width)); +} + +int Canvas::getClipMaxY() const +{ + return clipStack.empty() ? height : (int)std::round(std::min((clipStack.back().y + clipStack.back().height) * uiscale, (double)height)); +} + +///////////////////////////////////////////////////////////////////////////// + +class BitmapTexture : public CanvasTexture +{ +public: + std::vector Data; +}; + +class BitmapCanvas : public Canvas +{ +public: + void begin(const Colorf& color) override; + void end() override; + + void fillTile(float x, float y, float width, float height, Colorf color) override; + void drawTile(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) override; + void drawGlyph(CanvasTexture* texture, float x, float y, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) override; + void drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color) override; + void plot(float x, float y, float alpha, const Colorf& color); + + std::unique_ptr createTexture(int width, int height, const void* pixels, ImageFormat format = ImageFormat::B8G8R8A8) override; + + std::vector pixels; +}; + std::unique_ptr BitmapCanvas::createTexture(int width, int height, const void* pixels, ImageFormat format) { - auto texture = std::make_unique(); + auto texture = std::make_unique(); texture->Width = width; texture->Height = height; texture->Data.resize(width * height); @@ -485,47 +544,232 @@ std::unique_ptr BitmapCanvas::createTexture(int width, int height return texture; } -void BitmapCanvas::drawLineUnclipped(const Point& p0, const Point& p1, const Colorf& color) +void BitmapCanvas::plot(float x, float y, float alpha, const Colorf& color) { - if (p0.x == p1.x) + int xx = (int)x; + int yy = (int)y; + + int dwidth = width; + int dheight = height; + uint32_t* dest = pixels.data() + xx + yy * dwidth; + + uint32_t cred = (int32_t)clamp(color.r * 256.0f, 0.0f, 256.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 256.0f, 0.0f, 256.0f); + uint32_t cblue = (int32_t)clamp(color.b * 256.0f, 0.0f, 256.0f); + uint32_t calpha = (int32_t)clamp(color.a * alpha * 256.0f, 0.0f, 256.0f); + uint32_t invalpha = 256 - calpha; + + uint32_t dpixel = *dest; + uint32_t dalpha = dpixel >> 24; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // dest.rgba = color.rgba + dest.rgba * (1-color.a) + uint32_t a = (calpha * calpha + dalpha * invalpha + 127) >> 8; + uint32_t r = (cred * calpha + dred * invalpha + 127) >> 8; + uint32_t g = (cgreen * calpha + dgreen * invalpha + 127) >> 8; + uint32_t b = (cblue * calpha + dblue * invalpha + 127) >> 8; + *dest = (a << 24) | (r << 16) | (g << 8) | b; +} + +static float fpart(float x) +{ + return x - std::floor(x); +} + +static float rfpart(float x) +{ + return 1 - fpart(x); +} + +void BitmapCanvas::drawLineAntialiased(float x0, float y0, float x1, float y1, Colorf color) +{ + bool steep = std::abs(y1 - y0) > std::abs(x1 - x0); + + if (steep) { - drawTile(whiteTexture.get(), (float)((p0.x - 0.5) * uiscale), (float)(p0.y * uiscale), (float)((p1.x + 0.5) * uiscale), (float)(p1.y * uiscale), 0.0f, 0.0f, 1.0f, 1.0f, color); + std::swap(x0, y0); + std::swap(x1, y1); } - else if (p0.y == p1.y) + + if (x0 > x1) { - drawTile(whiteTexture.get(), (float)(p0.x * uiscale), (float)((p0.y - 0.5) * uiscale), (float)(p1.x * uiscale), (float)((p1.y + 0.5) * uiscale), 0.0f, 0.0f, 1.0f, 1.0f, color); + std::swap(x0, x1); + std::swap(y0, y1); + } + + float dx = x1 - x0; + float dy = y1 - y0; + float gradient = (dx == 0.0f) ? 1.0f : dy / dx; + + // handle first endpoint + float xend = std::round(x0); + float yend = y0 + gradient * (xend - x0); + float xgap = rfpart(x0 + 0.5f); + float xpxl1 = xend; // this will be used in the main loop + float ypxl1 = std::floor(yend); + if (steep) + { + plot(ypxl1, xpxl1, rfpart(yend) * xgap, color); + plot(ypxl1 + 1, xpxl1, fpart(yend) * xgap, color); } else { - // To do: draw line using bresenham + plot(xpxl1, ypxl1, rfpart(yend) * xgap, color); + plot(xpxl1, ypxl1 + 1, fpart(yend) * xgap, color); + } + float intery = yend + gradient; // first y-intersection for the main loop + + // handle second endpoint + xend = std::floor(x1 + 0.5f); + yend = y1 + gradient * (xend - x1); + xgap = fpart(x1 + 0.5f); + float xpxl2 = xend; // this will be used in the main loop + float ypxl2 = std::floor(yend); + if (steep) + { + plot(ypxl2, xpxl2, rfpart(yend) * xgap, color); + plot(ypxl2 + 1.0f, xpxl2, fpart(yend) * xgap, color); + } + else + { + plot(xpxl2, ypxl2, rfpart(yend) * xgap, color); + plot(xpxl2, ypxl2 + 1.0f, fpart(yend) * xgap, color); + } + + // main loop + if (steep) + { + for (float x = xpxl1 + 1.0f; x <= xpxl2 - 1.0f; x++) + { + plot(std::floor(intery), x, rfpart(intery), color); + plot(std::floor(intery) + 1.0f, x, fpart(intery), color); + intery = intery + gradient; + } + } + else + { + for (float x = xpxl1 + 1.0f; x <= xpxl2 - 1.0f; x++) + { + plot(x, std::floor(intery), rfpart(intery), color); + plot(x, std::floor(intery) + 1, fpart(intery), color); + intery = intery + gradient; + } } } -int BitmapCanvas::getClipMinX() const +void BitmapCanvas::fillTile(float left, float top, float width, float height, Colorf color) { - return clipStack.empty() ? 0 : (int)std::max(clipStack.back().x * uiscale, 0.0); -} - -int BitmapCanvas::getClipMinY() const -{ - return clipStack.empty() ? 0 : (int)std::max(clipStack.back().y * uiscale, 0.0); -} - -int BitmapCanvas::getClipMaxX() const -{ - return clipStack.empty() ? width : (int)std::min((clipStack.back().x + clipStack.back().width) * uiscale, (double)width); -} - -int BitmapCanvas::getClipMaxY() const -{ - return clipStack.empty() ? height : (int)std::min((clipStack.back().y + clipStack.back().height) * uiscale, (double)height); -} - -void BitmapCanvas::drawTile(CanvasTexture* texture, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) -{ - if (width <= 0.0f || height <= 0.0f) + if (width <= 0.0f || height <= 0.0f || color.a <= 0.0f) return; + int dwidth = this->width; + int dheight = this->height; + uint32_t* dest = this->pixels.data(); + + int x0 = (int)left; + int x1 = (int)(left + width); + int y0 = (int)top; + int y1 = (int)(top + height); + + x0 = std::max(x0, getClipMinX()); + y0 = std::max(y0, getClipMinY()); + x1 = std::min(x1, getClipMaxX()); + y1 = std::min(y1, getClipMaxY()); + if (x1 <= x0 || y1 <= y0) + return; + + uint32_t cred = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); + uint32_t cblue = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); + uint32_t calpha = (int32_t)clamp(color.a * 255.0f, 0.0f, 255.0f); + uint32_t invalpha = 256 - (calpha + (calpha >> 7)); + + if (invalpha == 0) // Solid fill + { + uint32_t c = (calpha << 24) | (cred << 16) | (cgreen << 8) | cblue; +#ifdef USE_SSE2 + __m128i cargb = _mm_set1_epi32(c); +#endif + + for (int y = y0; y < y1; y++) + { + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 2) << 2); + while (x < ssex1) + { + _mm_storeu_si128((__m128i*)(dline + x), cargb); + x += 4; + } +#endif + + while (x < x1) + { + dline[x] = c; + x++; + } + } + } + else // Alpha blended fill + { + cred <<= 8; + cgreen <<= 8; + cblue <<= 8; + calpha <<= 8; +#ifdef USE_SSE2 + __m128i cargb = _mm_set_epi16(calpha, cred, cgreen, cblue, calpha, cred, cgreen, cblue); + __m128i cinvalpha = _mm_set1_epi16(invalpha); +#endif + + for (int y = y0; y < y1; y++) + { + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 1) << 1); + while (x < ssex1) + { + __m128i dpixel = _mm_loadl_epi64((const __m128i*)(dline + x)); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + + // dest.rgba = color.rgba + dest.rgba * (1-color.a) + __m128i result = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(cargb, _mm_mullo_epi16(dpixel, cinvalpha)), _mm_set1_epi16(127)), 8); + _mm_storel_epi64((__m128i*)(dline + x), _mm_packus_epi16(result, _mm_setzero_si128())); + x += 2; + } +#endif + + while (x < x1) + { + uint32_t dpixel = dline[x]; + uint32_t dalpha = dpixel >> 24; + uint32_t dred = (dpixel >> 16) & 0xff; + uint32_t dgreen = (dpixel >> 8) & 0xff; + uint32_t dblue = dpixel & 0xff; + + // dest.rgba = color.rgba + dest.rgba * (1-color.a) + uint32_t a = (calpha + dalpha * invalpha + 127) >> 8; + uint32_t r = (cred + dred * invalpha + 127) >> 8; + uint32_t g = (cgreen + dgreen * invalpha + 127) >> 8; + uint32_t b = (cblue + dblue * invalpha + 127) >> 8; + dline[x] = (a << 24) | (r << 16) | (g << 8) | b; + x++; + } + } + } +} + +void BitmapCanvas::drawTile(CanvasTexture* tex, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) +{ + if (width <= 0.0f || height <= 0.0f || color.a <= 0.0f) + return; + + auto texture = static_cast(tex); int swidth = texture->Width; int sheight = texture->Height; const uint32_t* src = texture->Data.data(); @@ -550,6 +794,9 @@ void BitmapCanvas::drawTile(CanvasTexture* texture, float left, float top, float uint32_t cgreen = (int32_t)clamp(color.g * 256.0f, 0.0f, 256.0f); uint32_t cblue = (int32_t)clamp(color.b * 256.0f, 0.0f, 256.0f); uint32_t calpha = (int32_t)clamp(color.a * 256.0f, 0.0f, 256.0f); +#ifdef USE_SSE2 + __m128i cargb = _mm_set_epi16(calpha, cred, cgreen, cblue, calpha, cred, cgreen, cblue); +#endif float uscale = uvwidth / width; float vscale = uvheight / height; @@ -558,9 +805,39 @@ void BitmapCanvas::drawTile(CanvasTexture* texture, float left, float top, float { float vpix = v + vscale * (y + 0.5f - top); const uint32_t* sline = src + ((int)vpix) * swidth; - uint32_t* dline = dest + y * dwidth; - for (int x = x0; x < x1; x++) + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 1) << 1); + while (x < ssex1) + { + float upix0 = u + uscale * (x + 0.5f - left); + float upix1 = u + uscale * (x + 1 + 0.5f - left); + uint32_t spixel0 = sline[(int)upix0]; + uint32_t spixel1 = sline[(int)upix1]; + __m128i spixel = _mm_set_epi32(0, 0, spixel1, spixel0); + spixel = _mm_unpacklo_epi8(spixel, _mm_setzero_si128()); + + __m128i dpixel = _mm_loadl_epi64((const __m128i*)(dline + x)); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + + // Pixel shade + spixel = _mm_srli_epi16(_mm_add_epi16(_mm_mullo_epi16(spixel, cargb), _mm_set1_epi16(127)), 8); + + // Rescale from [0,255] to [0,256] + __m128i sa = _mm_shufflehi_epi16(_mm_shufflelo_epi16(spixel, _MM_SHUFFLE(3, 3, 3, 3)), _MM_SHUFFLE(3, 3, 3, 3)); + sa = _mm_add_epi16(sa, _mm_srli_epi16(sa, 7)); + __m128i sinva = _mm_sub_epi16(_mm_set1_epi16(256), sa); + + // dest.rgba = color.rgba * src.rgba * src.a + dest.rgba * (1-src.a) + __m128i result = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(_mm_mullo_epi16(spixel, sa), _mm_mullo_epi16(dpixel, sinva)), _mm_set1_epi16(127)), 8); + _mm_storel_epi64((__m128i*)(dline + x), _mm_packus_epi16(result, _mm_setzero_si128())); + x += 2; + } +#endif + + while (x < x1) { float upix = u + uscale * (x + 0.5f - left); uint32_t spixel = sline[(int)upix]; @@ -591,15 +868,17 @@ void BitmapCanvas::drawTile(CanvasTexture* texture, float left, float top, float uint32_t g = (sgreen * sa + dgreen * sinva + 127) >> 8; uint32_t b = (sblue * sa + dblue * sinva + 127) >> 8; dline[x] = (a << 24) | (r << 16) | (g << 8) | b; + x++; } } } -void BitmapCanvas::drawGlyph(CanvasTexture* texture, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) +void BitmapCanvas::drawGlyph(CanvasTexture* tex, float left, float top, float width, float height, float u, float v, float uvwidth, float uvheight, Colorf color) { if (width <= 0.0f || height <= 0.0f) return; + auto texture = static_cast(tex); int swidth = texture->Width; int sheight = texture->Height; const uint32_t* src = texture->Data.data(); @@ -620,9 +899,15 @@ void BitmapCanvas::drawGlyph(CanvasTexture* texture, float left, float top, floa if (x1 <= x0 || y1 <= y0) return; - uint32_t cred = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); - uint32_t cgreen = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); - uint32_t cblue = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); +#if 1 // Use gamma correction + + // To linear + float cred = color.r * color.r; // std::pow(color.r, 2.2f); + float cgreen = color.g * color.g; // std::pow(color.g, 2.2f); + float cblue = color.b * color.b; // std::pow(color.b, 2.2f); +#ifdef USE_SSE2 + __m128 crgba = _mm_set_ps(0.0f, cred, cgreen, cblue); +#endif float uscale = uvwidth / width; float vscale = uvheight / height; @@ -631,9 +916,120 @@ void BitmapCanvas::drawGlyph(CanvasTexture* texture, float left, float top, floa { float vpix = v + vscale * (y + 0.5f - top); const uint32_t* sline = src + ((int)vpix) * swidth; - uint32_t* dline = dest + y * dwidth; - for (int x = x0; x < x1; x++) + + int x = x0; +#ifdef USE_SSE2 + while (x < x1) + { + float upix = u + uscale * (x + 0.5f - left); + __m128i spixel = _mm_cvtsi32_si128(sline[(int)upix]); + spixel = _mm_unpacklo_epi8(spixel, _mm_setzero_si128()); + spixel = _mm_unpacklo_epi16(spixel, _mm_setzero_si128()); + __m128 srgba = _mm_mul_ps(_mm_cvtepi32_ps(spixel), _mm_set_ps1(1.0f / 255.0f)); + + __m128i dpixel = _mm_cvtsi32_si128(dline[x]); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + dpixel = _mm_unpacklo_epi16(dpixel, _mm_setzero_si128()); + __m128 drgba = _mm_mul_ps(_mm_cvtepi32_ps(dpixel), _mm_set_ps1(1.0f / 255.0f)); + + // To linear + drgba = _mm_mul_ps(drgba, drgba); + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + __m128 frgba = _mm_add_ps(_mm_mul_ps(crgba, srgba), _mm_mul_ps(drgba, _mm_sub_ps(_mm_set_ps1(1.0f), srgba))); + + // To srgb + frgba = _mm_sqrt_ps(frgba); + + __m128i rgba = _mm_cvtps_epi32(_mm_add_ps(_mm_mul_ps(frgba, _mm_set_ps1(255.0f)), _mm_set_ps1(0.5f))); + rgba = _mm_packs_epi32(rgba, _mm_setzero_si128()); + rgba = _mm_packus_epi16(rgba, _mm_setzero_si128()); + dline[x] = ((uint32_t)_mm_cvtsi128_si32(rgba)) | 0xff000000; + x++; + } +#else + while (x < x1) + { + float upix = u + uscale * (x + 0.5f - left); + uint32_t spixel = sline[(int)upix]; + float sred = ((spixel >> 16) & 0xff) * (1.0f / 255.0f); + float sgreen = ((spixel >> 8) & 0xff) * (1.0f / 255.0f); + float sblue = (spixel & 0xff) * (1.0f / 255.0f); + + uint32_t dpixel = dline[x]; + float dred = ((dpixel >> 16) & 0xff) * (1.0f / 255.0f); + float dgreen = ((dpixel >> 8) & 0xff) * (1.0f / 255.0f); + float dblue = (dpixel & 0xff) * (1.0f / 255.0f); + + // To linear + dred = dred * dred; // std::pow(dred, 2.2f); + dgreen = dgreen * dgreen; // std::pow(dgreen, 2.2f); + dblue = dblue * dblue; // std::pow(dblue, 2.2f); + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + double fr = cred * sred + dred * (1.0f - sred); + double fg = cgreen * sgreen + dgreen * (1.0f - sgreen); + double fb = cblue * sblue + dblue * (1.0f - sblue); + + // To srgb + fr = std::sqrt(fr); // std::pow(fr, 1.0f / 2.2f); + fg = std::sqrt(fg); // std::pow(fg, 1.0f / 2.2f); + fb = std::sqrt(fb); // std::pow(fb, 1.0f / 2.2f); + + uint32_t r = (int)(fr * 255.0f + 0.5f); + uint32_t g = (int)(fg * 255.0f + 0.5f); + uint32_t b = (int)(fb * 255.0f + 0.5f); + dline[x] = 0xff000000 | (r << 16) | (g << 8) | b; + x++; + } +#endif + } + +#else + + uint32_t cred = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); + uint32_t cgreen = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); + uint32_t cblue = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); +#ifdef USE_SSE2 + __m128i crgba = _mm_set_epi16(0, cred, cgreen, cblue, 0, cred, cgreen, cblue); +#endif + + float uscale = uvwidth / width; + float vscale = uvheight / height; + + for (int y = y0; y < y1; y++) + { + float vpix = v + vscale * (y + 0.5f - top); + const uint32_t* sline = src + ((int)vpix) * swidth; + uint32_t* dline = dest + y * dwidth; + + int x = x0; +#ifdef USE_SSE2 + int ssex1 = x0 + (((x1 - x0) >> 1) << 1); + while (x < ssex1) + { + float upix0 = u + uscale * (x + 0.5f - left); + float upix1 = u + uscale * (x + 1 + 0.5f - left); + uint32_t spixel0 = sline[(int)upix0]; + uint32_t spixel1 = sline[(int)upix1]; + __m128i spixel = _mm_set_epi32(0, 0, spixel1, spixel0); + spixel = _mm_unpacklo_epi8(spixel, _mm_setzero_si128()); + + __m128i dpixel = _mm_loadl_epi64((const __m128i*)(dline + x)); + dpixel = _mm_unpacklo_epi8(dpixel, _mm_setzero_si128()); + + // Rescale from [0,255] to [0,256] + spixel = _mm_add_epi16(spixel, _mm_srli_epi16(spixel, 7)); + + // dest.rgb = color.rgb * src.rgb + dest.rgb * (1-src.rgb) + __m128i result = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(_mm_mullo_epi16(crgba, spixel), _mm_mullo_epi16(dpixel, _mm_sub_epi16(_mm_set1_epi16(256), spixel))), _mm_set1_epi16(127)), 8); + _mm_storel_epi64((__m128i*)(dline + x), _mm_or_si128(_mm_packus_epi16(result, _mm_setzero_si128()), _mm_set1_epi32(0xff000000))); + x += 2; + } +#endif + + while (x < x1) { float upix = u + uscale * (x + 0.5f - left); uint32_t spixel = sline[(int)upix]; @@ -656,21 +1052,21 @@ void BitmapCanvas::drawGlyph(CanvasTexture* texture, float left, float top, floa uint32_t g = (cgreen * sgreen + dgreen * (256 - sgreen) + 127) >> 8; uint32_t b = (cblue * sblue + dblue * (256 - sblue) + 127) >> 8; dline[x] = 0xff000000 | (r << 16) | (g << 8) | b; + x++; } } +#endif } void BitmapCanvas::begin(const Colorf& color) { - uiscale = window->GetDpiScale(); + Canvas::begin(color); uint32_t r = (int32_t)clamp(color.r * 255.0f, 0.0f, 255.0f); uint32_t g = (int32_t)clamp(color.g * 255.0f, 0.0f, 255.0f); uint32_t b = (int32_t)clamp(color.b * 255.0f, 0.0f, 255.0f); uint32_t a = (int32_t)clamp(color.a * 255.0f, 0.0f, 255.0f); uint32_t bgcolor = (a << 24) | (r << 16) | (g << 8) | b; - width = window->GetPixelWidth(); - height = window->GetPixelHeight(); pixels.clear(); pixels.resize(width * height, bgcolor); } @@ -680,17 +1076,9 @@ void BitmapCanvas::end() window->PresentBitmap(width, height, pixels.data()); } -void BitmapCanvas::begin3d() -{ -} - -void BitmapCanvas::end3d() -{ -} - ///////////////////////////////////////////////////////////////////////////// -std::unique_ptr Canvas::create(DisplayWindow* window) +std::unique_ptr Canvas::create() { - return std::make_unique(window); + return std::make_unique(); } diff --git a/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h b/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h index 89fae3834..a3295098c 100644 --- a/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h +++ b/libraries/ZWidget/src/core/nanosvg/nanosvgrast.h @@ -331,7 +331,7 @@ static float nsvg__normalize(float *x, float* y) } static float nsvg__absf(float x) { return x < 0 ? -x : x; } -static float nsvg__roundf(float x) { return (x >= 0) ? floorf(x + 0.5) : ceilf(x - 0.5); } +static float nsvg__roundf(float x) { return (x >= 0) ? (float)floorf(x + 0.5f) : (float)ceilf(x - 0.5f); } static void nsvg__flattenCubicBez(NSVGrasterizer* r, float x1, float y1, float x2, float y2, diff --git a/libraries/ZWidget/src/core/pathfill.cpp b/libraries/ZWidget/src/core/pathfill.cpp index bac3dfb50..cb604ef3f 100644 --- a/libraries/ZWidget/src/core/pathfill.cpp +++ b/libraries/ZWidget/src/core/pathfill.cpp @@ -146,7 +146,7 @@ void PathFillRasterizer::Rasterize(const PathFillDesc& path, uint8_t* dest, int height = block_height; scanlines.resize(block_height); - first_scanline = scanlines.size(); + first_scanline = (int)scanlines.size(); last_scanline = 0; } @@ -264,7 +264,7 @@ void PathFillRasterizer::Clear() } } - first_scanline = scanlines.size(); + first_scanline = (int)scanlines.size(); last_scanline = 0; } diff --git a/libraries/ZWidget/src/core/theme.cpp b/libraries/ZWidget/src/core/theme.cpp new file mode 100644 index 000000000..415224429 --- /dev/null +++ b/libraries/ZWidget/src/core/theme.cpp @@ -0,0 +1,383 @@ + +#include "core/theme.h" +#include "core/widget.h" +#include "core/canvas.h" + +void WidgetStyle::SetBool(const std::string& state, const std::string& propertyName, bool value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetInt(const std::string& state, const std::string& propertyName, int value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetDouble(const std::string& state, const std::string& propertyName, double value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetString(const std::string& state, const std::string& propertyName, const std::string& value) +{ + StyleProperties[state][propertyName] = value; +} + +void WidgetStyle::SetColor(const std::string& state, const std::string& propertyName, const Colorf& value) +{ + StyleProperties[state][propertyName] = value; +} + +const WidgetStyle::PropertyVariant* WidgetStyle::FindProperty(const std::string& state, const std::string& propertyName) const +{ + const WidgetStyle* style = this; + do + { + // Look for property in the specific state + auto stateIt = style->StyleProperties.find(state); + if (stateIt != style->StyleProperties.end()) + { + auto it = stateIt->second.find(propertyName); + if (it != stateIt->second.end()) + return &it->second; + } + + // Fall back to the widget main style + if (state != std::string()) + { + stateIt = style->StyleProperties.find(std::string()); + if (stateIt != style->StyleProperties.end()) + { + auto it = stateIt->second.find(propertyName); + if (it != stateIt->second.end()) + return &it->second; + } + } + + style = style->ParentStyle; + } while (style); + return nullptr; +} + +bool WidgetStyle::GetBool(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : false; +} + +int WidgetStyle::GetInt(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : 0; +} + +double WidgetStyle::GetDouble(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : 0.0; +} + +std::string WidgetStyle::GetString(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : std::string(); +} + +Colorf WidgetStyle::GetColor(const std::string& state, const std::string& propertyName) const +{ + const PropertyVariant* prop = FindProperty(state, propertyName); + return prop ? std::get(*prop) : Colorf::transparent(); +} + +///////////////////////////////////////////////////////////////////////////// + +void BasicWidgetStyle::Paint(Widget* widget, Canvas* canvas, Size size) +{ + Colorf bgcolor = widget->GetStyleColor("background-color"); + if (bgcolor.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, 0.0, size.width, size.height), bgcolor); + + Colorf borderleft = widget->GetStyleColor("border-left-color"); + Colorf bordertop = widget->GetStyleColor("border-top-color"); + Colorf borderright = widget->GetStyleColor("border-right-color"); + Colorf borderbottom = widget->GetStyleColor("border-bottom-color"); + + double borderwidth = widget->GridFitSize(1.0); + + if (bordertop.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, 0.0, size.width, borderwidth), bordertop); + if (borderbottom.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, size.height - borderwidth, size.width, borderwidth), borderbottom); + if (borderleft.a > 0.0f) + canvas->fillRect(Rect::xywh(0.0, 0.0, borderwidth, size.height), borderleft); + if (borderright.a > 0.0f) + canvas->fillRect(Rect::xywh(size.width - borderwidth, 0.0, borderwidth, size.height), borderright); +} + +///////////////////////////////////////////////////////////////////////////// + +static std::unique_ptr CurrentTheme; + +WidgetStyle* WidgetTheme::RegisterStyle(std::unique_ptr widgetStyle, const std::string& widgetClass) +{ + auto& style = Styles[widgetClass]; + style = std::move(widgetStyle); + return style.get(); +} + +WidgetStyle* WidgetTheme::GetStyle(const std::string& widgetClass) +{ + auto it = Styles.find(widgetClass); + return it != Styles.end() ? it->second.get() : nullptr; +} + +void WidgetTheme::SetTheme(std::unique_ptr theme) +{ + CurrentTheme = std::move(theme); +} + +WidgetTheme* WidgetTheme::GetTheme() +{ + return CurrentTheme.get(); +} + +///////////////////////////////////////////////////////////////////////////// + +DarkWidgetTheme::DarkWidgetTheme() +{ + auto widget = RegisterStyle(std::make_unique(), "widget"); + auto textlabel = RegisterStyle(std::make_unique(widget), "textlabel"); + auto pushbutton = RegisterStyle(std::make_unique(widget), "pushbutton"); + auto lineedit = RegisterStyle(std::make_unique(widget), "lineedit"); + auto textedit = RegisterStyle(std::make_unique(widget), "textedit"); + auto listview = RegisterStyle(std::make_unique(widget), "listview"); + auto scrollbar = RegisterStyle(std::make_unique(widget), "scrollbar"); + auto tabbar = RegisterStyle(std::make_unique(widget), "tabbar"); + auto tabbar_tab = RegisterStyle(std::make_unique(widget), "tabbar-tab"); + auto tabwidget_stack = RegisterStyle(std::make_unique(widget), "tabwidget-stack"); + auto checkbox_label = RegisterStyle(std::make_unique(widget), "checkbox-label"); + auto menubar = RegisterStyle(std::make_unique(widget), "menubar"); + auto menubaritem = RegisterStyle(std::make_unique(widget), "menubaritem"); + auto menu = RegisterStyle(std::make_unique(widget), "menu"); + auto menuitem = RegisterStyle(std::make_unique(widget), "menuitem"); + auto toolbar = RegisterStyle(std::make_unique(widget), "toolbar"); + auto toolbarbutton = RegisterStyle(std::make_unique(widget), "toolbarbutton"); + auto statusbar = RegisterStyle(std::make_unique(widget), "statusbar"); + + widget->SetString("font-family", "NotoSans"); + widget->SetColor("color", Colorf::fromRgba8(226, 223, 219)); + widget->SetColor("window-background", Colorf::fromRgba8(51, 51, 51)); + widget->SetColor("window-border", Colorf::fromRgba8(51, 51, 51)); + widget->SetColor("window-caption-color", Colorf::fromRgba8(33, 33, 33)); + widget->SetColor("window-caption-text-color", Colorf::fromRgba8(226, 223, 219)); + + pushbutton->SetDouble("noncontent-left", 10.0); + pushbutton->SetDouble("noncontent-top", 5.0); + pushbutton->SetDouble("noncontent-right", 10.0); + pushbutton->SetDouble("noncontent-bottom", 5.0); + pushbutton->SetColor("background-color", Colorf::fromRgba8(68, 68, 68)); + pushbutton->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + pushbutton->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + pushbutton->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); + + lineedit->SetDouble("noncontent-left", 5.0); + lineedit->SetDouble("noncontent-top", 3.0); + lineedit->SetDouble("noncontent-right", 5.0); + lineedit->SetDouble("noncontent-bottom", 3.0); + lineedit->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + lineedit->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("selection-color", Colorf::fromRgba8(100, 100, 100)); + lineedit->SetColor("no-focus-selection-color", Colorf::fromRgba8(68, 68, 68)); + + textedit->SetDouble("noncontent-left", 8.0); + textedit->SetDouble("noncontent-top", 8.0); + textedit->SetDouble("noncontent-right", 8.0); + textedit->SetDouble("noncontent-bottom", 8.0); + textedit->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + textedit->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + textedit->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + textedit->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + textedit->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + + listview->SetDouble("noncontent-left", 10.0); + listview->SetDouble("noncontent-top", 10.0); + listview->SetDouble("noncontent-right", 3.0); + listview->SetDouble("noncontent-bottom", 10.0); + listview->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + listview->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + listview->SetColor("selection-color", Colorf::fromRgba8(100, 100, 100)); + + scrollbar->SetColor("track-color", Colorf::fromRgba8(33, 33, 33)); + scrollbar->SetColor("thumb-color", Colorf::fromRgba8(58, 58, 58)); + + tabbar->SetDouble("noncontent-left", 20.0); + tabbar->SetDouble("noncontent-right", 20.0); + tabbar->SetColor("background-color", Colorf::fromRgba8(38, 38, 38)); + + tabbar_tab->SetDouble("noncontent-left", 15.0); + tabbar_tab->SetDouble("noncontent-right", 15.0); + tabbar_tab->SetColor("hover", "background-color", Colorf::fromRgba8(45, 45, 45)); + tabbar_tab->SetColor("active", "background-color", Colorf::fromRgba8(51, 51, 51)); + + tabwidget_stack->SetDouble("noncontent-left", 20.0); + tabwidget_stack->SetDouble("noncontent-top", 5.0); + tabwidget_stack->SetDouble("noncontent-right", 20.0); + tabwidget_stack->SetDouble("noncontent-bottom", 5.0); + + checkbox_label->SetColor("checked-outer-border-color", Colorf::fromRgba8(100, 100, 100)); + checkbox_label->SetColor("checked-inner-border-color", Colorf::fromRgba8(51, 51, 51)); + checkbox_label->SetColor("checked-color", Colorf::fromRgba8(226, 223, 219)); + checkbox_label->SetColor("unchecked-outer-border-color", Colorf::fromRgba8(99, 99, 99)); + checkbox_label->SetColor("unchecked-inner-border-color", Colorf::fromRgba8(51, 51, 51)); + + menubar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + toolbar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + statusbar->SetColor("background-color", Colorf::fromRgba8(33, 33, 33)); + + toolbarbutton->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + toolbarbutton->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); + + menubaritem->SetColor("color", Colorf::fromRgba8(226, 223, 219)); + menubaritem->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + menubaritem->SetColor("hover", "color", Colorf::fromRgba8(0, 0, 0)); + menubaritem->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); + menubaritem->SetColor("down", "color", Colorf::fromRgba8(0, 0, 0)); + + menu->SetDouble("noncontent-left", 5.0); + menu->SetDouble("noncontent-top", 5.0); + menu->SetDouble("noncontent-right", 5.0); + menu->SetDouble("noncontent-bottom", 5.0); + menu->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100)); + menu->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100)); + menu->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100)); + menu->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100)); + + menuitem->SetColor("hover", "background-color", Colorf::fromRgba8(78, 78, 78)); + menuitem->SetColor("down", "background-color", Colorf::fromRgba8(88, 88, 88)); +} + +///////////////////////////////////////////////////////////////////////////// + +LightWidgetTheme::LightWidgetTheme() +{ + auto widget = RegisterStyle(std::make_unique(), "widget"); + auto textlabel = RegisterStyle(std::make_unique(widget), "textlabel"); + auto pushbutton = RegisterStyle(std::make_unique(widget), "pushbutton"); + auto lineedit = RegisterStyle(std::make_unique(widget), "lineedit"); + auto textedit = RegisterStyle(std::make_unique(widget), "textedit"); + auto listview = RegisterStyle(std::make_unique(widget), "listview"); + auto scrollbar = RegisterStyle(std::make_unique(widget), "scrollbar"); + auto tabbar = RegisterStyle(std::make_unique(widget), "tabbar"); + auto tabbar_tab = RegisterStyle(std::make_unique(widget), "tabbar-tab"); + auto tabwidget_stack = RegisterStyle(std::make_unique(widget), "tabwidget-stack"); + auto checkbox_label = RegisterStyle(std::make_unique(widget), "checkbox-label"); + auto menubar = RegisterStyle(std::make_unique(widget), "menubar"); + auto menubaritem = RegisterStyle(std::make_unique(widget), "menubaritem"); + auto menu = RegisterStyle(std::make_unique(widget), "menu"); + auto menuitem = RegisterStyle(std::make_unique(widget), "menuitem"); + + widget->SetString("font-family", "NotoSans"); + widget->SetColor("color", Colorf::fromRgba8(0, 0, 0)); + widget->SetColor("window-background", Colorf::fromRgba8(240, 240, 240)); + widget->SetColor("window-border", Colorf::fromRgba8(100, 100, 100)); + widget->SetColor("window-caption-color", Colorf::fromRgba8(70, 70, 70)); + widget->SetColor("window-caption-text-color", Colorf::fromRgba8(226, 223, 219)); + + pushbutton->SetDouble("noncontent-left", 10.0); + pushbutton->SetDouble("noncontent-top", 5.0); + pushbutton->SetDouble("noncontent-right", 10.0); + pushbutton->SetDouble("noncontent-bottom", 5.0); + pushbutton->SetColor("background-color", Colorf::fromRgba8(210, 210, 210)); + pushbutton->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + pushbutton->SetColor("hover", "background-color", Colorf::fromRgba8(200, 200, 200)); + pushbutton->SetColor("down", "background-color", Colorf::fromRgba8(190, 190, 190)); + + lineedit->SetDouble("noncontent-left", 5.0); + lineedit->SetDouble("noncontent-top", 3.0); + lineedit->SetDouble("noncontent-right", 5.0); + lineedit->SetDouble("noncontent-bottom", 3.0); + lineedit->SetColor("background-color", Colorf::fromRgba8(255, 255, 255)); + lineedit->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + lineedit->SetColor("selection-color", Colorf::fromRgba8(210, 210, 255)); + lineedit->SetColor("no-focus-selection-color", Colorf::fromRgba8(240, 240, 255)); + + textedit->SetDouble("noncontent-left", 8.0); + textedit->SetDouble("noncontent-top", 8.0); + textedit->SetDouble("noncontent-right", 8.0); + textedit->SetDouble("noncontent-bottom", 8.0); + textedit->SetColor("background-color", Colorf::fromRgba8(255, 255, 255)); + textedit->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + textedit->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + textedit->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + textedit->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + + listview->SetDouble("noncontent-left", 10.0); + listview->SetDouble("noncontent-top", 10.0); + listview->SetDouble("noncontent-right", 3.0); + listview->SetDouble("noncontent-bottom", 10.0); + listview->SetColor("background-color", Colorf::fromRgba8(230, 230, 230)); + listview->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + listview->SetColor("selection-color", Colorf::fromRgba8(200, 200, 200)); + + scrollbar->SetColor("track-color", Colorf::fromRgba8(210, 210, 220)); + scrollbar->SetColor("thumb-color", Colorf::fromRgba8(180, 180, 180)); + + tabbar->SetDouble("noncontent-left", 20.0); + tabbar->SetDouble("noncontent-right", 20.0); + tabbar->SetColor("background-color", Colorf::fromRgba8(220, 220, 220)); + + tabbar_tab->SetDouble("noncontent-left", 15.0); + tabbar_tab->SetDouble("noncontent-right", 15.0); + tabbar_tab->SetColor("hover", "background-color", Colorf::fromRgba8(210, 210, 210)); + tabbar_tab->SetColor("active", "background-color", Colorf::fromRgba8(240, 240, 240)); + + tabwidget_stack->SetDouble("noncontent-left", 20.0); + tabwidget_stack->SetDouble("noncontent-top", 5.0); + tabwidget_stack->SetDouble("noncontent-right", 20.0); + tabwidget_stack->SetDouble("noncontent-bottom", 5.0); + + checkbox_label->SetColor("checked-outer-border-color", Colorf::fromRgba8(155, 155, 155)); + checkbox_label->SetColor("checked-inner-border-color", Colorf::fromRgba8(200, 200, 200)); + checkbox_label->SetColor("checked-color", Colorf::fromRgba8(50, 50, 50)); + checkbox_label->SetColor("unchecked-outer-border-color", Colorf::fromRgba8(156, 156, 156)); + checkbox_label->SetColor("unchecked-inner-border-color", Colorf::fromRgba8(200, 200, 200)); + + menubar->SetColor("background-color", Colorf::fromRgba8(70, 70, 70)); + + menubaritem->SetColor("color", Colorf::fromRgba8(226, 223, 219)); + menubaritem->SetColor("hover", "background-color", Colorf::fromRgba8(200, 200, 200)); + menubaritem->SetColor("hover", "color", Colorf::fromRgba8(0, 0, 0)); + menubaritem->SetColor("down", "background-color", Colorf::fromRgba8(190, 190, 190)); + menubaritem->SetColor("down", "color", Colorf::fromRgba8(0, 0, 0)); + + menu->SetDouble("noncontent-left", 5.0); + menu->SetDouble("noncontent-top", 5.0); + menu->SetDouble("noncontent-right", 5.0); + menu->SetDouble("noncontent-bottom", 5.0); + menu->SetColor("background-color", Colorf::fromRgba8(255, 255, 255)); + menu->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155)); + menu->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155)); + menu->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155)); + menu->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155)); + + menuitem->SetColor("hover", "background-color", Colorf::fromRgba8(200, 200, 200)); + menuitem->SetColor("down", "background-color", Colorf::fromRgba8(190, 190, 190)); +} diff --git a/libraries/ZWidget/src/core/timer.cpp b/libraries/ZWidget/src/core/timer.cpp index 4c748561b..5ebe2612f 100644 --- a/libraries/ZWidget/src/core/timer.cpp +++ b/libraries/ZWidget/src/core/timer.cpp @@ -13,6 +13,8 @@ Timer::Timer(Widget* owner) : OwnerObj(owner) Timer::~Timer() { + Stop(); + if (PrevTimerObj) PrevTimerObj->NextTimerObj = NextTimerObj; if (NextTimerObj) diff --git a/libraries/ZWidget/src/core/truetypefont.cpp b/libraries/ZWidget/src/core/truetypefont.cpp index 3f71c058d..757929e3e 100644 --- a/libraries/ZWidget/src/core/truetypefont.cpp +++ b/libraries/ZWidget/src/core/truetypefont.cpp @@ -11,7 +11,7 @@ #include #endif -TrueTypeFont::TrueTypeFont(std::shared_ptr& initdata, int ttcFontIndex) : data(initdata) +TrueTypeFont::TrueTypeFont(std::shared_ptr initdata, int ttcFontIndex) : data(std::move(initdata)) { if (data->size() > 0x7fffffff) throw std::runtime_error("TTF file is larger than 2 gigabytes!"); @@ -23,7 +23,7 @@ TrueTypeFont::TrueTypeFont(std::shared_ptr& initdata, int if (memcmp(versionTag.data(), "ttcf", 4) == 0) // TTC header { ttcHeader.Load(reader); - if (ttcFontIndex >= ttcHeader.numFonts) + if (ttcFontIndex >= (int)ttcHeader.numFonts) throw std::runtime_error("TTC font index out of bounds"); reader.Seek(ttcHeader.tableDirectoryOffsets[ttcFontIndex]); } @@ -122,7 +122,7 @@ TrueTypeGlyph TrueTypeFont::LoadGlyph(uint32_t glyphIndex, double height) const path.fill_mode = PathFillMode::winding; int startPoint = 0; - int numberOfContours = g.endPtsOfContours.size(); + int numberOfContours = (int)g.endPtsOfContours.size(); for (int i = 0; i < numberOfContours; i++) { int endPoint = g.endPtsOfContours[i]; @@ -310,7 +310,7 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos if (numberOfContours > 0) // Simple glyph { - int pointsOffset = g.points.size(); + int pointsOffset = (int)g.points.size(); for (ttf_uint16 i = 0; i < numberOfContours; i++) g.endPtsOfContours.push_back(pointsOffset + reader.ReadUInt16()); @@ -339,15 +339,15 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos if (g.flags[i] & TTF_X_SHORT_VECTOR) { ttf_int16 x = reader.ReadUInt8(); - g.points[i].x = (g.flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) ? x : -x; + g.points[i].x = (float)((g.flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) ? x : -x); } else if (g.flags[i] & TTF_X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR) { - g.points[i].x = 0; + g.points[i].x = 0.0f; } else { - g.points[i].x = reader.ReadInt16(); + g.points[i].x = (float)reader.ReadInt16(); } } @@ -356,15 +356,15 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos if (g.flags[i] & TTF_Y_SHORT_VECTOR) { ttf_int16 y = reader.ReadUInt8(); - g.points[i].y = (g.flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) ? y : -y; + g.points[i].y = (float)((g.flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) ? y : -y); } else if (g.flags[i] & TTF_Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR) { - g.points[i].y = 0; + g.points[i].y = 0.0f; } else { - g.points[i].y = reader.ReadInt16(); + g.points[i].y = (float)reader.ReadInt16(); } } @@ -380,7 +380,7 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos if (compositeDepth == 8) throw std::runtime_error("Composite glyph recursion exceeded"); - int parentPointsOffset = g.points.size(); + int parentPointsOffset = (int)g.points.size(); bool weHaveInstructions = false; while (true) @@ -420,7 +420,7 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos bool transform = true; if (flags & TTF_WE_HAVE_A_SCALE) { - ttf_F2DOT14 scale = F2DOT14_ToFloat(reader.ReadF2DOT14()); + float scale = F2DOT14_ToFloat(reader.ReadF2DOT14()); mat2x2[0] = scale; mat2x2[1] = 0; mat2x2[2] = 0; @@ -445,7 +445,7 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos transform = false; } - int childPointsOffset = g.points.size(); + int childPointsOffset = (int)g.points.size(); LoadGlyph(g, childGlyphIndex, compositeDepth + 1); if (transform) @@ -463,8 +463,8 @@ void TrueTypeFont::LoadGlyph(TTF_SimpleGlyph& g, uint32_t glyphIndex, int compos if (flags & TTF_ARGS_ARE_XY_VALUES) { - dx = argument1; - dy = argument2; + dx = (float)argument1; + dy = (float)argument2; // Spec states we must fall back to TTF_UNSCALED_COMPONENT_OFFSET if both flags are set if ((flags & (TTF_SCALED_COMPONENT_OFFSET | TTF_UNSCALED_COMPONENT_OFFSET)) == TTF_SCALED_COMPONENT_OFFSET) @@ -976,8 +976,8 @@ void TTF_NamingTable::Load(TrueTypeFileReader& reader) for (ttf_uint16 i = 0; i < langTagCount; i++) { LangTagRecord record; - ttf_uint16 length; - ttf_Offset16 langTagOffset; + record.length = reader.ReadUInt16(); + record.langTagOffset = reader.ReadOffset16(); langTagRecord.push_back(record); } } diff --git a/libraries/ZWidget/src/core/truetypefont.h b/libraries/ZWidget/src/core/truetypefont.h index 5fd4c5099..53d8837fc 100644 --- a/libraries/ZWidget/src/core/truetypefont.h +++ b/libraries/ZWidget/src/core/truetypefont.h @@ -445,7 +445,7 @@ public: class TrueTypeFontFileData { public: - TrueTypeFontFileData(std::vector& data) : dataVector(std::move(data)) + TrueTypeFontFileData(std::vector data) : dataVector(std::move(data)) { dataPtr = dataVector.data(); dataSize = dataVector.size(); @@ -491,7 +491,7 @@ private: class TrueTypeFont { public: - TrueTypeFont(std::shared_ptr& data, int ttcFontIndex = 0); + TrueTypeFont(std::shared_ptr data, int ttcFontIndex = 0); static std::vector GetFontNames(const std::shared_ptr& data); diff --git a/libraries/ZWidget/src/core/widget.cpp b/libraries/ZWidget/src/core/widget.cpp index 2bbbf21f2..e0c637f08 100644 --- a/libraries/ZWidget/src/core/widget.cpp +++ b/libraries/ZWidget/src/core/widget.cpp @@ -2,14 +2,31 @@ #include "core/widget.h" #include "core/timer.h" #include "core/colorf.h" +#include "core/theme.h" #include +#include +#include -Widget::Widget(Widget* parent, WidgetType type) : Type(type) +Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type) { if (type != WidgetType::Child) { - DispWindow = DisplayWindow::Create(this); - DispCanvas = Canvas::create(DispWindow.get()); + Widget* owner = parent ? parent->Window() : nullptr; + DispWindow = DisplayWindow::Create(this, type == WidgetType::Popup, owner ? owner->DispWindow.get() : nullptr, renderAPI); + if (renderAPI == RenderAPI::Unspecified || renderAPI == RenderAPI::Bitmap) + { + DispCanvas = Canvas::create(); + DispCanvas->attach(DispWindow.get()); + } + SetStyleState("root"); + + SetWindowBackground(GetStyleColor("window-background")); + if (GetStyleColor("window-border").a > 0.0f) + SetWindowBorderColor(GetStyleColor("window-border")); + if (GetStyleColor("window-caption-color").a > 0.0f) + SetWindowCaptionColor(GetStyleColor("window-caption-color")); + if (GetStyleColor("window-caption-text-color").a > 0.0f) + SetWindowCaptionTextColor(GetStyleColor("window-caption-text-color")); } SetParent(parent); @@ -17,6 +34,9 @@ Widget::Widget(Widget* parent, WidgetType type) : Type(type) Widget::~Widget() { + if (DispCanvas) + DispCanvas->detach(); + while (LastChildObj) delete LastChildObj; @@ -26,6 +46,17 @@ Widget::~Widget() DetachFromParent(); } +void Widget::SetCanvas(std::unique_ptr canvas) +{ + if (DispWindow) + { + if (DispCanvas) + DispCanvas->detach(); + DispCanvas = std::move(canvas); + DispCanvas->attach(DispWindow.get()); + } +} + void Widget::SetParent(Widget* newParent) { if (ParentObj != newParent) @@ -138,10 +169,10 @@ Rect Widget::GetFrameGeometry() const void Widget::SetNoncontentSizes(double left, double top, double right, double bottom) { - Noncontent.Left = left; - Noncontent.Top = top; - Noncontent.Right = right; - Noncontent.Bottom = bottom; + SetStyleDouble("noncontent-left", left); + SetStyleDouble("noncontent-top", top); + SetStyleDouble("noncontent-right", right); + SetStyleDouble("noncontent-bottom", bottom); } void Widget::SetFrameGeometry(const Rect& geometry) @@ -149,14 +180,18 @@ void Widget::SetFrameGeometry(const Rect& geometry) if (Type == WidgetType::Child) { FrameGeometry = geometry; - double left = FrameGeometry.left() + Noncontent.Left; - double top = FrameGeometry.top() + Noncontent.Top; - double right = FrameGeometry.right() - Noncontent.Right; - double bottom = FrameGeometry.bottom() - Noncontent.Bottom; + double left = FrameGeometry.left() + GetNoncontentLeft(); + double top = FrameGeometry.top() + GetNoncontentTop(); + double right = FrameGeometry.right() - GetNoncontentRight(); + double bottom = FrameGeometry.bottom() - GetNoncontentBottom(); left = std::min(left, FrameGeometry.right()); top = std::min(top, FrameGeometry.bottom()); right = std::max(right, FrameGeometry.left()); bottom = std::max(bottom, FrameGeometry.top()); + left = GridFitPoint(left); + top = GridFitPoint(top); + right = GridFitPoint(right); + bottom = GridFitPoint(bottom); ContentGeometry = Rect::ltrb(left, top, right, bottom); OnGeometryChanged(); } @@ -187,6 +222,15 @@ void Widget::ShowFullscreen() } } +bool Widget::IsFullscreen() +{ + if (Type != WidgetType::Child) + { + return DispWindow->IsWindowFullscreen(); + } + return false; +} + void Widget::ShowMaximized() { if (Type != WidgetType::Child) @@ -290,9 +334,12 @@ void Widget::Update() void Widget::Repaint() { Widget* w = Window(); - w->DispCanvas->begin(WindowBackground); - w->Paint(w->DispCanvas.get()); - w->DispCanvas->end(); + if (w->DispCanvas) + { + w->DispCanvas->begin(WindowBackground); + w->Paint(w->DispCanvas.get()); + w->DispCanvas->end(); + } } void Widget::Paint(Canvas* canvas) @@ -316,7 +363,16 @@ void Widget::Paint(Canvas* canvas) canvas->popClip(); } -bool Widget::GetKeyState(EInputKey key) +void Widget::OnPaintFrame(Canvas* canvas) +{ + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + if (style) + { + style->Paint(this, canvas, GetFrameGeometry().size()); + } +} + +bool Widget::GetKeyState(InputKey key) { Widget* window = Window(); return window ? window->DispWindow->GetKeyState(key) : false; @@ -403,7 +459,7 @@ void Widget::SetCursor(StandardCursor cursor) } } -void Widget::CaptureMouse() +void Widget::SetPointerCapture() { Widget* w = Window(); if (w && w->CaptureWidget != this) @@ -413,7 +469,7 @@ void Widget::CaptureMouse() } } -void Widget::ReleaseMouseCapture() +void Widget::ReleasePointerCapture() { Widget* w = Window(); if (w && w->CaptureWidget != nullptr) @@ -423,6 +479,24 @@ void Widget::ReleaseMouseCapture() } } +void Widget::SetModalCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != this) + { + w->CaptureWidget = this; + } +} + +void Widget::ReleaseModalCapture() +{ + Widget* w = Window(); + if (w && w->CaptureWidget != nullptr) + { + w->CaptureWidget = nullptr; + } +} + std::string Widget::GetClipboardText() { Widget* w = Window(); @@ -439,12 +513,12 @@ void Widget::SetClipboardText(const std::string& text) w->DispWindow->SetClipboardText(text); } -Widget* Widget::Window() +Widget* Widget::Window() const { - for (Widget* w = this; w != nullptr; w = w->Parent()) + for (const Widget* w = this; w != nullptr; w = w->Parent()) { if (w->DispWindow) - return w; + return const_cast(w); } return nullptr; } @@ -459,11 +533,29 @@ Canvas* Widget::GetCanvas() const return nullptr; } +bool Widget::IsParent(const Widget* w) const +{ + while (w) + { + w = w->Parent(); + if (w == this) + return true; + } + return false; +} + +bool Widget::IsChild(const Widget* w) const +{ + if (!w) + return false; + return w->IsParent(this); +} + Widget* Widget::ChildAt(const Point& pos) { for (Widget* cur = LastChild(); cur != nullptr; cur = cur->PrevSibling()) { - if (!cur->HiddenFlag && cur->FrameGeometry.contains(pos)) + if (cur->Type == WidgetType::Child && !cur->HiddenFlag && cur->FrameGeometry.contains(pos)) { Widget* cur2 = cur->ChildAt(pos - cur->ContentGeometry.topLeft()); return cur2 ? cur2 : cur; @@ -491,7 +583,7 @@ Point Widget::MapFromGlobal(const Point& pos) const { if (cur->DispWindow) { - return p - cur->GetFrameGeometry().topLeft(); + return cur->DispWindow->MapFromGlobal(p); } p -= cur->ContentGeometry.topLeft(); } @@ -517,7 +609,7 @@ Point Widget::MapToGlobal(const Point& pos) const { if (cur->DispWindow) { - return cur->GetFrameGeometry().topLeft() + p; + return cur->DispWindow->MapToGlobal(p); } p += cur->ContentGeometry.topLeft(); } @@ -570,7 +662,19 @@ void Widget::OnWindowMouseMove(const Point& pos) } } -void Widget::OnWindowMouseDown(const Point& pos, EInputKey key) +void Widget::OnWindowMouseLeave() +{ + if (HoverWidget) + { + for (Widget* w = HoverWidget; w; w = w->Parent()) + { + w->OnMouseLeave(); + } + HoverWidget = nullptr; + } +} + +void Widget::OnWindowMouseDown(const Point& pos, InputKey key) { if (CaptureWidget) { @@ -591,7 +695,7 @@ void Widget::OnWindowMouseDown(const Point& pos, EInputKey key) } } -void Widget::OnWindowMouseDoubleclick(const Point& pos, EInputKey key) +void Widget::OnWindowMouseDoubleclick(const Point& pos, InputKey key) { if (CaptureWidget) { @@ -612,7 +716,7 @@ void Widget::OnWindowMouseDoubleclick(const Point& pos, EInputKey key) } } -void Widget::OnWindowMouseUp(const Point& pos, EInputKey key) +void Widget::OnWindowMouseUp(const Point& pos, InputKey key) { if (CaptureWidget) { @@ -633,7 +737,7 @@ void Widget::OnWindowMouseUp(const Point& pos, EInputKey key) } } -void Widget::OnWindowMouseWheel(const Point& pos, EInputKey key) +void Widget::OnWindowMouseWheel(const Point& pos, InputKey key) { if (CaptureWidget) { @@ -672,13 +776,13 @@ void Widget::OnWindowKeyChar(std::string chars) FocusWidget->OnKeyChar(chars); } -void Widget::OnWindowKeyDown(EInputKey key) +void Widget::OnWindowKeyDown(InputKey key) { if (FocusWidget) FocusWidget->OnKeyDown(key); } -void Widget::OnWindowKeyUp(EInputKey key) +void Widget::OnWindowKeyUp(InputKey key) { if (FocusWidget) FocusWidget->OnKeyUp(key); @@ -686,9 +790,21 @@ void Widget::OnWindowKeyUp(EInputKey key) void Widget::OnWindowGeometryChanged() { + if (!DispWindow) + return; Size size = DispWindow->GetClientSize(); FrameGeometry = Rect::xywh(0.0, 0.0, size.width, size.height); - ContentGeometry = FrameGeometry; + + double left = FrameGeometry.left() + GetNoncontentLeft(); + double top = FrameGeometry.top() + GetNoncontentTop(); + double right = FrameGeometry.right() - GetNoncontentRight(); + double bottom = FrameGeometry.bottom() - GetNoncontentBottom(); + left = std::min(left, FrameGeometry.right()); + top = std::min(top, FrameGeometry.bottom()); + right = std::max(right, FrameGeometry.left()); + bottom = std::max(bottom, FrameGeometry.top()); + ContentGeometry = Rect::ltrb(left, top, right, bottom); + OnGeometryChanged(); } @@ -709,7 +825,133 @@ void Widget::OnWindowDpiScaleChanged() { } +double Widget::GetDpiScale() const +{ + Widget* w = Window(); + return w ? w->DispWindow->GetDpiScale() : 1.0; +} + +double Widget::GridFitPoint(double p) const +{ + double dpiscale = GetDpiScale(); + return std::round(p * dpiscale) / dpiscale; +} + +double Widget::GridFitSize(double s) const +{ + if (s <= 0.0) + return 0.0; + double dpiscale = GetDpiScale(); + return std::max(std::floor(s * dpiscale + 0.25), 1.0) / dpiscale; +} + Size Widget::GetScreenSize() { return DisplayWindow::GetScreenSize(); } + +void* Widget::GetNativeHandle() +{ + Widget* w = Window(); + return w ? w->DispWindow->GetNativeHandle() : nullptr; +} + +int Widget::GetNativePixelWidth() +{ + Widget* w = Window(); + return w ? w->DispWindow->GetPixelWidth() : 0; +} + +int Widget::GetNativePixelHeight() +{ + Widget* w = Window(); + return w ? w->DispWindow->GetPixelHeight() : 0; +} + +void Widget::SetStyleClass(const std::string& themeClass) +{ + if (StyleClass != themeClass) + { + StyleClass = themeClass; + Update(); + } +} + +void Widget::SetStyleState(const std::string& state) +{ + if (StyleState != state) + { + StyleState = state; + Update(); + } +} + +void Widget::SetStyleBool(const std::string& propertyName, bool value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleInt(const std::string& propertyName, int value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleDouble(const std::string& propertyName, double value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleString(const std::string& propertyName, const std::string& value) +{ + StyleProperties[propertyName] = value; +} + +void Widget::SetStyleColor(const std::string& propertyName, const Colorf& value) +{ + StyleProperties[propertyName] = value; +} + +bool Widget::GetStyleBool(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetBool(StyleState, propertyName) : false; +} + +int Widget::GetStyleInt(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetInt(StyleState, propertyName) : 0; +} + +double Widget::GetStyleDouble(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetDouble(StyleState, propertyName) : 0.0; +} + +std::string Widget::GetStyleString(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetString(StyleState, propertyName) : std::string(); +} + +Colorf Widget::GetStyleColor(const std::string& propertyName) const +{ + auto it = StyleProperties.find(propertyName); + if (it != StyleProperties.end()) + return std::get(it->second); + WidgetStyle* style = WidgetTheme::GetTheme()->GetStyle(StyleClass); + return style ? style->GetColor(StyleState, propertyName) : Colorf::transparent(); +} diff --git a/libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp b/libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp new file mode 100644 index 000000000..6d0ce3931 --- /dev/null +++ b/libraries/ZWidget/src/systemdialogs/open_file_dialog.cpp @@ -0,0 +1,12 @@ + +#include "systemdialogs/open_file_dialog.h" +#include "window/window.h" +#include "core/widget.h" + +std::unique_ptr OpenFileDialog::Create(Widget* owner) +{ + DisplayWindow* windowOwner = nullptr; + if (owner && owner->Window()) + windowOwner = owner->Window()->DispWindow.get(); + return DisplayBackend::Get()->CreateOpenFileDialog(windowOwner); +} diff --git a/libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp b/libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp new file mode 100644 index 000000000..8ccf66237 --- /dev/null +++ b/libraries/ZWidget/src/systemdialogs/open_folder_dialog.cpp @@ -0,0 +1,12 @@ + +#include "systemdialogs/open_folder_dialog.h" +#include "window/window.h" +#include "core/widget.h" + +std::unique_ptr OpenFolderDialog::Create(Widget* owner) +{ + DisplayWindow* windowOwner = nullptr; + if (owner && owner->Window()) + windowOwner = owner->Window()->DispWindow.get(); + return DisplayBackend::Get()->CreateOpenFolderDialog(windowOwner); +} diff --git a/libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp b/libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp new file mode 100644 index 000000000..0163fe7cb --- /dev/null +++ b/libraries/ZWidget/src/systemdialogs/save_file_dialog.cpp @@ -0,0 +1,12 @@ + +#include "systemdialogs/save_file_dialog.h" +#include "window/window.h" +#include "core/widget.h" + +std::unique_ptr SaveFileDialog::Create(Widget* owner) +{ + DisplayWindow* windowOwner = nullptr; + if (owner && owner->Window()) + windowOwner = owner->Window()->DispWindow.get(); + return DisplayBackend::Get()->CreateSaveFileDialog(windowOwner); +} diff --git a/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp b/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp index 6cf90badf..77c1bd645 100644 --- a/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp +++ b/libraries/ZWidget/src/widgets/checkboxlabel/checkboxlabel.cpp @@ -3,6 +3,7 @@ CheckboxLabel::CheckboxLabel(Widget* parent) : Widget(parent) { + SetStyleClass("checkbox-label"); } void CheckboxLabel::SetText(const std::string& value) @@ -40,30 +41,37 @@ double CheckboxLabel::GetPreferredHeight() const void CheckboxLabel::OnPaint(Canvas* canvas) { + // To do: add and use GetStyleImage for the checkbox + + double center = GridFitPoint(GetHeight() * 0.5); + double borderwidth = GridFitSize(1.0); + double outerboxsize = GridFitSize(10.0); + double innerboxsize = outerboxsize - 2.0 * borderwidth; + double checkedsize = innerboxsize - 2.0 * borderwidth; if (checked) { - canvas->fillRect(Rect::xywh(0.0, GetHeight() * 0.5 - 6.0, 10.0, 10.0), Colorf::fromRgba8(100, 100, 100)); - canvas->fillRect(Rect::xywh(1.0, GetHeight() * 0.5 - 5.0, 8.0, 8.0), Colorf::fromRgba8(51, 51, 51)); - canvas->fillRect(Rect::xywh(2.0, GetHeight() * 0.5 - 4.0, 6.0, 6.0), Colorf::fromRgba8(226, 223, 219)); + canvas->fillRect(Rect::xywh(0.0, center - 6.0 * borderwidth, outerboxsize, outerboxsize), GetStyleColor("checked-outer-border-color")); + canvas->fillRect(Rect::xywh(1.0 * borderwidth, center - 5.0 * borderwidth, innerboxsize, innerboxsize), GetStyleColor("checked-inner-border-color")); + canvas->fillRect(Rect::xywh(2.0 * borderwidth, center - 4.0 * borderwidth, checkedsize, checkedsize), GetStyleColor("checked-color")); } else { - canvas->fillRect(Rect::xywh(0.0, GetHeight() * 0.5 - 6.0, 10.0, 10.0), Colorf::fromRgba8(99, 99, 99)); - canvas->fillRect(Rect::xywh(1.0, GetHeight() * 0.5 - 5.0, 8.0, 8.0), Colorf::fromRgba8(51, 51, 51)); + canvas->fillRect(Rect::xywh(0.0, center - 6.0 * borderwidth, outerboxsize, outerboxsize), GetStyleColor("unchecked-outer-border-color")); + canvas->fillRect(Rect::xywh(1.0 * borderwidth, center - 5.0 * borderwidth, innerboxsize, innerboxsize), GetStyleColor("unchecked-inner-border-color")); } - canvas->drawText(Point(14.0, GetHeight() - 5.0), Colorf::fromRgba8(255, 255, 255), text); + canvas->drawText(Point(14.0, GetHeight() - 5.0), GetStyleColor("color"), text); } -bool CheckboxLabel::OnMouseDown(const Point& pos, int key) +bool CheckboxLabel::OnMouseDown(const Point& pos, InputKey key) { mouseDownActive = true; SetFocus(); return true; } -bool CheckboxLabel::OnMouseUp(const Point& pos, int key) +bool CheckboxLabel::OnMouseUp(const Point& pos, InputKey key) { if (mouseDownActive) { @@ -78,9 +86,9 @@ void CheckboxLabel::OnMouseLeave() mouseDownActive = false; } -void CheckboxLabel::OnKeyUp(EInputKey key) +void CheckboxLabel::OnKeyUp(InputKey key) { - if (key == IK_Space) + if (key == InputKey::Space) Toggle(); } diff --git a/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp b/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp index 45d35a160..70323cc89 100644 --- a/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp +++ b/libraries/ZWidget/src/widgets/imagebox/imagebox.cpp @@ -5,6 +5,14 @@ ImageBox::ImageBox(Widget* parent) : Widget(parent) { } +double ImageBox::GetPreferredWidth() const +{ + if (image) + return (double)image->GetWidth(); + else + return 0.0; +} + double ImageBox::GetPreferredHeight() const { if (image) @@ -22,10 +30,44 @@ void ImageBox::SetImage(std::shared_ptr newImage) } } +void ImageBox::SetImageMode(ImageBoxMode newMode) +{ + if (mode != newMode) + { + mode = newMode; + Update(); + } +} + void ImageBox::OnPaint(Canvas* canvas) { if (image) { - canvas->drawImage(image, Point((GetWidth() - (double)image->GetWidth()) * 0.5, (GetHeight() - (double)image->GetHeight()) * 0.5)); + if (mode == ImageBoxMode::Center) + { + canvas->drawImage(image, Point((GetWidth() - (double)image->GetWidth()) * 0.5, (GetHeight() - (double)image->GetHeight()) * 0.5)); + } + else if (mode == ImageBoxMode::Contain) + { + double bw = GetWidth(); + double bh = GetHeight(); + double iw = image->GetWidth(); + double ih = image->GetHeight(); + double xscale = bw / iw; + double yscale = bh / ih; + double scale = std::min(xscale, yscale); + canvas->drawImage(image, Rect::xywh((bw - iw * scale) * 0.5, (bh - ih * scale) * 0.5, iw * scale, ih * scale)); + } + else if (mode == ImageBoxMode::Cover) + { + double bw = GetWidth(); + double bh = GetHeight(); + double iw = image->GetWidth(); + double ih = image->GetHeight(); + double xscale = bw / iw; + double yscale = bh / ih; + double scale = std::max(xscale, yscale); + canvas->drawImage(image, Rect::xywh((bw - iw * scale) * 0.5, (bh - ih * scale) * 0.5, iw * scale, ih * scale)); + } } } diff --git a/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp b/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp index 9065ccde4..b34a18c5e 100644 --- a/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp +++ b/libraries/ZWidget/src/widgets/lineedit/lineedit.cpp @@ -1,11 +1,12 @@ -#include + #include "widgets/lineedit/lineedit.h" #include "core/utf8reader.h" #include "core/colorf.h" +#include LineEdit::LineEdit(Widget* parent) : Widget(parent) { - SetNoncontentSizes(5.0, 3.0, 5.0, 3.0); + SetStyleClass("lineedit"); timer = new Timer(this); timer->FuncExpired = [=]() { OnTimerExpired(); }; @@ -18,8 +19,6 @@ LineEdit::LineEdit(Widget* parent) : Widget(parent) LineEdit::~LineEdit() { - delete timer; - delete scroll_timer; } bool LineEdit::IsReadOnly() const @@ -303,13 +302,13 @@ void LineEdit::OnMouseMove(const Point& pos) } } -bool LineEdit::OnMouseDown(const Point& pos, int key) +bool LineEdit::OnMouseDown(const Point& pos, InputKey key) { - if (key == IK_LeftMouse) + if (key == InputKey::LeftMouse) { if (HasFocus()) { - CaptureMouse(); + SetPointerCapture(); mouse_selecting = true; cursor_pos = GetCharacterIndex(pos.x); SetTextSelection(cursor_pos, 0); @@ -323,25 +322,25 @@ bool LineEdit::OnMouseDown(const Point& pos, int key) return true; } -bool LineEdit::OnMouseDoubleclick(const Point& pos, int key) +bool LineEdit::OnMouseDoubleclick(const Point& pos, InputKey key) { return true; } -bool LineEdit::OnMouseUp(const Point& pos, int key) +bool LineEdit::OnMouseUp(const Point& pos, InputKey key) { - if (mouse_selecting && key == IK_LeftMouse) + if (mouse_selecting && key == InputKey::LeftMouse) { if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained. { - ReleaseMouseCapture(); + ReleasePointerCapture(); ignore_mouse_events = false; mouse_selecting = false; } else { scroll_timer->Stop(); - ReleaseMouseCapture(); + ReleasePointerCapture(); mouse_selecting = false; int sel_end = GetCharacterIndex(pos.x); SetSelectionLength(sel_end - selection_start); @@ -414,12 +413,12 @@ void LineEdit::OnKeyChar(std::string chars) } } -void LineEdit::OnKeyDown(EInputKey key) +void LineEdit::OnKeyDown(InputKey key) { if (FuncIgnoreKeyDown && FuncIgnoreKeyDown(key)) return; - if (key == IK_Enter) + if (key == InputKey::Enter) { if (FuncEnterPressed) FuncEnterPressed(); @@ -432,12 +431,12 @@ void LineEdit::OnKeyDown(EInputKey key) timer->Start(500); // don't blink cursor when moving or typing. } - if (key == IK_Enter || key == IK_Escape || key == IK_Tab) + if (key == InputKey::Enter || key == InputKey::Escape || key == InputKey::Tab) { // Do not consume these. return; } - else if (key == IK_A && GetKeyState(IK_Ctrl)) + else if (key == InputKey::A && GetKeyState(InputKey::Ctrl)) { // select all SetTextSelection(0, (int)text.size()); @@ -445,7 +444,7 @@ void LineEdit::OnKeyDown(EInputKey key) UpdateTextClipping(); Update(); } - else if (key == IK_C && GetKeyState(IK_Ctrl)) + else if (key == InputKey::C && GetKeyState(InputKey::Ctrl)) { if (!password_mode) // Do not allow copying the password to clipboard { @@ -458,54 +457,54 @@ void LineEdit::OnKeyDown(EInputKey key) // Do not consume messages on read only component (only allow CTRL-A and CTRL-C) return; } - else if (key == IK_Left) + else if (key == InputKey::Left) { - Move(-1, GetKeyState(IK_Ctrl), GetKeyState(IK_Shift)); + Move(-1, GetKeyState(InputKey::Ctrl), GetKeyState(InputKey::Shift)); } - else if (key == IK_Right) + else if (key == InputKey::Right) { - Move(1, GetKeyState(IK_Ctrl), GetKeyState(IK_Shift)); + Move(1, GetKeyState(InputKey::Ctrl), GetKeyState(InputKey::Shift)); } - else if (key == IK_Backspace) + else if (key == InputKey::Backspace) { Backspace(); UpdateTextClipping(); } - else if (key == IK_Delete) + else if (key == InputKey::Delete) { Del(); UpdateTextClipping(); } - else if (key == IK_Home) + else if (key == InputKey::Home) { SetSelectionStart(cursor_pos); cursor_pos = 0; - if (GetKeyState(IK_Shift)) + if (GetKeyState(InputKey::Shift)) SetSelectionLength(-selection_start); else SetTextSelection(0, 0); UpdateTextClipping(); Update(); } - else if (key == IK_End) + else if (key == InputKey::End) { SetSelectionStart(cursor_pos); cursor_pos = (int)text.size(); - if (GetKeyState(IK_Shift)) + if (GetKeyState(InputKey::Shift)) SetSelectionLength((int)text.size() - selection_start); else SetTextSelection(0, 0); UpdateTextClipping(); Update(); } - else if (key == IK_X && GetKeyState(IK_Ctrl)) + else if (key == InputKey::X && GetKeyState(InputKey::Ctrl)) { std::string str = GetSelection(); DeleteSelectedText(); SetClipboardText(str); UpdateTextClipping(); } - else if (key == IK_V && GetKeyState(IK_Ctrl)) + else if (key == InputKey::V && GetKeyState(InputKey::Ctrl)) { std::string str = GetClipboardText(); std::string::const_iterator end_str = std::remove(str.begin(), str.end(), '\n'); @@ -576,7 +575,7 @@ void LineEdit::OnKeyDown(EInputKey key) UpdateTextClipping(); } - else if (GetKeyState(IK_Ctrl) && key == IK_Z) + else if (GetKeyState(InputKey::Ctrl) && key == InputKey::Z) { if (!readonly) { @@ -585,7 +584,7 @@ void LineEdit::OnKeyDown(EInputKey key) SetText(tmp); } } - else if (key == IK_Shift) + else if (key == InputKey::Shift) { if (selection_start == -1) SetTextSelection(cursor_pos, 0); @@ -595,7 +594,7 @@ void LineEdit::OnKeyDown(EInputKey key) FuncAfterEditChanged(); } -void LineEdit::OnKeyUp(EInputKey key) +void LineEdit::OnKeyUp(InputKey key) { } @@ -817,6 +816,8 @@ int LineEdit::GetCharacterIndex(double mouse_x) void LineEdit::UpdateTextClipping() { Canvas* canvas = GetCanvas(); + if (!canvas) + return; Size text_size = GetVisualTextSize(canvas, clip_start_offset, (int)text.size() - clip_start_offset); @@ -1067,18 +1068,6 @@ std::string LineEdit::GetVisibleTextAfterSelection() } } -void LineEdit::OnPaintFrame(Canvas* canvas) -{ - double w = GetFrameGeometry().width; - double h = GetFrameGeometry().height; - Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); - canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); -} - void LineEdit::OnPaint(Canvas* canvas) { std::string txt_before = GetVisibleTextBeforeSelection(); @@ -1101,21 +1090,21 @@ void LineEdit::OnPaint(Canvas* canvas) { // Draw selection box. Rect selection_rect = GetSelectionRect(); - canvas->fillRect(selection_rect, HasFocus() ? Colorf::fromRgba8(100, 100, 100) : Colorf::fromRgba8(68, 68, 68)); + canvas->fillRect(selection_rect, HasFocus() ? GetStyleColor("selection-color") : GetStyleColor("no-focus-selection-color")); } // Draw text before selection if (!txt_before.empty()) { - canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_before); + canvas->drawText(Point(0.0, canvas->verticalTextAlign().baseline), GetStyleColor("color"), txt_before); } if (!txt_selected.empty()) { - canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_selected); + canvas->drawText(Point(size_before.width, canvas->verticalTextAlign().baseline), GetStyleColor("color"), txt_selected); } if (!txt_after.empty()) { - canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), Colorf::fromRgba8(255, 255, 255), txt_after); + canvas->drawText(Point(size_before.width + size_selected.width, canvas->verticalTextAlign().baseline), GetStyleColor("color"), txt_after); } // draw cursor @@ -1124,7 +1113,7 @@ void LineEdit::OnPaint(Canvas* canvas) if (cursor_blink_visible) { Rect cursor_rect = GetCursorRect(); - canvas->fillRect(cursor_rect, Colorf::fromRgba8(255, 255, 255)); + canvas->fillRect(cursor_rect, GetStyleColor("color")); } } } diff --git a/libraries/ZWidget/src/widgets/listview/listview.cpp b/libraries/ZWidget/src/widgets/listview/listview.cpp index a8dd066d0..146d9bc23 100644 --- a/libraries/ZWidget/src/widgets/listview/listview.cpp +++ b/libraries/ZWidget/src/widgets/listview/listview.cpp @@ -4,7 +4,7 @@ ListView::ListView(Widget* parent) : Widget(parent) { - SetNoncontentSizes(10.0, 10.0, 3.0, 10.0); + SetStyleClass("listview"); scrollbar = new Scrollbar(this); scrollbar->FuncScroll = [=]() { OnScrollbarScroll(); }; @@ -28,7 +28,7 @@ void ListView::SetColumnWidths(const std::vector& widths) while (column.size() > newWidth) { updated = true; - column.erase(column.end()); + column.pop_back(); } } @@ -146,6 +146,9 @@ void ListView::OnPaint(Canvas* canvas) double w = GetWidth() - scrollbar->GetPreferredWidth() - 2.0; double h = 20.0; + Colorf textColor = GetStyleColor("color"); + Colorf selectionColor = GetStyleColor("selection-color"); + int index = 0; for (const std::vector& item : items) { @@ -154,12 +157,12 @@ void ListView::OnPaint(Canvas* canvas) { if (index == selectedItem) { - canvas->fillRect(Rect::xywh(x - 2.0, itemY, w, h), Colorf::fromRgba8(100, 100, 100)); + canvas->fillRect(Rect::xywh(x - 2.0, itemY, w, h), selectionColor); } double cx = x; for (size_t entry = 0u; entry < item.size(); ++entry) { - canvas->drawText(Point(cx, y + 15.0), Colorf::fromRgba8(255, 255, 255), item[entry]); + canvas->drawText(Point(cx, y + 15.0), textColor, item[entry]); cx += columnwidths[entry]; } } @@ -168,23 +171,11 @@ void ListView::OnPaint(Canvas* canvas) } } -void ListView::OnPaintFrame(Canvas* canvas) -{ - double w = GetFrameGeometry().width; - double h = GetFrameGeometry().height; - Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); - canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); -} - -bool ListView::OnMouseDown(const Point& pos, int key) +bool ListView::OnMouseDown(const Point& pos, InputKey key) { SetFocus(); - if (key == IK_LeftMouse) + if (key == InputKey::LeftMouse) { int index = (int)((pos.y - 5.0 + scrollbar->GetPosition()) / 20.0); if (index >= 0 && (size_t)index < items.size()) @@ -196,31 +187,31 @@ bool ListView::OnMouseDown(const Point& pos, int key) return true; } -bool ListView::OnMouseDoubleclick(const Point& pos, int key) +bool ListView::OnMouseDoubleclick(const Point& pos, InputKey key) { - if (key == IK_LeftMouse) + if (key == InputKey::LeftMouse) { Activate(); } return true; } -bool ListView::OnMouseWheel(const Point& pos, EInputKey key) +bool ListView::OnMouseWheel(const Point& pos, InputKey key) { - if (key == IK_MouseWheelUp) + if (key == InputKey::MouseWheelUp) { scrollbar->SetPosition(std::max(scrollbar->GetPosition() - 20.0, 0.0)); } - else if (key == IK_MouseWheelDown) + else if (key == InputKey::MouseWheelDown) { scrollbar->SetPosition(std::min(scrollbar->GetPosition() + 20.0, scrollbar->GetMax())); } return true; } -void ListView::OnKeyDown(EInputKey key) +void ListView::OnKeyDown(InputKey key) { - if (key == IK_Down) + if (key == InputKey::Down) { if (selectedItem + 1 < (int)items.size()) { @@ -228,7 +219,7 @@ void ListView::OnKeyDown(EInputKey key) } ScrollToItem(selectedItem); } - else if (key == IK_Up) + else if (key == InputKey::Up) { if (selectedItem > 0) { @@ -236,7 +227,7 @@ void ListView::OnKeyDown(EInputKey key) } ScrollToItem(selectedItem); } - else if (key == IK_Enter) + else if (key == InputKey::Enter) { Activate(); } diff --git a/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp b/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp index 7a98a2ce5..a59680a4e 100644 --- a/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp +++ b/libraries/ZWidget/src/widgets/mainwindow/mainwindow.cpp @@ -4,10 +4,13 @@ #include "widgets/toolbar/toolbar.h" #include "widgets/statusbar/statusbar.h" -MainWindow::MainWindow() : Widget(nullptr, WidgetType::Window) +MainWindow::MainWindow(RenderAPI api) : Widget(nullptr, WidgetType::Window, api) { MenubarWidget = new Menubar(this); - // ToolbarWidget = new Toolbar(this); + TopToolbarWidget = new Toolbar(this); + TopToolbarWidget->SetDirection(ToolbarDirection::Horizontal); + LeftToolbarWidget = new Toolbar(this); + LeftToolbarWidget->SetDirection(ToolbarDirection::Vertical); StatusbarWidget = new Statusbar(this); } @@ -32,9 +35,10 @@ void MainWindow::OnGeometryChanged() Size s = GetSize(); MenubarWidget->SetFrameGeometry(0.0, 0.0, s.width, 32.0); - // ToolbarWidget->SetFrameGeometry(0.0, 32.0, s.width, 36.0); + TopToolbarWidget->SetFrameGeometry(0.0, 32.0, s.width, 32.0); + LeftToolbarWidget->SetFrameGeometry(0.0, 64.0, 32.0, s.height - 64.0); StatusbarWidget->SetFrameGeometry(0.0, s.height - 32.0, s.width, 32.0); if (CentralWidget) - CentralWidget->SetFrameGeometry(0.0, 32.0, s.width, s.height - 32.0 - 32.0); + CentralWidget->SetFrameGeometry(32.0, 64.0, s.width - 32.0, s.height - 64.0 - 32.0); } diff --git a/libraries/ZWidget/src/widgets/menubar/menubar.cpp b/libraries/ZWidget/src/widgets/menubar/menubar.cpp index 8a66ce8f3..37353756f 100644 --- a/libraries/ZWidget/src/widgets/menubar/menubar.cpp +++ b/libraries/ZWidget/src/widgets/menubar/menubar.cpp @@ -4,13 +4,387 @@ Menubar::Menubar(Widget* parent) : Widget(parent) { + SetStyleClass("menubar"); } Menubar::~Menubar() { } -void Menubar::OnPaint(Canvas* canvas) +MenubarItem* Menubar::AddItem(std::string text, std::function onOpen, bool alignRight) { - canvas->drawText(Point(16.0, 21.0), Colorf::fromRgba8(0, 0, 0), "File Edit View Tools Window Help"); + auto item = new MenubarItem(this, text, alignRight); + item->SetOpenCallback(std::move(onOpen)); + menuItems.push_back(item); + OnGeometryChanged(); + return item; +} + +void Menubar::ShowMenu(MenubarItem* item) +{ + int index = GetItemIndex(item); + if (index == currentMenubarItem) + return; + + CloseMenu(); + SetFocus(); + SetModalCapture(); + currentMenubarItem = index; + if (currentMenubarItem != -1) + menuItems[currentMenubarItem]->SetStyleState("hover"); + modalMode = true; + if (item->GetOpenCallback()) + { + openMenu = new Menu(this); + openMenu->onCloseMenu = [=]() { CloseMenu(); }; + item->GetOpenCallback()(openMenu); + if (item->AlignRight) + openMenu->SetRightPosition(item->MapToGlobal(Point(item->GetWidth(), item->GetHeight()))); + else + openMenu->SetLeftPosition(item->MapToGlobal(Point(0.0, item->GetHeight()))); + openMenu->Show(); + } +} + +void Menubar::CloseMenu() +{ + if (currentMenubarItem != -1) + menuItems[currentMenubarItem]->SetStyleState(""); + currentMenubarItem = -1; + delete openMenu; + openMenu = nullptr; + ReleaseModalCapture(); + modalMode = false; +} + +void Menubar::OnGeometryChanged() +{ + double w = GetWidth(); + double h = GetHeight(); + double left = 0.0; + double right = w; + for (MenubarItem* item : menuItems) + { + double itemwidth = item->GetPreferredWidth(); + itemwidth += 16.0; + if (!item->AlignRight) + { + item->SetFrameGeometry(left, 0.0, itemwidth, h); + left += itemwidth; + } + else + { + right -= itemwidth; + item->SetFrameGeometry(right, 0.0, itemwidth, h); + } + } +} + +MenubarItem* Menubar::GetMenubarItemAt(const Point& pos) +{ + Widget* widget = ChildAt(pos); + for (MenubarItem* item : menuItems) + { + if (widget == item) + return item; + } + return nullptr; +} + +bool Menubar::OnMouseDown(const Point& pos, InputKey key) +{ + if (!modalMode) + return Widget::OnMouseDown(pos, key); + + MenubarItem* item = GetMenubarItemAt(pos); + if (item) + ShowMenu(item); + else + CloseMenu(); + return true; +} + +bool Menubar::OnMouseUp(const Point& pos, InputKey key) +{ + if (!modalMode) + return Widget::OnMouseUp(pos, key); + + MenubarItem* item = GetMenubarItemAt(pos); + if (!item) + CloseMenu(); + return true; +} + +void Menubar::OnMouseMove(const Point& pos) +{ + if (!modalMode) + return Widget::OnMouseMove(pos); + + MenubarItem* item = GetMenubarItemAt(pos); + if (item) + ShowMenu(item); +} + +int Menubar::GetItemIndex(MenubarItem* item) +{ + int i = 0; + for (MenubarItem* cur : menuItems) + { + if (cur == item) + return i; + i++; + } + return -1; +} + +void Menubar::OnKeyDown(InputKey key) +{ + if (!modalMode) + return Widget::OnKeyDown(key); + + if (key == InputKey::Left) + { + if (!menuItems.empty()) + { + int index = currentMenubarItem - 1; + if (index < 0) + index = (int)menuItems.size() - 1; + ShowMenu(menuItems[index]); + } + } + else if (key == InputKey::Right) + { + if (!menuItems.empty()) + { + int index = currentMenubarItem + 1; + if (index == (int)menuItems.size()) + index = 0; + ShowMenu(menuItems[index]); + } + } + else if (key == InputKey::Up) + { + if (openMenu) + { + // Keep trying until we don't find a separator + for (int i = 0; i < 10; i++) + { + if (!openMenu->selectedItem || !openMenu->selectedItem->PrevSibling()) + { + if (openMenu->LastChild()) + openMenu->SetSelected(static_cast(openMenu->LastChild())); + } + else + { + openMenu->SetSelected(static_cast(openMenu->selectedItem->PrevSibling())); + } + + if (openMenu->selectedItem && openMenu->selectedItem->GetStyleClass() != "menuitemseparator") + break; + } + } + } + else if (key == InputKey::Down) + { + if (openMenu) + { + // Keep trying until we don't find a separator + for (int i = 0; i < 10; i++) + { + if (!openMenu->selectedItem || !openMenu->selectedItem->NextSibling()) + { + if (openMenu->FirstChild()) + openMenu->SetSelected(static_cast(openMenu->FirstChild())); + } + else + { + openMenu->SetSelected(static_cast(openMenu->selectedItem->NextSibling())); + } + + if (openMenu->selectedItem && openMenu->selectedItem->GetStyleClass() != "menuitemseparator") + break; + } + } + } + else if (key == InputKey::Enter) + { + if (openMenu && openMenu->selectedItem) + openMenu->selectedItem->Click(); + } +} + +///////////////////////////////////////////////////////////////////////////// + +MenubarItem::MenubarItem(Menubar* menubar, std::string text, bool alignRight) : Widget(menubar), menubar(menubar), text(text), AlignRight(alignRight) +{ + SetStyleClass("menubaritem"); +} + +bool MenubarItem::OnMouseDown(const Point& pos, InputKey key) +{ + menubar->ShowMenu(this); + return true; +} + +bool MenubarItem::OnMouseUp(const Point& pos, InputKey key) +{ + return true; +} + +void MenubarItem::OnMouseMove(const Point& pos) +{ + if (GetStyleState().empty()) + { + SetStyleState("hover"); + } +} + +void MenubarItem::OnMouseLeave() +{ + SetStyleState(""); +} + +double MenubarItem::GetPreferredWidth() const +{ + Canvas* canvas = GetCanvas(); + return canvas->measureText(text).width; +} + +void MenubarItem::OnPaint(Canvas* canvas) +{ + double x = (GetWidth() - canvas->measureText(text).width) * 0.5; + canvas->drawText(Point(x, 21.0), GetStyleColor("color"), text); +} + +///////////////////////////////////////////////////////////////////////////// + +Menu::Menu(Widget* parent) : Widget(parent, WidgetType::Popup) +{ + SetStyleClass("menu"); +} + +void Menu::SetLeftPosition(const Point& pos) +{ + SetFrameGeometry(Rect::xywh(pos.x, pos.y, GetPreferredWidth() + GetNoncontentLeft() + GetNoncontentRight(), GetPreferredHeight() + GetNoncontentTop() + GetNoncontentBottom())); +} + +void Menu::SetRightPosition(const Point& pos) +{ + SetFrameGeometry(Rect::xywh(pos.x - GetWidth() - GetNoncontentLeft() - GetNoncontentRight(), pos.y, GetWidth() + GetNoncontentLeft() + GetNoncontentRight(), GetHeight() + GetNoncontentTop() + GetNoncontentBottom())); +} + +MenuItem* Menu::AddItem(std::shared_ptr icon, std::string text, std::function onClick) +{ + auto item = new MenuItem(this, [this, onClick]() { if (onCloseMenu) onCloseMenu(); if (onClick) onClick(); }); + if (icon) + item->icon->SetImage(icon); + item->text->SetText(text); + return item; +} + +MenuItemSeparator* Menu::AddSeparator() +{ + auto sep = new MenuItemSeparator(this); + return sep; +} + +double Menu::GetPreferredWidth() const +{ + return GridFitSize(200.0); +} + +double Menu::GetPreferredHeight() const +{ + double h = 0.0; + for (Widget* item = FirstChild(); item != nullptr; item = item->NextSibling()) + { + double itemheight = GridFitSize((item->GetStyleClass() == "menuitemseparator") ? 7.0 : 30.0); + h += itemheight; + } + return h; +} + +void Menu::OnGeometryChanged() +{ + double w = GetWidth(); + double y = 0.0; + for (Widget* item = FirstChild(); item != nullptr; item = item->NextSibling()) + { + double itemheight = GridFitSize((item->GetStyleClass() == "menuitemseparator") ? 7.0 : 30.0); + item->SetFrameGeometry(Rect::xywh(0.0, y, w, itemheight)); + y += itemheight; + } +} + +void Menu::SetSelected(MenuItem* item) +{ + if (selectedItem) + { + selectedItem->SetStyleState(""); + } + selectedItem = item; + if (selectedItem) + { + selectedItem->SetStyleState("hover"); + } +} + +///////////////////////////////////////////////////////////////////////////// + +MenuItem::MenuItem(Menu* menu, std::function onClick) : Widget(menu), menu(menu), onClick(onClick) +{ + SetStyleClass("menuitem"); + icon = new ImageBox(this); + text = new TextLabel(this); +} + +void MenuItem::OnMouseMove(const Point& pos) +{ + menu->SetSelected(this); +} + +bool MenuItem::OnMouseUp(const Point& pos, InputKey key) +{ + Click(); + return true; +} + +void MenuItem::Click() +{ + if (onClick) + { + // We have to make a copy of the handler as it may delete 'this' + auto handler = onClick; + handler(); + } +} + +void MenuItem::OnMouseLeave() +{ + menu->SetSelected(nullptr); +} + +void MenuItem::OnGeometryChanged() +{ + double iconwidth = GridFitSize(icon->GetPreferredWidth()); + double iconheight = GridFitSize(icon->GetPreferredHeight()); + double w = GetWidth(); + double h = GetHeight(); + double textheight = 19.0; + double x0 = GridFitPoint(5.0); + double x1 = GridFitPoint(5.0 + iconwidth); + icon->SetFrameGeometry(Rect::xywh(x0, GridFitPoint((h - iconheight) * 0.5), iconwidth, iconheight)); + text->SetFrameGeometry(Rect::xywh(x1, GridFitPoint((h - textheight) * 0.5), w - x1, textheight)); +} + +///////////////////////////////////////////////////////////////////////////// + +MenuItemSeparator::MenuItemSeparator(Widget* parent) : Widget(parent) +{ + SetStyleClass("menuitemseparator"); +} + +void MenuItemSeparator::OnPaint(Canvas* canvas) +{ + canvas->fillRect(Rect::xywh(0.0, GridFitPoint(GetHeight() * 0.5), GetWidth(), GridFitSize(1.0)), Colorf::fromRgba8(75, 75, 75)); } diff --git a/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp b/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp index d838fe424..e85b2c557 100644 --- a/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp +++ b/libraries/ZWidget/src/widgets/pushbutton/pushbutton.cpp @@ -3,7 +3,7 @@ PushButton::PushButton(Widget* parent) : Widget(parent) { - SetNoncontentSizes(10.0, 5.0, 10.0, 5.0); + SetStyleClass("pushbutton"); } void PushButton::SetText(const std::string& value) @@ -25,52 +25,32 @@ double PushButton::GetPreferredHeight() const return 30.0; } -void PushButton::OnPaintFrame(Canvas* canvas) -{ - double w = GetFrameGeometry().width; - double h = GetFrameGeometry().height; - Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); - Colorf buttoncolor = Colorf::fromRgba8(68, 68, 68); - if (buttonDown) - buttoncolor = Colorf::fromRgba8(88, 88, 88); - else if (hot) - buttoncolor = Colorf::fromRgba8(78, 78, 78); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), buttoncolor); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); - canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); -} - void PushButton::OnPaint(Canvas* canvas) { Rect box = canvas->measureText(text); - canvas->drawText(Point((GetWidth() - box.width) * 0.5, GetHeight() - 5.0), Colorf::fromRgba8(255, 255, 255), text); + canvas->drawText(Point((GetWidth() - box.width) * 0.5, GetHeight() - 5.0), GetStyleColor("color"), text); } void PushButton::OnMouseMove(const Point& pos) { - if (!hot) + if (GetStyleState().empty()) { - hot = true; - Update(); + SetStyleState("hover"); } } -bool PushButton::OnMouseDown(const Point& pos, int key) +bool PushButton::OnMouseDown(const Point& pos, InputKey key) { SetFocus(); - buttonDown = true; - Update(); + SetStyleState("down"); return true; } -bool PushButton::OnMouseUp(const Point& pos, int key) +bool PushButton::OnMouseUp(const Point& pos, InputKey key) { - if (buttonDown) + if (GetStyleState() == "down") { - buttonDown = false; - hot = false; + SetStyleState(""); Repaint(); Click(); } @@ -79,26 +59,23 @@ bool PushButton::OnMouseUp(const Point& pos, int key) void PushButton::OnMouseLeave() { - hot = false; - buttonDown = false; - Update(); + SetStyleState(""); } -void PushButton::OnKeyDown(EInputKey key) +void PushButton::OnKeyDown(InputKey key) { - if (key == IK_Space || key == IK_Enter) + if (key == InputKey::Space || key == InputKey::Enter) { - buttonDown = true; + SetStyleState("down"); Update(); } } -void PushButton::OnKeyUp(EInputKey key) +void PushButton::OnKeyUp(InputKey key) { - if (key == IK_Space || key == IK_Enter) + if (key == InputKey::Space || key == InputKey::Enter) { - buttonDown = false; - hot = false; + SetStyleState(""); Repaint(); Click(); } diff --git a/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp b/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp index 489a66343..c4e29329a 100644 --- a/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp +++ b/libraries/ZWidget/src/widgets/scrollbar/scrollbar.cpp @@ -5,6 +5,7 @@ Scrollbar::Scrollbar(Widget* parent) : Widget(parent) { + SetStyleClass("scrollbar"); UpdatePartPositions(); mouse_down_timer = new Timer(this); @@ -169,7 +170,7 @@ void Scrollbar::OnMouseMove(const Point& pos) Update(); } -bool Scrollbar::OnMouseDown(const Point& pos, int key) +bool Scrollbar::OnMouseDown(const Point& pos, InputKey key) { mouse_drag_start_pos = pos; @@ -253,11 +254,11 @@ bool Scrollbar::OnMouseDown(const Point& pos, int key) UpdatePartPositions(); Update(); - CaptureMouse(); + SetPointerCapture(); return true; } -bool Scrollbar::OnMouseUp(const Point& pos, int key) +bool Scrollbar::OnMouseUp(const Point& pos, InputKey key) { if (mouse_down_mode == mouse_down_thumb_drag) { @@ -269,7 +270,7 @@ bool Scrollbar::OnMouseUp(const Point& pos, int key) mouse_down_timer->Stop(); Update(); - ReleaseMouseCapture(); + ReleasePointerCapture(); return true; } @@ -294,8 +295,8 @@ void Scrollbar::OnPaint(Canvas* canvas) part_button_increment.render_box(canvas, rect_button_increment); */ - canvas->fillRect(Rect::shrink(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight()), 4.0, 0.0, 4.0, 0.0), Colorf::fromRgba8(33, 33, 33)); - canvas->fillRect(Rect::shrink(rect_thumb, 4.0, 0.0, 4.0, 0.0), Colorf::fromRgba8(58, 58, 58)); + canvas->fillRect(Rect::shrink(Rect::xywh(0.0, 0.0, GetWidth(), GetHeight()), 4.0, 0.0, 4.0, 0.0), GetStyleColor("track-color")); + canvas->fillRect(Rect::shrink(rect_thumb, 4.0, 0.0, 4.0, 0.0), GetStyleColor("thumb-color")); } // Calculates positions of all parts. Returns true if thumb position was changed compared to previously, false otherwise. diff --git a/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp b/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp index fd914298c..33faa15c3 100644 --- a/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp +++ b/libraries/ZWidget/src/widgets/statusbar/statusbar.cpp @@ -5,6 +5,8 @@ Statusbar::Statusbar(Widget* parent) : Widget(parent) { + SetStyleClass("statusbar"); + CommandEdit = new LineEdit(this); CommandEdit->SetFrameGeometry(Rect::xywh(90.0, 4.0, 400.0, 23.0)); } @@ -15,5 +17,5 @@ Statusbar::~Statusbar() void Statusbar::OnPaint(Canvas* canvas) { - canvas->drawText(Point(16.0, 21.0), Colorf::fromRgba8(0, 0, 0), "Command:"); + canvas->drawText(Point(16.0, 21.0), Colorf::fromRgba8(226, 223, 219), "Command:"); } diff --git a/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp b/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp index cbac877aa..4bd7551c7 100644 --- a/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp +++ b/libraries/ZWidget/src/widgets/tabwidget/tabwidget.cpp @@ -85,7 +85,7 @@ int TabWidget::GetPageIndex(Widget* pageWidget) const for (size_t i = 0; i < Pages.size(); i++) { if (Pages[i] == pageWidget) - return i; + return (int)i; } return -1; } @@ -98,10 +98,6 @@ void TabWidget::OnBarCurrentChanged() OnCurrentChanged(); } -void TabWidget::OnPaintFrame(Canvas* canvas) -{ -} - void TabWidget::OnGeometryChanged() { double w = GetWidth(); @@ -115,7 +111,7 @@ void TabWidget::OnGeometryChanged() TabBar::TabBar(Widget* parent) : Widget(parent) { - SetNoncontentSizes(20.0, 0.0, 20.0, 0.0); + SetStyleClass("tabbar"); } int TabBar::AddTab(const std::string& label) @@ -129,7 +125,7 @@ int TabBar::AddTab(const std::shared_ptr& icon, const std::string& label) tab->SetIcon(icon); tab->SetText(label); tab->OnClick = [=]() { OnTabClicked(tab); }; - int pageIndex = Tabs.size(); + int pageIndex = (int)Tabs.size(); Tabs.push_back(tab); if (CurrentIndex == -1) SetCurrentIndex(pageIndex); @@ -182,18 +178,11 @@ int TabBar::GetTabIndex(TabBarTab* tab) for (size_t i = 0; i < Tabs.size(); i++) { if (Tabs[i] == tab) - return i; + return (int)i; } return -1; } -void TabBar::OnPaintFrame(Canvas* canvas) -{ - double w = GetFrameGeometry().width; - double h = GetFrameGeometry().height; - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); -} - void TabBar::OnGeometryChanged() { double w = GetWidth(); @@ -211,7 +200,7 @@ void TabBar::OnGeometryChanged() TabBarTab::TabBarTab(Widget* parent) : Widget(parent) { - SetNoncontentSizes(15.0, 0.0, 15.0, 0.0); + SetStyleClass("tabbar-tab"); } void TabBarTab::SetText(const std::string& text) @@ -257,7 +246,7 @@ void TabBarTab::SetCurrent(bool value) if (IsCurrent != value) { IsCurrent = value; - Update(); + SetStyleState(value ? "active" : ""); } } @@ -268,20 +257,6 @@ double TabBarTab::GetPreferredWidth() const return x; } -void TabBarTab::OnPaintFrame(Canvas* canvas) -{ - double w = GetFrameGeometry().width; - double h = GetFrameGeometry().height; - if (IsCurrent) - { - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(51, 51, 51)); - } - else if (hot) - { - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(45, 45, 45)); - } -} - void TabBarTab::OnGeometryChanged() { double x = 0.0; @@ -300,28 +275,28 @@ void TabBarTab::OnGeometryChanged() void TabBarTab::OnMouseMove(const Point& pos) { - if (!hot) + if (GetStyleState().empty()) { - hot = true; - Update(); + SetStyleState("hover"); } } -bool TabBarTab::OnMouseDown(const Point& pos, int key) +bool TabBarTab::OnMouseDown(const Point& pos, InputKey key) { if (OnClick) OnClick(); return true; } -bool TabBarTab::OnMouseUp(const Point& pos, int key) +bool TabBarTab::OnMouseUp(const Point& pos, InputKey key) { return true; } void TabBarTab::OnMouseLeave() { - hot = false; + if (GetStyleState() == "hover") + SetStyleState(""); Update(); } @@ -329,7 +304,7 @@ void TabBarTab::OnMouseLeave() TabWidgetStack::TabWidgetStack(Widget* parent) : Widget(parent) { - SetNoncontentSizes(20.0, 5.0, 20.0, 5.0); + SetStyleClass("tabwidget-stack"); } void TabWidgetStack::SetCurrentWidget(Widget* widget) @@ -347,10 +322,6 @@ void TabWidgetStack::SetCurrentWidget(Widget* widget) } } -void TabWidgetStack::OnPaintFrame(Canvas* canvas) -{ -} - void TabWidgetStack::OnGeometryChanged() { if (CurrentWidget) diff --git a/libraries/ZWidget/src/widgets/textedit/textedit.cpp b/libraries/ZWidget/src/widgets/textedit/textedit.cpp index dca840a1b..338efb512 100644 --- a/libraries/ZWidget/src/widgets/textedit/textedit.cpp +++ b/libraries/ZWidget/src/widgets/textedit/textedit.cpp @@ -3,6 +3,7 @@ #include "widgets/scrollbar/scrollbar.h" #include "core/utf8reader.h" #include "core/colorf.h" +#include #ifdef _MSC_VER #pragma warning(disable: 4267) // warning C4267: 'initializing': conversion from 'size_t' to 'int', possible loss of data @@ -10,7 +11,7 @@ TextEdit::TextEdit(Widget* parent) : Widget(parent) { - SetNoncontentSizes(8.0, 8.0, 8.0, 8.0); + SetStyleClass("textedit"); timer = new Timer(this); timer->FuncExpired = [=]() { OnTimerExpired(); }; @@ -286,11 +287,11 @@ void TextEdit::OnMouseMove(const Point& pos) } } -bool TextEdit::OnMouseDown(const Point& pos, int key) +bool TextEdit::OnMouseDown(const Point& pos, InputKey key) { - if (key == IK_LeftMouse) + if (key == InputKey::LeftMouse) { - CaptureMouse(); + SetPointerCapture(); mouse_selecting = true; cursor_pos = GetCharacterIndex(pos); selection_start = cursor_pos; @@ -301,25 +302,25 @@ bool TextEdit::OnMouseDown(const Point& pos, int key) return true; } -bool TextEdit::OnMouseDoubleclick(const Point& pos, int key) +bool TextEdit::OnMouseDoubleclick(const Point& pos, InputKey key) { return true; } -bool TextEdit::OnMouseUp(const Point& pos, int key) +bool TextEdit::OnMouseUp(const Point& pos, InputKey key) { - if (mouse_selecting && key == IK_LeftMouse) + if (mouse_selecting && key == InputKey::LeftMouse) { if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained. { - ReleaseMouseCapture(); + ReleasePointerCapture(); ignore_mouse_events = false; mouse_selecting = false; } else { scroll_timer->Stop(); - ReleaseMouseCapture(); + ReleasePointerCapture(); mouse_selecting = false; ivec2 sel_end = GetCharacterIndex(pos); selection_length = ToOffset(sel_end) - ToOffset(selection_start); @@ -360,9 +361,9 @@ void TextEdit::OnKeyChar(std::string chars) } } -void TextEdit::OnKeyDown(EInputKey key) +void TextEdit::OnKeyDown(InputKey key) { - if (!readonly && key == IK_Enter) + if (!readonly && key == InputKey::Enter) { if (FuncEnterPressed) { @@ -383,17 +384,17 @@ void TextEdit::OnKeyDown(EInputKey key) timer->Start(500); // don't blink cursor when moving or typing. } - if (key == IK_Enter || key == IK_Escape || key == IK_Tab) + if (key == InputKey::Enter || key == InputKey::Escape || key == InputKey::Tab) { // Do not consume these. return; } - else if (key == IK_A && GetKeyState(IK_Ctrl)) + else if (key == InputKey::A && GetKeyState(InputKey::Ctrl)) { // select all SelectAll(); } - else if (key == IK_C && GetKeyState(IK_Ctrl)) + else if (key == InputKey::C && GetKeyState(InputKey::Ctrl)) { std::string str = GetSelection(); SetClipboardText(str); @@ -403,9 +404,9 @@ void TextEdit::OnKeyDown(EInputKey key) // Do not consume messages on read only component (only allow CTRL-A and CTRL-C) return; } - else if (key == IK_Up) + else if (key == InputKey::Up) { - if (GetKeyState(IK_Shift) && selection_length == 0) + if (GetKeyState(InputKey::Shift) && selection_length == 0) selection_start = cursor_pos; if (cursor_pos.y > 0) @@ -414,7 +415,7 @@ void TextEdit::OnKeyDown(EInputKey key) cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x); } - if (GetKeyState(IK_Shift)) + if (GetKeyState(InputKey::Shift)) { selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); } @@ -428,9 +429,9 @@ void TextEdit::OnKeyDown(EInputKey key) Update(); undo_info.first_text_insert = true; } - else if (key == IK_Down) + else if (key == InputKey::Down) { - if (GetKeyState(IK_Shift) && selection_length == 0) + if (GetKeyState(InputKey::Shift) && selection_length == 0) selection_start = cursor_pos; if (cursor_pos.y < lines.size() - 1) @@ -439,7 +440,7 @@ void TextEdit::OnKeyDown(EInputKey key) cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x); } - if (GetKeyState(IK_Shift)) + if (GetKeyState(InputKey::Shift)) { selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); } @@ -454,55 +455,55 @@ void TextEdit::OnKeyDown(EInputKey key) Update(); undo_info.first_text_insert = true; } - else if (key == IK_Left) + else if (key == InputKey::Left) { - Move(-1, GetKeyState(IK_Shift), GetKeyState(IK_Ctrl)); + Move(-1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl)); } - else if (key == IK_Right) + else if (key == InputKey::Right) { - Move(1, GetKeyState(IK_Shift), GetKeyState(IK_Ctrl)); + Move(1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl)); } - else if (key == IK_Backspace) + else if (key == InputKey::Backspace) { Backspace(); } - else if (key == IK_Delete) + else if (key == InputKey::Delete) { Del(); } - else if (key == IK_Home) + else if (key == InputKey::Home) { - if (GetKeyState(IK_Ctrl)) + if (GetKeyState(InputKey::Ctrl)) cursor_pos = ivec2(0, 0); else cursor_pos.x = 0; - if (GetKeyState(IK_Shift)) + if (GetKeyState(InputKey::Shift)) selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); else ClearSelection(); Update(); MoveVerticalScroll(); } - else if (key == IK_End) + else if (key == InputKey::End) { - if (GetKeyState(IK_Ctrl)) + if (GetKeyState(InputKey::Ctrl)) cursor_pos = ivec2(lines.back().text.length(), lines.size() - 1); else cursor_pos.x = lines[cursor_pos.y].text.size(); - if (GetKeyState(IK_Shift)) + if (GetKeyState(InputKey::Shift)) selection_length = ToOffset(cursor_pos) - ToOffset(selection_start); else ClearSelection(); Update(); } - else if (key == IK_X && GetKeyState(IK_Ctrl)) + else if (key == InputKey::X && GetKeyState(InputKey::Ctrl)) { std::string str = GetSelection(); DeleteSelectedText(); SetClipboardText(str); } - else if (key == IK_V && GetKeyState(IK_Ctrl)) + else if (key == InputKey::V && GetKeyState(InputKey::Ctrl)) { std::string str = GetClipboardText(); std::string::const_iterator end_str = std::remove(str.begin(), str.end(), '\r'); @@ -524,7 +525,7 @@ void TextEdit::OnKeyDown(EInputKey key) } MoveVerticalScroll(); } - else if (GetKeyState(IK_Ctrl) && key == IK_Z) + else if (GetKeyState(InputKey::Ctrl) && key == InputKey::Z) { if (!readonly) { @@ -533,7 +534,7 @@ void TextEdit::OnKeyDown(EInputKey key) SetText(tmp); } } - else if (key == IK_Shift) + else if (key == InputKey::Shift) { if (selection_length == 0) selection_start = cursor_pos; @@ -543,7 +544,7 @@ void TextEdit::OnKeyDown(EInputKey key) FuncAfterEditChanged(); } -void TextEdit::OnKeyUp(EInputKey key) +void TextEdit::OnKeyUp(InputKey key) { } @@ -961,17 +962,18 @@ void TextEdit::LayoutLines(Canvas* canvas) sel_end = selection_start; } + Colorf textColor = GetStyleColor("color"); Point draw_pos; - for (size_t i = vert_scrollbar->GetPosition(); i < lines.size(); i++) + for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++) { Line& line = lines[i]; if (line.invalidated) { line.layout.Clear(); if (!line.text.empty()) - line.layout.AddText(line.text, font, Colorf::fromRgba8(255, 255, 255)); + line.layout.AddText(line.text, font, textColor); else - line.layout.AddText(" ", font, Colorf::fromRgba8(255, 255, 255)); // Draw one space character to get the correct height + line.layout.AddText(" ", font, textColor); // Draw one space character to get the correct height line.layout.Layout(canvas, GetWidth()); line.box = Rect(draw_pos, line.layout.GetSize()); line.invalidated = false; @@ -992,7 +994,7 @@ void TextEdit::LayoutLines(Canvas* canvas) if (cursor_blink_visible && cursor_pos.y == i) { line.layout.SetCursorPos(cursor_pos.x); - line.layout.SetCursorColor(Colorf::fromRgba8(255, 255, 255)); + line.layout.SetCursorColor(textColor); line.layout.ShowCursor(); } } @@ -1006,22 +1008,10 @@ void TextEdit::LayoutLines(Canvas* canvas) UpdateVerticalScroll(); } -void TextEdit::OnPaintFrame(Canvas* canvas) -{ - double w = GetFrameGeometry().width; - double h = GetFrameGeometry().height; - Colorf bordercolor = Colorf::fromRgba8(100, 100, 100); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, h), Colorf::fromRgba8(38, 38, 38)); - canvas->fillRect(Rect::xywh(0.0, 0.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, h - 1.0, w, 1.0), bordercolor); - canvas->fillRect(Rect::xywh(0.0, 0.0, 1.0, h - 0.0), bordercolor); - canvas->fillRect(Rect::xywh(w - 1.0, 0.0, 1.0, h - 0.0), bordercolor); -} - void TextEdit::OnPaint(Canvas* canvas) { LayoutLines(canvas); - for (size_t i = vert_scrollbar->GetPosition(); i < lines.size(); i++) + for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++) lines[i].layout.DrawLayout(canvas); } diff --git a/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp b/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp index 55518f0a4..aa6e7e739 100644 --- a/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp +++ b/libraries/ZWidget/src/widgets/textlabel/textlabel.cpp @@ -56,5 +56,5 @@ void TextLabel::OnPaint(Canvas* canvas) x = GetWidth() - canvas->measureText(text).width; } - canvas->drawText(Point(x, GetHeight() - 5.0), Colorf::fromRgba8(255, 255, 255), text); + canvas->drawText(Point(x, GetHeight() - 5.0), GetStyleColor("color"), text); } diff --git a/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp b/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp index d028dd2d2..4629f3a1e 100644 --- a/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp +++ b/libraries/ZWidget/src/widgets/toolbar/toolbar.cpp @@ -1,10 +1,63 @@ #include "widgets/toolbar/toolbar.h" +#include "widgets/toolbar/toolbarbutton.h" Toolbar::Toolbar(Widget* parent) : Widget(parent) { + SetStyleClass("toolbar"); } Toolbar::~Toolbar() { } + +void Toolbar::SetDirection(ToolbarDirection newDirection) +{ + if (direction != newDirection) + { + direction = newDirection; + Update(); + } +} + +ToolbarButton* Toolbar::AddButton(std::string icon, std::string text, std::function onClicked) +{ + ToolbarButton* button = new ToolbarButton(this); + if (!icon.empty()) + button->SetIcon(icon); + if (!text.empty()) + button->SetText(text); + button->OnClick = std::move(onClicked); + buttons.push_back(button); + return button; +} + +void Toolbar::OnGeometryChanged() +{ + if (direction == ToolbarDirection::Horizontal) + { + double x = 7.0; + double barHeight = GetHeight(); + double gap = 7.0; + for (ToolbarButton* button : buttons) + { + double width = button->GetPreferredWidth(); + double height = button->GetPreferredHeight(); + button->SetFrameGeometry(Rect::xywh(x, (barHeight - height) * 0.5, width, height)); + x += width + gap; + } + } + else + { + double y = 7.0; + double barWidth = GetWidth(); + double gap = 7.0; + for (ToolbarButton* button : buttons) + { + double width = button->GetPreferredWidth(); + double height = button->GetPreferredHeight(); + button->SetFrameGeometry(Rect::xywh((barWidth - width) * 0.5, y, width, height)); + y += height + gap; + } + } +} diff --git a/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp b/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp index adb6d08bf..5eda9a13d 100644 --- a/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp +++ b/libraries/ZWidget/src/widgets/toolbar/toolbarbutton.cpp @@ -3,12 +3,112 @@ ToolbarButton::ToolbarButton(Widget* parent) : Widget(parent) { + SetStyleClass("toolbarbutton"); } ToolbarButton::~ToolbarButton() { } -void ToolbarButton::OnPaint(Canvas* canvas) +void ToolbarButton::SetIcon(std::string icon) { + if (!icon.empty()) + { + if (!image) + image = new ImageBox(this); + image->SetImage(Image::LoadResource(icon, GetDpiScale())); + image->SetImageMode(ImageBoxMode::Contain); + } + else + { + delete image; + image = nullptr; + } +} + +void ToolbarButton::SetText(std::string text) +{ + if (!text.empty()) + { + if (!label) + label = new TextLabel(this); + label->SetText(text); + } + else + { + delete label; + label = nullptr; + } +} + +void ToolbarButton::Click() +{ + if (OnClick) + OnClick(); +} + +double ToolbarButton::GetPreferredWidth() +{ + double w = 0.0; + if (image) + w = 26.0; + if (label) + w += label->GetPreferredWidth(); + return w; +} + +double ToolbarButton::GetPreferredHeight() +{ + double h = 0.0; + if (image) + h = 24.0; + if (label) + h = std::max(h, label->GetPreferredHeight()); + return h; +} + +void ToolbarButton::OnMouseMove(const Point& pos) +{ + if (GetStyleState().empty()) + { + SetStyleState("hover"); + } +} + +void ToolbarButton::OnMouseLeave() +{ + SetStyleState(""); +} + +bool ToolbarButton::OnMouseDown(const Point& pos, InputKey key) +{ + SetStyleState("down"); + return true; +} + +bool ToolbarButton::OnMouseUp(const Point& pos, InputKey key) +{ + if (GetStyleState() == "down") + { + SetStyleState(""); + Repaint(); + Click(); + } + return true; +} + +void ToolbarButton::OnGeometryChanged() +{ + double totalHeight = GetPreferredHeight(); + double x = 0.0; + if (image) + { + image->SetFrameGeometry(Rect::xywh(0.0, (totalHeight - 24.0) * 0.5, 24.0, 24.0)); + x += 26.0; + } + if (label) + { + double labelHeight = label->GetPreferredHeight(); + label->SetFrameGeometry(Rect::xywh(x, (totalHeight - labelHeight) * 0.5, GetWidth() - x, labelHeight)); + } } diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp new file mode 100644 index 000000000..1ebc0668f --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.cpp @@ -0,0 +1,288 @@ + +#include "dbus_open_file_dialog.h" +#include + +DBusOpenFileDialog::DBusOpenFileDialog(std::string ownerHandle) : ownerHandle(ownerHandle) +{ +} + +std::string DBusOpenFileDialog::Filename() const +{ + return outputFilenames.empty() ? std::string() : outputFilenames.front(); +} + +std::vector DBusOpenFileDialog::Filenames() const +{ + return outputFilenames; +} + +void DBusOpenFileDialog::SetMultiSelect(bool multiselect) +{ + this->multiSelect = multiSelect; +} + +void DBusOpenFileDialog::SetFilename(const std::string &filename) +{ + inputFilename = filename; +} + +void DBusOpenFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultExt = extension; +} + +void DBusOpenFileDialog::AddFilter(const std::string &filter_description, const std::string &filter_extension) +{ + filters.push_back({ filter_description, filter_extension }); +} + +void DBusOpenFileDialog::ClearFilters() +{ + filters.clear(); +} + +void DBusOpenFileDialog::SetFilterIndex(int filter_index) +{ + this->filter_index = filter_index; +} + +void DBusOpenFileDialog::SetInitialDirectory(const std::string &path) +{ + initialDirectory = path; +} + +void DBusOpenFileDialog::SetTitle(const std::string &title) +{ + this->title = title; +} + +bool DBusOpenFileDialog::Show() +{ + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html + + dbus_bool_t bresult = {}; + DBusError error = {}; + dbus_error_init(&error); + + DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (!connection) + { + dbus_error_free(&error); + return false; + } + + std::string busname = dbus_bus_get_unique_name(connection); + + DBusMessage* request = dbus_message_new_method_call("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.FileChooser", "OpenFile"); + if (!request) + { + dbus_connection_unref(connection); + dbus_error_free(&error); + return false; + } + + const char* parentWindow = ownerHandle.c_str(); + const char* title = this->title.c_str(); + + DBusMessageIter requestArgs = {}; + dbus_message_iter_init_append(request, &requestArgs); + + bresult = dbus_message_iter_append_basic(&requestArgs, DBUS_TYPE_STRING, &parentWindow); + bresult = dbus_message_iter_append_basic(&requestArgs, DBUS_TYPE_STRING, &title); + + DBusMessageIter requestOptions = {}; + bresult = dbus_message_iter_open_container(&requestArgs, DBUS_TYPE_ARRAY, "{sv}", &requestOptions); + + // handle_token - s race condition prevention for signal (/org/freedesktop/portal/desktop/request/SENDER/TOKEN) + // accept_label - s text label for the OK button + // modal - b makes dialog modal. Defaults to true. Not really sure what it means since nothing stayed modal on my computer + // directory - b open folder mode, added in version 3 + // filters - a(sa(us)) [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] + // current_filter - sa(us) filter from filters that should be current filter + // choices - a(ssa(ss)s) list of serialized combo boxes to add to the file chooser + // current_name - s suggested name + + // current_folder - ay + if (!initialDirectory.empty()) + { + // Note: the docs unfortunately says "The portal implementation is free to ignore this option" + + const char* key = "current_folder"; + const char* value = initialDirectory.c_str(); + int valueCount = (int)initialDirectory.size() + 1; + + DBusMessageIter entry = {}; + bresult = dbus_message_iter_open_container(&requestOptions, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + bresult = dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key); + + DBusMessageIter variant = {}; + DBusMessageIter array = {}; + bresult = dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "ay", &variant); + bresult = dbus_message_iter_open_container(&variant, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE_AS_STRING, &array); + bresult = dbus_message_iter_append_fixed_array(&array, DBUS_TYPE_BYTE, &value, valueCount); + bresult = dbus_message_iter_close_container(&variant, &array); + bresult = dbus_message_iter_close_container(&entry, &variant); + + bresult = dbus_message_iter_close_container(&requestOptions, &entry); + } + + // multiple - b + { + const char* key = "multiple"; + dbus_bool_t value = multiSelect; + + DBusMessageIter entry = {}; + bresult = dbus_message_iter_open_container(&requestOptions, DBUS_TYPE_DICT_ENTRY, nullptr, &entry); + bresult = dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key); + + DBusMessageIter variant = {}; + bresult = dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, DBUS_TYPE_BOOLEAN_AS_STRING, &variant); + bresult = dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &value); + bresult = dbus_message_iter_close_container(&entry, &variant); + + bresult = dbus_message_iter_close_container(&requestOptions, &entry); + } + + bresult = dbus_message_iter_close_container(&requestArgs, &requestOptions); + + DBusMessage* response = dbus_connection_send_with_reply_and_block(connection, request, DBUS_TIMEOUT_USE_DEFAULT, &error); + if (!response) + { + dbus_message_unref(request); + dbus_connection_unref(connection); + dbus_error_free(&error); + return false; + } + + const char* handle = nullptr; + bresult = dbus_message_get_args(response, &error, DBUS_TYPE_OBJECT_PATH, &handle, DBUS_TYPE_INVALID); + if (!bresult) + { + dbus_message_unref(response); + dbus_message_unref(request); + dbus_connection_unref(connection); + dbus_error_free(&error); + return false; + } + + std::string signalObjectPath = handle; + + dbus_message_unref(response); + dbus_message_unref(request); + + std::string rule = "type='signal',interface='org.freedesktop.portal.Request',member='Response',path='" + signalObjectPath + "'"; + dbus_bus_add_match(connection, rule.c_str(), &error); + + // Wait for the response signal + // + // To do: process the run loop while we wait + // + DBusMessage* signalmsg = nullptr; + while (!signalmsg && dbus_connection_read_write(connection, -1)) + { + while (true) + { + DBusMessage* message = dbus_connection_pop_message(connection); + if (!message) + break; + + if (dbus_message_is_signal(message, "org.freedesktop.portal.Request", "Response") && dbus_message_get_path(message) == signalObjectPath) + { + signalmsg = message; + break; + } + else + { + dbus_message_unref(message); + } + } + } + dbus_bus_remove_match(connection, rule.c_str(), &error); + + // Read the response + + dbus_uint32_t responseCode = 0; + std::vector uris; + + DBusMessageIter signalArgs; + bresult = dbus_message_iter_init(signalmsg, &signalArgs); + + // response code - u + if (dbus_message_iter_get_arg_type(&signalArgs) == DBUS_TYPE_UINT32) + { + dbus_message_iter_get_basic(&signalArgs, &responseCode); + } + dbus_message_iter_next(&signalArgs); + + // results - a{sv} + if (dbus_message_iter_get_arg_type(&signalArgs) == DBUS_TYPE_ARRAY) + { + DBusMessageIter resultsArray = {}; + dbus_message_iter_recurse(&signalArgs, &resultsArray); + while (true) + { + int type = dbus_message_iter_get_arg_type(&resultsArray); + if (type != DBUS_TYPE_DICT_ENTRY) + break; + + DBusMessageIter entry = {}; + dbus_message_iter_recurse(&resultsArray, &entry); + + const char* key = nullptr; + dbus_message_iter_get_basic(&entry, &key); + dbus_message_iter_next(&entry); + + DBusMessageIter value = {}; + dbus_message_iter_recurse(&entry, &value); + + std::string k = key; + if (k == "uris" && dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_ARRAY) // as + { + DBusMessageIter uriArray = {}; + dbus_message_iter_recurse(&value, &uriArray); + while (true) + { + int type = dbus_message_iter_get_arg_type(&uriArray); + if (type != DBUS_TYPE_STRING) + break; + + const char* uri = nullptr; + dbus_message_iter_get_basic(&uriArray, &uri); + uris.push_back(uri); + + dbus_message_iter_next(&uriArray); + } + } + else if (k == "choices" && dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_ARRAY) // a(ss) + { + // An array of pairs of strings, + // the first string being the ID of a combobox that was passed into this call, + // the second string being the selected option. + } + else if (k == "current_filter" && dbus_message_iter_get_arg_type(&value) == DBUS_TYPE_STRUCT) // sa(us) + { + // The filter that was selected. + // This may match a filter in the filter list or another filter that was applied unconditionally. + } + + dbus_message_iter_next(&entry); + dbus_message_iter_next(&resultsArray); + } + } + dbus_message_iter_next(&signalArgs); + + dbus_message_unref(signalmsg); + dbus_connection_unref(connection); + dbus_error_free(&error); + + if (responseCode != 0) // User cancelled + return false; + + for (const std::string& uri : uris) + { + if (uri.size() > 7 && uri.substr(0, 7) == "file://") + outputFilenames.push_back(uri.substr(7)); + } + + return !uris.empty(); +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h new file mode 100644 index 000000000..f4efd2663 --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_file_dialog.h @@ -0,0 +1,38 @@ +#pragma once + +#include "systemdialogs/open_file_dialog.h" + +class DBusOpenFileDialog : public OpenFileDialog +{ +public: + DBusOpenFileDialog(std::string ownerHandle); + + std::string Filename() const override; + std::vector Filenames() const override; + void SetMultiSelect(bool multiselect) override; + void SetFilename(const std::string &filename) override; + void SetDefaultExtension(const std::string& extension) override; + void AddFilter(const std::string &filter_description, const std::string &filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string &path) override; + void SetTitle(const std::string& newtitle) override; + bool Show() override; + +private: + std::string ownerHandle; + std::string title; + std::string initialDirectory; + std::string inputFilename; + std::string defaultExt; + std::vector outputFilenames; + bool multiSelect = false; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filter_index = 0; +}; diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp new file mode 100644 index 000000000..7bcd94953 --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.cpp @@ -0,0 +1,26 @@ + +#include "dbus_open_folder_dialog.h" + +DBusOpenFolderDialog::DBusOpenFolderDialog(std::string ownerHandle) : ownerHandle(ownerHandle) +{ +} + +bool DBusOpenFolderDialog::Show() +{ + return false; +} + +std::string DBusOpenFolderDialog::SelectedPath() const +{ + return selected_path; +} + +void DBusOpenFolderDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void DBusOpenFolderDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h new file mode 100644 index 000000000..3e40cc7bc --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_open_folder_dialog.h @@ -0,0 +1,21 @@ +#pragma once + +#include "systemdialogs/open_folder_dialog.h" + +class DBusOpenFolderDialog : public OpenFolderDialog +{ +public: + DBusOpenFolderDialog(std::string ownerHandle); + + bool Show() override; + std::string SelectedPath() const override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + +private: + std::string ownerHandle; + + std::string selected_path; + std::string initial_directory; + std::string title; +}; diff --git a/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp new file mode 100644 index 000000000..ba6b6d1f4 --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.cpp @@ -0,0 +1,54 @@ + +#include "dbus_save_file_dialog.h" + +DBusSaveFileDialog::DBusSaveFileDialog(std::string ownerHandle) : ownerHandle(ownerHandle) +{ +} + +bool DBusSaveFileDialog::Show() +{ + return false; +} + +std::string DBusSaveFileDialog::Filename() const +{ + return filename; +} + +void DBusSaveFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void DBusSaveFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void DBusSaveFileDialog::ClearFilters() +{ + filters.clear(); +} + +void DBusSaveFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void DBusSaveFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void DBusSaveFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void DBusSaveFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} diff --git a/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h new file mode 100644 index 000000000..835789b2c --- /dev/null +++ b/libraries/ZWidget/src/window/dbus/dbus_save_file_dialog.h @@ -0,0 +1,37 @@ +#pragma once + +#include "systemdialogs/save_file_dialog.h" + +class DBusSaveFileDialog : public SaveFileDialog +{ +public: + DBusSaveFileDialog(std::string ownerHandle); + + bool Show() override; + std::string Filename() const override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + std::string ownerHandle; + + std::string filename; + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp new file mode 100644 index 000000000..a5b40f743 --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.cpp @@ -0,0 +1,38 @@ + +#include "sdl2_display_backend.h" +#include "sdl2_display_window.h" + +std::unique_ptr SDL2DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void SDL2DisplayBackend::ProcessEvents() +{ + SDL2DisplayWindow::ProcessEvents(); +} + +void SDL2DisplayBackend::RunLoop() +{ + SDL2DisplayWindow::RunLoop(); +} + +void SDL2DisplayBackend::ExitLoop() +{ + SDL2DisplayWindow::ExitLoop(); +} + +Size SDL2DisplayBackend::GetScreenSize() +{ + return SDL2DisplayWindow::GetScreenSize(); +} + +void* SDL2DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return SDL2DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void SDL2DisplayBackend::StopTimer(void* timerID) +{ + SDL2DisplayWindow::StopTimer(timerID); +} diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h new file mode 100644 index 000000000..e80b8352e --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_backend.h @@ -0,0 +1,19 @@ +#pragma once + +#include "window/window.h" + +class SDL2DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + bool IsSDL2() override { return true; } +}; diff --git a/libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp new file mode 100644 index 000000000..b5d2ffad5 --- /dev/null +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.cpp @@ -0,0 +1,772 @@ + +#include "sdl2_display_window.h" +#include +#include + +Uint32 SDL2DisplayWindow::PaintEventNumber = 0xffffffff; +bool SDL2DisplayWindow::ExitRunLoop; +std::unordered_map SDL2DisplayWindow::WindowList; + +class InitSDL +{ +public: + InitSDL() + { + int result = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); + if (result != 0) + throw std::runtime_error(std::string("Unable to initialize SDL:") + SDL_GetError()); + + SDL2DisplayWindow::PaintEventNumber = SDL_RegisterEvents(1); + } +}; + +static void CheckInitSDL() +{ + static InitSDL initsdl; +} + +SDL2DisplayWindow::SDL2DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, SDL2DisplayWindow* owner, RenderAPI renderAPI) : WindowHost(windowHost) +{ + CheckInitSDL(); + + unsigned int flags = SDL_WINDOW_HIDDEN /*| SDL_WINDOW_ALLOW_HIGHDPI*/; + if (renderAPI == RenderAPI::Vulkan) + flags |= SDL_WINDOW_VULKAN; + else if (renderAPI == RenderAPI::OpenGL) + flags |= SDL_WINDOW_OPENGL; +#if defined(__APPLE__) + else if (renderAPI == RenderAPI::Metal) + flags |= SDL_WINDOW_METAL; +#endif + if (popupWindow) + flags |= SDL_WINDOW_BORDERLESS; + + if (renderAPI == RenderAPI::Vulkan || renderAPI == RenderAPI::OpenGL || renderAPI == RenderAPI::Metal) + { + Handle.window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 320, 200, flags); + if (!Handle.window) + throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); + } + else + { + int result = SDL_CreateWindowAndRenderer(320, 200, flags, &Handle.window, &RendererHandle); + if (result != 0) + throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); + } + + WindowList[SDL_GetWindowID(Handle.window)] = this; +} + +SDL2DisplayWindow::~SDL2DisplayWindow() +{ + UnlockCursor(); + + WindowList.erase(WindowList.find(SDL_GetWindowID(Handle.window))); + + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + if (RendererHandle) + SDL_DestroyRenderer(RendererHandle); + SDL_DestroyWindow(Handle.window); + RendererHandle = nullptr; + Handle.window = nullptr; +} + +std::vector SDL2DisplayWindow::GetVulkanInstanceExtensions() +{ + unsigned int extCount = 0; + SDL_Vulkan_GetInstanceExtensions(Handle.window, &extCount, nullptr); + std::vector extNames(extCount); + SDL_Vulkan_GetInstanceExtensions(Handle.window, &extCount, extNames.data()); + + std::vector result; + result.reserve(extNames.size()); + for (const char* ext : extNames) + result.emplace_back(ext); + return result; +} + +VkSurfaceKHR SDL2DisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + VkSurfaceKHR surfaceHandle = {}; + SDL_Vulkan_CreateSurface(Handle.window, instance, &surfaceHandle); + if (surfaceHandle) + throw std::runtime_error("Could not create vulkan surface"); + return surfaceHandle; +} + +void SDL2DisplayWindow::SetWindowTitle(const std::string& text) +{ + SDL_SetWindowTitle(Handle.window, text.c_str()); +} + +void SDL2DisplayWindow::SetWindowFrame(const Rect& box) +{ + // SDL2 doesn't really seem to have an API for this. + // The docs aren't clear what you're setting when calling SDL_SetWindowSize. + SetClientFrame(box); +} + +void SDL2DisplayWindow::SetClientFrame(const Rect& box) +{ + // Is there a way to set both in one call? + + double uiscale = GetDpiScale(); + int x = (int)std::round(box.x * uiscale); + int y = (int)std::round(box.y * uiscale); + int w = (int)std::round(box.width * uiscale); + int h = (int)std::round(box.height * uiscale); + + SDL_SetWindowPosition(Handle.window, x, y); + SDL_SetWindowSize(Handle.window, w, h); +} + +void SDL2DisplayWindow::Show() +{ + SDL_ShowWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowFullscreen() +{ + SDL_ShowWindow(Handle.window); + SDL_SetWindowFullscreen(Handle.window, SDL_WINDOW_FULLSCREEN_DESKTOP); + isFullscreen = true; +} + +void SDL2DisplayWindow::ShowMaximized() +{ + SDL_ShowWindow(Handle.window); + SDL_MaximizeWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowMinimized() +{ + SDL_ShowWindow(Handle.window); + SDL_MinimizeWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowNormal() +{ + SDL_ShowWindow(Handle.window); + SDL_SetWindowFullscreen(Handle.window, 0); + isFullscreen = false; +} + +bool SDL2DisplayWindow::IsWindowFullscreen() +{ + return isFullscreen; +} + +void SDL2DisplayWindow::Hide() +{ + SDL_HideWindow(Handle.window); +} + +void SDL2DisplayWindow::Activate() +{ + SDL_RaiseWindow(Handle.window); +} + +void SDL2DisplayWindow::ShowCursor(bool enable) +{ + SDL_ShowCursor(enable); +} + +void SDL2DisplayWindow::LockCursor() +{ + if (!CursorLocked) + { + SDL_SetRelativeMouseMode(SDL_TRUE); + CursorLocked = true; + } +} + +void SDL2DisplayWindow::UnlockCursor() +{ + if (CursorLocked) + { + SDL_SetRelativeMouseMode(SDL_FALSE); + CursorLocked = false; + } +} + +void SDL2DisplayWindow::CaptureMouse() +{ +} + +void SDL2DisplayWindow::ReleaseMouseCapture() +{ +} + +void SDL2DisplayWindow::SetCursor(StandardCursor cursor) +{ +} + +void SDL2DisplayWindow::Update() +{ + SDL_Event event = {}; + event.type = PaintEventNumber; + event.user.windowID = SDL_GetWindowID(Handle.window); + SDL_PushEvent(&event); +} + +bool SDL2DisplayWindow::GetKeyState(InputKey key) +{ + int numkeys = 0; + const Uint8* state = SDL_GetKeyboardState(&numkeys); + if (!state) return false; + + SDL_Scancode index = InputKeyToScancode(key); + return (index < numkeys) ? state[index] != 0 : false; +} + +Rect SDL2DisplayWindow::GetWindowFrame() const +{ + int x = 0; + int y = 0; + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(Handle.window, &x, &y); + SDL_GetWindowSize(Handle.window, &w, &h); + return Rect::xywh(x / uiscale, y / uiscale, w / uiscale, h / uiscale); +} + +Point SDL2DisplayWindow::MapFromGlobal(const Point& pos) const +{ + int x = 0; + int y = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(Handle.window, &x, &y); + return Point(pos.x - x / uiscale, pos.y - y / uiscale); +} + +Point SDL2DisplayWindow::MapToGlobal(const Point& pos) const +{ + int x = 0; + int y = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowPosition(Handle.window, &x, &y); + return Point(pos.x + x / uiscale, pos.y + y / uiscale); +} + +Size SDL2DisplayWindow::GetClientSize() const +{ + int w = 0; + int h = 0; + double uiscale = GetDpiScale(); + SDL_GetWindowSize(Handle.window, &w, &h); + return Size(w / uiscale, h / uiscale); +} + +int SDL2DisplayWindow::GetPixelWidth() const +{ + int w = 0; + int h = 0; + if (RendererHandle) + SDL_GetRendererOutputSize(RendererHandle, &w, &h); + else + SDL_GL_GetDrawableSize(Handle.window, &w, &h); + return w; +} + +int SDL2DisplayWindow::GetPixelHeight() const +{ + int w = 0; + int h = 0; + if (RendererHandle) + SDL_GetRendererOutputSize(RendererHandle, &w, &h); + else + SDL_GL_GetDrawableSize(Handle.window, &w, &h); + return h; +} + +double SDL2DisplayWindow::GetDpiScale() const +{ + // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + return 1.0; +} + +void SDL2DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + if (!RendererHandle) + return; + + if (!BackBufferTexture || BackBufferWidth != width || BackBufferHeight != height) + { + if (BackBufferTexture) + { + SDL_DestroyTexture(BackBufferTexture); + BackBufferTexture = nullptr; + } + + BackBufferTexture = SDL_CreateTexture(RendererHandle, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); + if (!BackBufferTexture) + return; + + BackBufferWidth = width; + BackBufferHeight = height; + } + + int destpitch = 0; + void* dest = nullptr; + int result = SDL_LockTexture(BackBufferTexture, nullptr, &dest, &destpitch); + if (result != 0) return; + for (int y = 0; y < height; y++) + { + const void* sline = pixels + y * width; + void* dline = (uint8_t*)dest + y * destpitch; + memcpy(dline, sline, width << 2); + } + SDL_UnlockTexture(BackBufferTexture); + + SDL_RenderCopy(RendererHandle, BackBufferTexture, nullptr, nullptr); + SDL_RenderPresent(RendererHandle); +} + +void SDL2DisplayWindow::SetBorderColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +void SDL2DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + // SDL doesn't have this +} + +std::string SDL2DisplayWindow::GetClipboardText() +{ + char* buffer = SDL_GetClipboardText(); + if (!buffer) + return {}; + std::string text = buffer; + SDL_free(buffer); + return text; +} + +void SDL2DisplayWindow::SetClipboardText(const std::string& text) +{ + SDL_SetClipboardText(text.c_str()); +} + +void SDL2DisplayWindow::ProcessEvents() +{ + CheckInitSDL(); + + SDL_Event event; + while (SDL_PollEvent(&event) != 0) + { + DispatchEvent(event); + } +} + +void SDL2DisplayWindow::RunLoop() +{ + CheckInitSDL(); + + ExitRunLoop = false; + while (!ExitRunLoop) + { + SDL_Event event = {}; + int result = SDL_WaitEvent(&event); + if (result == 1) + DispatchEvent(event); // Silently ignore if it fails and pray it doesn't busy loop, because SDL and Linux utterly sucks! + } +} + +void SDL2DisplayWindow::ExitLoop() +{ + CheckInitSDL(); + + ExitRunLoop = true; +} + +Size SDL2DisplayWindow::GetScreenSize() +{ + CheckInitSDL(); + + SDL_Rect rect = {}; + int result = SDL_GetDisplayBounds(0, &rect); + if (result != 0) + throw std::runtime_error(std::string("Unable to get screen size:") + SDL_GetError()); + + double uiscale = 1.0; // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. + return Size(rect.w / uiscale, rect.h / uiscale); +} + +void* SDL2DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + CheckInitSDL(); + + // To do: implement timers + + return nullptr; +} + +void SDL2DisplayWindow::StopTimer(void* timerID) +{ + CheckInitSDL(); + + // To do: implement timers +} + +SDL2DisplayWindow* SDL2DisplayWindow::FindEventWindow(const SDL_Event& event) +{ + int windowID; + switch (event.type) + { + case SDL_WINDOWEVENT: windowID = event.window.windowID; break; + case SDL_TEXTINPUT: windowID = event.text.windowID; break; + case SDL_KEYUP: windowID = event.key.windowID; break; + case SDL_KEYDOWN: windowID = event.key.windowID; break; + case SDL_MOUSEBUTTONUP: windowID = event.button.windowID; break; + case SDL_MOUSEBUTTONDOWN: windowID = event.button.windowID; break; + case SDL_MOUSEWHEEL: windowID = event.wheel.windowID; break; + case SDL_MOUSEMOTION: windowID = event.motion.windowID; break; + default: + if (event.type == PaintEventNumber) windowID = event.user.windowID; + else return nullptr; + } + + auto it = WindowList.find(windowID); + return it != WindowList.end() ? it->second : nullptr; +} + +void SDL2DisplayWindow::DispatchEvent(const SDL_Event& event) +{ + SDL2DisplayWindow* window = FindEventWindow(event); + if (!window) return; + + switch (event.type) + { + case SDL_WINDOWEVENT: window->OnWindowEvent(event.window); break; + case SDL_TEXTINPUT: window->OnTextInput(event.text); break; + case SDL_KEYUP: window->OnKeyUp(event.key); break; + case SDL_KEYDOWN: window->OnKeyDown(event.key); break; + case SDL_MOUSEBUTTONUP: window->OnMouseButtonUp(event.button); break; + case SDL_MOUSEBUTTONDOWN: window->OnMouseButtonDown(event.button); break; + case SDL_MOUSEWHEEL: window->OnMouseWheel(event.wheel); break; + case SDL_MOUSEMOTION: window->OnMouseMotion(event.motion); break; + default: + if (event.type == PaintEventNumber) window->OnPaintEvent(); + } +} + +void SDL2DisplayWindow::OnWindowEvent(const SDL_WindowEvent& event) +{ + switch (event.event) + { + case SDL_WINDOWEVENT_CLOSE: WindowHost->OnWindowClose(); break; + case SDL_WINDOWEVENT_MOVED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_RESIZED: WindowHost->OnWindowGeometryChanged(); break; + case SDL_WINDOWEVENT_SHOWN: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_EXPOSED: WindowHost->OnWindowPaint(); break; + case SDL_WINDOWEVENT_FOCUS_GAINED: WindowHost->OnWindowActivated(); break; + case SDL_WINDOWEVENT_FOCUS_LOST: WindowHost->OnWindowDeactivated(); break; + } +} + +void SDL2DisplayWindow::OnTextInput(const SDL_TextInputEvent& event) +{ + WindowHost->OnWindowKeyChar(event.text); +} + +void SDL2DisplayWindow::OnKeyUp(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyUp(ScancodeToInputKey(event.keysym.scancode)); +} + +void SDL2DisplayWindow::OnKeyDown(const SDL_KeyboardEvent& event) +{ + WindowHost->OnWindowKeyDown(ScancodeToInputKey(event.keysym.scancode)); +} + +InputKey SDL2DisplayWindow::GetMouseButtonKey(const SDL_MouseButtonEvent& event) +{ + switch (event.button) + { + case SDL_BUTTON_LEFT: return InputKey::LeftMouse; + case SDL_BUTTON_MIDDLE: return InputKey::MiddleMouse; + case SDL_BUTTON_RIGHT: return InputKey::RightMouse; + // case SDL_BUTTON_X1: return InputKey::XButton1; + // case SDL_BUTTON_X2: return InputKey::XButton2; + default: return InputKey::None; + } +} + +void SDL2DisplayWindow::OnMouseButtonUp(const SDL_MouseButtonEvent& event) +{ + InputKey key = GetMouseButtonKey(event); + if (key != InputKey::None) + { + WindowHost->OnWindowMouseUp(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseButtonDown(const SDL_MouseButtonEvent& event) +{ + InputKey key = GetMouseButtonKey(event); + if (key != InputKey::None) + { + WindowHost->OnWindowMouseDown(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseWheel(const SDL_MouseWheelEvent& event) +{ + InputKey key = (event.y > 0) ? InputKey::MouseWheelUp : (event.y < 0) ? InputKey::MouseWheelDown : InputKey::None; + if (key != InputKey::None) + { + WindowHost->OnWindowMouseWheel(GetMousePos(event), key); + } +} + +void SDL2DisplayWindow::OnMouseMotion(const SDL_MouseMotionEvent& event) +{ + if (CursorLocked) + { + WindowHost->OnWindowRawMouseMove(event.xrel, event.yrel); + } + else + { + WindowHost->OnWindowMouseMove(GetMousePos(event)); + } +} + +void SDL2DisplayWindow::OnPaintEvent() +{ + WindowHost->OnWindowPaint(); +} + +InputKey SDL2DisplayWindow::ScancodeToInputKey(SDL_Scancode keycode) +{ + switch (keycode) + { + case SDL_SCANCODE_BACKSPACE: return InputKey::Backspace; + case SDL_SCANCODE_TAB: return InputKey::Tab; + case SDL_SCANCODE_CLEAR: return InputKey::OEMClear; + case SDL_SCANCODE_RETURN: return InputKey::Enter; + case SDL_SCANCODE_MENU: return InputKey::Alt; + case SDL_SCANCODE_PAUSE: return InputKey::Pause; + case SDL_SCANCODE_ESCAPE: return InputKey::Escape; + case SDL_SCANCODE_SPACE: return InputKey::Space; + case SDL_SCANCODE_END: return InputKey::End; + case SDL_SCANCODE_HOME: return InputKey::Home; + case SDL_SCANCODE_LEFT: return InputKey::Left; + case SDL_SCANCODE_UP: return InputKey::Up; + case SDL_SCANCODE_RIGHT: return InputKey::Right; + case SDL_SCANCODE_DOWN: return InputKey::Down; + case SDL_SCANCODE_SELECT: return InputKey::Select; + case SDL_SCANCODE_PRINTSCREEN: return InputKey::Print; + case SDL_SCANCODE_EXECUTE: return InputKey::Execute; + case SDL_SCANCODE_INSERT: return InputKey::Insert; + case SDL_SCANCODE_DELETE: return InputKey::Delete; + case SDL_SCANCODE_HELP: return InputKey::Help; + case SDL_SCANCODE_0: return InputKey::_0; + case SDL_SCANCODE_1: return InputKey::_1; + case SDL_SCANCODE_2: return InputKey::_2; + case SDL_SCANCODE_3: return InputKey::_3; + case SDL_SCANCODE_4: return InputKey::_4; + case SDL_SCANCODE_5: return InputKey::_5; + case SDL_SCANCODE_6: return InputKey::_6; + case SDL_SCANCODE_7: return InputKey::_7; + case SDL_SCANCODE_8: return InputKey::_8; + case SDL_SCANCODE_9: return InputKey::_9; + case SDL_SCANCODE_A: return InputKey::A; + case SDL_SCANCODE_B: return InputKey::B; + case SDL_SCANCODE_C: return InputKey::C; + case SDL_SCANCODE_D: return InputKey::D; + case SDL_SCANCODE_E: return InputKey::E; + case SDL_SCANCODE_F: return InputKey::F; + case SDL_SCANCODE_G: return InputKey::G; + case SDL_SCANCODE_H: return InputKey::H; + case SDL_SCANCODE_I: return InputKey::I; + case SDL_SCANCODE_J: return InputKey::J; + case SDL_SCANCODE_K: return InputKey::K; + case SDL_SCANCODE_L: return InputKey::L; + case SDL_SCANCODE_M: return InputKey::M; + case SDL_SCANCODE_N: return InputKey::N; + case SDL_SCANCODE_O: return InputKey::O; + case SDL_SCANCODE_P: return InputKey::P; + case SDL_SCANCODE_Q: return InputKey::Q; + case SDL_SCANCODE_R: return InputKey::R; + case SDL_SCANCODE_S: return InputKey::S; + case SDL_SCANCODE_T: return InputKey::T; + case SDL_SCANCODE_U: return InputKey::U; + case SDL_SCANCODE_V: return InputKey::V; + case SDL_SCANCODE_W: return InputKey::W; + case SDL_SCANCODE_X: return InputKey::X; + case SDL_SCANCODE_Y: return InputKey::Y; + case SDL_SCANCODE_Z: return InputKey::Z; + case SDL_SCANCODE_KP_0: return InputKey::NumPad0; + case SDL_SCANCODE_KP_1: return InputKey::NumPad1; + case SDL_SCANCODE_KP_2: return InputKey::NumPad2; + case SDL_SCANCODE_KP_3: return InputKey::NumPad3; + case SDL_SCANCODE_KP_4: return InputKey::NumPad4; + case SDL_SCANCODE_KP_5: return InputKey::NumPad5; + case SDL_SCANCODE_KP_6: return InputKey::NumPad6; + case SDL_SCANCODE_KP_7: return InputKey::NumPad7; + case SDL_SCANCODE_KP_8: return InputKey::NumPad8; + case SDL_SCANCODE_KP_9: return InputKey::NumPad9; + // case SDL_SCANCODE_KP_ENTER: return InputKey::NumPadEnter; + // case SDL_SCANCODE_KP_MULTIPLY: return InputKey::Multiply; + // case SDL_SCANCODE_KP_PLUS: return InputKey::Add; + case SDL_SCANCODE_SEPARATOR: return InputKey::Separator; + // case SDL_SCANCODE_KP_MINUS: return InputKey::Subtract; + case SDL_SCANCODE_KP_PERIOD: return InputKey::NumPadPeriod; + // case SDL_SCANCODE_KP_DIVIDE: return InputKey::Divide; + case SDL_SCANCODE_F1: return InputKey::F1; + case SDL_SCANCODE_F2: return InputKey::F2; + case SDL_SCANCODE_F3: return InputKey::F3; + case SDL_SCANCODE_F4: return InputKey::F4; + case SDL_SCANCODE_F5: return InputKey::F5; + case SDL_SCANCODE_F6: return InputKey::F6; + case SDL_SCANCODE_F7: return InputKey::F7; + case SDL_SCANCODE_F8: return InputKey::F8; + case SDL_SCANCODE_F9: return InputKey::F9; + case SDL_SCANCODE_F10: return InputKey::F10; + case SDL_SCANCODE_F11: return InputKey::F11; + case SDL_SCANCODE_F12: return InputKey::F12; + case SDL_SCANCODE_F13: return InputKey::F13; + case SDL_SCANCODE_F14: return InputKey::F14; + case SDL_SCANCODE_F15: return InputKey::F15; + case SDL_SCANCODE_F16: return InputKey::F16; + case SDL_SCANCODE_F17: return InputKey::F17; + case SDL_SCANCODE_F18: return InputKey::F18; + case SDL_SCANCODE_F19: return InputKey::F19; + case SDL_SCANCODE_F20: return InputKey::F20; + case SDL_SCANCODE_F21: return InputKey::F21; + case SDL_SCANCODE_F22: return InputKey::F22; + case SDL_SCANCODE_F23: return InputKey::F23; + case SDL_SCANCODE_F24: return InputKey::F24; + case SDL_SCANCODE_NUMLOCKCLEAR: return InputKey::NumLock; + case SDL_SCANCODE_SCROLLLOCK: return InputKey::ScrollLock; + case SDL_SCANCODE_LSHIFT: return InputKey::LShift; + case SDL_SCANCODE_RSHIFT: return InputKey::RShift; + case SDL_SCANCODE_LCTRL: return InputKey::LControl; + case SDL_SCANCODE_RCTRL: return InputKey::RControl; + case SDL_SCANCODE_GRAVE: return InputKey::Tilde; + default: return InputKey::None; + } +} + +SDL_Scancode SDL2DisplayWindow::InputKeyToScancode(InputKey inputkey) +{ + switch (inputkey) + { + case InputKey::Backspace: return SDL_SCANCODE_BACKSPACE; + case InputKey::Tab: return SDL_SCANCODE_TAB; + case InputKey::OEMClear: return SDL_SCANCODE_CLEAR; + case InputKey::Enter: return SDL_SCANCODE_RETURN; + case InputKey::Alt: return SDL_SCANCODE_MENU; + case InputKey::Pause: return SDL_SCANCODE_PAUSE; + case InputKey::Escape: return SDL_SCANCODE_ESCAPE; + case InputKey::Space: return SDL_SCANCODE_SPACE; + case InputKey::End: return SDL_SCANCODE_END; + case InputKey::Home: return SDL_SCANCODE_HOME; + case InputKey::Left: return SDL_SCANCODE_LEFT; + case InputKey::Up: return SDL_SCANCODE_UP; + case InputKey::Right: return SDL_SCANCODE_RIGHT; + case InputKey::Down: return SDL_SCANCODE_DOWN; + case InputKey::Select: return SDL_SCANCODE_SELECT; + case InputKey::Print: return SDL_SCANCODE_PRINTSCREEN; + case InputKey::Execute: return SDL_SCANCODE_EXECUTE; + case InputKey::Insert: return SDL_SCANCODE_INSERT; + case InputKey::Delete: return SDL_SCANCODE_DELETE; + case InputKey::Help: return SDL_SCANCODE_HELP; + case InputKey::_0: return SDL_SCANCODE_0; + case InputKey::_1: return SDL_SCANCODE_1; + case InputKey::_2: return SDL_SCANCODE_2; + case InputKey::_3: return SDL_SCANCODE_3; + case InputKey::_4: return SDL_SCANCODE_4; + case InputKey::_5: return SDL_SCANCODE_5; + case InputKey::_6: return SDL_SCANCODE_6; + case InputKey::_7: return SDL_SCANCODE_7; + case InputKey::_8: return SDL_SCANCODE_8; + case InputKey::_9: return SDL_SCANCODE_9; + case InputKey::A: return SDL_SCANCODE_A; + case InputKey::B: return SDL_SCANCODE_B; + case InputKey::C: return SDL_SCANCODE_C; + case InputKey::D: return SDL_SCANCODE_D; + case InputKey::E: return SDL_SCANCODE_E; + case InputKey::F: return SDL_SCANCODE_F; + case InputKey::G: return SDL_SCANCODE_G; + case InputKey::H: return SDL_SCANCODE_H; + case InputKey::I: return SDL_SCANCODE_I; + case InputKey::J: return SDL_SCANCODE_J; + case InputKey::K: return SDL_SCANCODE_K; + case InputKey::L: return SDL_SCANCODE_L; + case InputKey::M: return SDL_SCANCODE_M; + case InputKey::N: return SDL_SCANCODE_N; + case InputKey::O: return SDL_SCANCODE_O; + case InputKey::P: return SDL_SCANCODE_P; + case InputKey::Q: return SDL_SCANCODE_Q; + case InputKey::R: return SDL_SCANCODE_R; + case InputKey::S: return SDL_SCANCODE_S; + case InputKey::T: return SDL_SCANCODE_T; + case InputKey::U: return SDL_SCANCODE_U; + case InputKey::V: return SDL_SCANCODE_V; + case InputKey::W: return SDL_SCANCODE_W; + case InputKey::X: return SDL_SCANCODE_X; + case InputKey::Y: return SDL_SCANCODE_Y; + case InputKey::Z: return SDL_SCANCODE_Z; + case InputKey::NumPad0: return SDL_SCANCODE_KP_0; + case InputKey::NumPad1: return SDL_SCANCODE_KP_1; + case InputKey::NumPad2: return SDL_SCANCODE_KP_2; + case InputKey::NumPad3: return SDL_SCANCODE_KP_3; + case InputKey::NumPad4: return SDL_SCANCODE_KP_4; + case InputKey::NumPad5: return SDL_SCANCODE_KP_5; + case InputKey::NumPad6: return SDL_SCANCODE_KP_6; + case InputKey::NumPad7: return SDL_SCANCODE_KP_7; + case InputKey::NumPad8: return SDL_SCANCODE_KP_8; + case InputKey::NumPad9: return SDL_SCANCODE_KP_9; + // case InputKey::NumPadEnter: return SDL_SCANCODE_KP_ENTER; + // case InputKey::Multiply return SDL_SCANCODE_KP_MULTIPLY:; + // case InputKey::Add: return SDL_SCANCODE_KP_PLUS; + case InputKey::Separator: return SDL_SCANCODE_SEPARATOR; + // case InputKey::Subtract: return SDL_SCANCODE_KP_MINUS; + case InputKey::NumPadPeriod: return SDL_SCANCODE_KP_PERIOD; + // case InputKey::Divide: return SDL_SCANCODE_KP_DIVIDE; + case InputKey::F1: return SDL_SCANCODE_F1; + case InputKey::F2: return SDL_SCANCODE_F2; + case InputKey::F3: return SDL_SCANCODE_F3; + case InputKey::F4: return SDL_SCANCODE_F4; + case InputKey::F5: return SDL_SCANCODE_F5; + case InputKey::F6: return SDL_SCANCODE_F6; + case InputKey::F7: return SDL_SCANCODE_F7; + case InputKey::F8: return SDL_SCANCODE_F8; + case InputKey::F9: return SDL_SCANCODE_F9; + case InputKey::F10: return SDL_SCANCODE_F10; + case InputKey::F11: return SDL_SCANCODE_F11; + case InputKey::F12: return SDL_SCANCODE_F12; + case InputKey::F13: return SDL_SCANCODE_F13; + case InputKey::F14: return SDL_SCANCODE_F14; + case InputKey::F15: return SDL_SCANCODE_F15; + case InputKey::F16: return SDL_SCANCODE_F16; + case InputKey::F17: return SDL_SCANCODE_F17; + case InputKey::F18: return SDL_SCANCODE_F18; + case InputKey::F19: return SDL_SCANCODE_F19; + case InputKey::F20: return SDL_SCANCODE_F20; + case InputKey::F21: return SDL_SCANCODE_F21; + case InputKey::F22: return SDL_SCANCODE_F22; + case InputKey::F23: return SDL_SCANCODE_F23; + case InputKey::F24: return SDL_SCANCODE_F24; + case InputKey::NumLock: return SDL_SCANCODE_NUMLOCKCLEAR; + case InputKey::ScrollLock: return SDL_SCANCODE_SCROLLLOCK; + case InputKey::LShift: return SDL_SCANCODE_LSHIFT; + case InputKey::RShift: return SDL_SCANCODE_RSHIFT; + case InputKey::LControl: return SDL_SCANCODE_LCTRL; + case InputKey::RControl: return SDL_SCANCODE_RCTRL; + case InputKey::Tilde: return SDL_SCANCODE_GRAVE; + default: return (SDL_Scancode)0; + } +} diff --git a/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.h b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.h similarity index 76% rename from libraries/ZWidget/src/window/sdl2/sdl2displaywindow.h rename to libraries/ZWidget/src/window/sdl2/sdl2_display_window.h index fa0e8253c..f891c6720 100644 --- a/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.h +++ b/libraries/ZWidget/src/window/sdl2/sdl2_display_window.h @@ -3,12 +3,13 @@ #include #include #include +#include #include class SDL2DisplayWindow : public DisplayWindow { public: - SDL2DisplayWindow(DisplayWindowHost* windowHost); + SDL2DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, SDL2DisplayWindow* owner, RenderAPI renderAPI); ~SDL2DisplayWindow(); void SetWindowTitle(const std::string& text) override; @@ -19,6 +20,7 @@ public: void ShowMaximized() override; void ShowMinimized() override; void ShowNormal() override; + bool IsWindowFullscreen() override; void Hide() override; void Activate() override; void ShowCursor(bool enable) override; @@ -27,7 +29,7 @@ public: void CaptureMouse() override; void ReleaseMouseCapture() override; void Update() override; - bool GetKeyState(EInputKey key) override; + bool GetKeyState(InputKey key) override; void SetCursor(StandardCursor cursor) override; Rect GetWindowFrame() const override; @@ -45,6 +47,14 @@ public: std::string GetClipboardText() override; void SetClipboardText(const std::string& text) override; + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + void* GetNativeHandle() override { return &Handle; } + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + static void DispatchEvent(const SDL_Event& event); static SDL2DisplayWindow* FindEventWindow(const SDL_Event& event); @@ -58,10 +68,10 @@ public: void OnMouseMotion(const SDL_MouseMotionEvent& event); void OnPaintEvent(); - EInputKey GetMouseButtonKey(const SDL_MouseButtonEvent& event); + InputKey GetMouseButtonKey(const SDL_MouseButtonEvent& event); - static EInputKey ScancodeToInputKey(SDL_Scancode keycode); - static SDL_Scancode InputKeyToScancode(EInputKey inputkey); + static InputKey ScancodeToInputKey(SDL_Scancode keycode); + static SDL_Scancode InputKeyToScancode(InputKey inputkey); template Point GetMousePos(const T& event) @@ -79,12 +89,15 @@ public: static void StopTimer(void* timerID); DisplayWindowHost* WindowHost = nullptr; - SDL_Window* WindowHandle = nullptr; + SDL2NativeHandle Handle; SDL_Renderer* RendererHandle = nullptr; SDL_Texture* BackBufferTexture = nullptr; int BackBufferWidth = 0; int BackBufferHeight = 0; + bool CursorLocked = false; + bool isFullscreen = false; + static bool ExitRunLoop; static Uint32 PaintEventNumber; static std::unordered_map WindowList; diff --git a/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp b/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp deleted file mode 100644 index 17db8a863..000000000 --- a/libraries/ZWidget/src/window/sdl2/sdl2displaywindow.cpp +++ /dev/null @@ -1,675 +0,0 @@ - -#include "sdl2displaywindow.h" -#include - -Uint32 SDL2DisplayWindow::PaintEventNumber = 0xffffffff; -bool SDL2DisplayWindow::ExitRunLoop; -std::unordered_map SDL2DisplayWindow::WindowList; - -class InitSDL -{ -public: - InitSDL() - { - int result = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS); - if (result != 0) - throw std::runtime_error(std::string("Unable to initialize SDL:") + SDL_GetError()); - - SDL2DisplayWindow::PaintEventNumber = SDL_RegisterEvents(1); - } -}; - -static void CheckInitSDL() -{ - static InitSDL initsdl; -} - -SDL2DisplayWindow::SDL2DisplayWindow(DisplayWindowHost* windowHost) : WindowHost(windowHost) -{ - CheckInitSDL(); - - int result = SDL_CreateWindowAndRenderer(320, 200, SDL_WINDOW_HIDDEN /*| SDL_WINDOW_ALLOW_HIGHDPI*/, &WindowHandle, &RendererHandle); - if (result != 0) - throw std::runtime_error(std::string("Unable to create SDL window:") + SDL_GetError()); - - WindowList[SDL_GetWindowID(WindowHandle)] = this; -} - -SDL2DisplayWindow::~SDL2DisplayWindow() -{ - WindowList.erase(WindowList.find(SDL_GetWindowID(WindowHandle))); - - if (BackBufferTexture) - { - SDL_DestroyTexture(BackBufferTexture); - BackBufferTexture = nullptr; - } - - SDL_DestroyRenderer(RendererHandle); - SDL_DestroyWindow(WindowHandle); - RendererHandle = nullptr; - WindowHandle = nullptr; -} - -void SDL2DisplayWindow::SetWindowTitle(const std::string& text) -{ - SDL_SetWindowTitle(WindowHandle, text.c_str()); -} - -void SDL2DisplayWindow::SetWindowFrame(const Rect& box) -{ - // SDL2 doesn't really seem to have an API for this. - // The docs aren't clear what you're setting when calling SDL_SetWindowSize. - SetClientFrame(box); -} - -void SDL2DisplayWindow::SetClientFrame(const Rect& box) -{ - // Is there a way to set both in one call? - - double uiscale = GetDpiScale(); - int x = (int)std::round(box.x * uiscale); - int y = (int)std::round(box.y * uiscale); - int w = (int)std::round(box.width * uiscale); - int h = (int)std::round(box.height * uiscale); - - SDL_SetWindowPosition(WindowHandle, x, y); - SDL_SetWindowSize(WindowHandle, w, h); -} - -void SDL2DisplayWindow::Show() -{ - SDL_ShowWindow(WindowHandle); -} - -void SDL2DisplayWindow::ShowFullscreen() -{ - SDL_SetWindowFullscreen(WindowHandle, SDL_WINDOW_FULLSCREEN_DESKTOP); -} - -void SDL2DisplayWindow::ShowMaximized() -{ - SDL_ShowWindow(WindowHandle); - SDL_MaximizeWindow(WindowHandle); -} - -void SDL2DisplayWindow::ShowMinimized() -{ - SDL_ShowWindow(WindowHandle); - SDL_MinimizeWindow(WindowHandle); -} - -void SDL2DisplayWindow::ShowNormal() -{ - SDL_ShowWindow(WindowHandle); - SDL_SetWindowFullscreen(WindowHandle, 0); -} - -void SDL2DisplayWindow::Hide() -{ - SDL_HideWindow(WindowHandle); -} - -void SDL2DisplayWindow::Activate() -{ - SDL_RaiseWindow(WindowHandle); -} - -void SDL2DisplayWindow::ShowCursor(bool enable) -{ - SDL_ShowCursor(enable); -} - -void SDL2DisplayWindow::LockCursor() -{ - SDL_SetWindowGrab(WindowHandle, SDL_TRUE); - SDL_ShowCursor(0); -} - -void SDL2DisplayWindow::UnlockCursor() -{ - SDL_SetWindowGrab(WindowHandle, SDL_FALSE); - SDL_ShowCursor(1); -} - -void SDL2DisplayWindow::CaptureMouse() -{ -} - -void SDL2DisplayWindow::ReleaseMouseCapture() -{ -} - -void SDL2DisplayWindow::SetCursor(StandardCursor cursor) -{ -} - -void SDL2DisplayWindow::Update() -{ - SDL_Event event = {}; - event.type = PaintEventNumber; - event.user.windowID = SDL_GetWindowID(WindowHandle); - SDL_PushEvent(&event); -} - -bool SDL2DisplayWindow::GetKeyState(EInputKey key) -{ - int numkeys = 0; - const Uint8* state = SDL_GetKeyboardState(&numkeys); - if (!state) return false; - - SDL_Scancode index = InputKeyToScancode(key); - return (index < numkeys) ? state[index] != 0 : false; -} - -Rect SDL2DisplayWindow::GetWindowFrame() const -{ - int x = 0; - int y = 0; - int w = 0; - int h = 0; - double uiscale = GetDpiScale(); - SDL_GetWindowPosition(WindowHandle, &x, &y); - SDL_GetWindowSize(WindowHandle, &w, &h); - return Rect::xywh(x / uiscale, y / uiscale, w / uiscale, h / uiscale); -} - -Size SDL2DisplayWindow::GetClientSize() const -{ - int w = 0; - int h = 0; - double uiscale = GetDpiScale(); - SDL_GetWindowSize(WindowHandle, &w, &h); - return Size(w / uiscale, h / uiscale); -} - -int SDL2DisplayWindow::GetPixelWidth() const -{ - int w = 0; - int h = 0; - int result = SDL_GetRendererOutputSize(RendererHandle, &w, &h); - return w; -} - -int SDL2DisplayWindow::GetPixelHeight() const -{ - int w = 0; - int h = 0; - int result = SDL_GetRendererOutputSize(RendererHandle, &w, &h); - return h; -} - -double SDL2DisplayWindow::GetDpiScale() const -{ - // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. - return 1.0; -} - -void SDL2DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) -{ - if (!BackBufferTexture || BackBufferWidth != width || BackBufferHeight != height) - { - if (BackBufferTexture) - { - SDL_DestroyTexture(BackBufferTexture); - BackBufferTexture = nullptr; - } - - BackBufferTexture = SDL_CreateTexture(RendererHandle, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height); - if (!BackBufferTexture) - return; - - BackBufferWidth = width; - BackBufferHeight = height; - } - - int destpitch = 0; - void* dest = nullptr; - int result = SDL_LockTexture(BackBufferTexture, nullptr, &dest, &destpitch); - if (result != 0) return; - for (int y = 0; y < height; y++) - { - const void* sline = pixels + y * width; - void* dline = (uint8_t*)dest + y * destpitch; - memcpy(dline, sline, width << 2); - } - SDL_UnlockTexture(BackBufferTexture); - - SDL_RenderCopy(RendererHandle, BackBufferTexture, nullptr, nullptr); - SDL_RenderPresent(RendererHandle); -} - -void SDL2DisplayWindow::SetBorderColor(uint32_t bgra8) -{ - // SDL doesn't have this -} - -void SDL2DisplayWindow::SetCaptionColor(uint32_t bgra8) -{ - // SDL doesn't have this -} - -void SDL2DisplayWindow::SetCaptionTextColor(uint32_t bgra8) -{ - // SDL doesn't have this -} - -std::string SDL2DisplayWindow::GetClipboardText() -{ - char* buffer = SDL_GetClipboardText(); - if (!buffer) - return {}; - std::string text = buffer; - SDL_free(buffer); - return text; -} - -void SDL2DisplayWindow::SetClipboardText(const std::string& text) -{ - SDL_SetClipboardText(text.c_str()); -} - -void SDL2DisplayWindow::ProcessEvents() -{ - CheckInitSDL(); - - SDL_Event event; - while (SDL_PollEvent(&event) != 0) - { - DispatchEvent(event); - } -} - -void SDL2DisplayWindow::RunLoop() -{ - CheckInitSDL(); - - while (!ExitRunLoop) - { - SDL_Event event; - int result = SDL_WaitEvent(&event); - if (result == 1) - DispatchEvent(event); - } -} - -void SDL2DisplayWindow::ExitLoop() -{ - CheckInitSDL(); - - ExitRunLoop = true; -} - -Size SDL2DisplayWindow::GetScreenSize() -{ - CheckInitSDL(); - - SDL_Rect rect = {}; - int result = SDL_GetDisplayBounds(0, &rect); - if (result != 0) - throw std::runtime_error(std::string("Unable to get screen size:") + SDL_GetError()); - - double uiscale = 1.0; // SDL2 doesn't really support this properly. SDL_GetDisplayDPI returns the wrong information according to the docs. - return Size(rect.w / uiscale, rect.h / uiscale); -} - -void* SDL2DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) -{ - CheckInitSDL(); - - // To do: implement timers - - return nullptr; -} - -void SDL2DisplayWindow::StopTimer(void* timerID) -{ - CheckInitSDL(); - - // To do: implement timers -} - -SDL2DisplayWindow* SDL2DisplayWindow::FindEventWindow(const SDL_Event& event) -{ - int windowID; - switch (event.type) - { - case SDL_WINDOWEVENT: windowID = event.window.windowID; break; - case SDL_TEXTINPUT: windowID = event.text.windowID; break; - case SDL_KEYUP: windowID = event.key.windowID; break; - case SDL_KEYDOWN: windowID = event.key.windowID; break; - case SDL_MOUSEBUTTONUP: windowID = event.button.windowID; break; - case SDL_MOUSEBUTTONDOWN: windowID = event.button.windowID; break; - case SDL_MOUSEWHEEL: windowID = event.wheel.windowID; break; - case SDL_MOUSEMOTION: windowID = event.motion.windowID; break; - default: - if (event.type == PaintEventNumber) windowID = event.user.windowID; - else return nullptr; - } - - auto it = WindowList.find(windowID); - return it != WindowList.end() ? it->second : nullptr; -} - -void SDL2DisplayWindow::DispatchEvent(const SDL_Event& event) -{ - SDL2DisplayWindow* window = FindEventWindow(event); - if (!window) return; - - switch (event.type) - { - case SDL_WINDOWEVENT: window->OnWindowEvent(event.window); break; - case SDL_TEXTINPUT: window->OnTextInput(event.text); break; - case SDL_KEYUP: window->OnKeyUp(event.key); break; - case SDL_KEYDOWN: window->OnKeyDown(event.key); break; - case SDL_MOUSEBUTTONUP: window->OnMouseButtonUp(event.button); break; - case SDL_MOUSEBUTTONDOWN: window->OnMouseButtonDown(event.button); break; - case SDL_MOUSEWHEEL: window->OnMouseWheel(event.wheel); break; - case SDL_MOUSEMOTION: window->OnMouseMotion(event.motion); break; - default: - if (event.type == PaintEventNumber) window->OnPaintEvent(); - } -} - -void SDL2DisplayWindow::OnWindowEvent(const SDL_WindowEvent& event) -{ - switch (event.event) - { - case SDL_WINDOWEVENT_CLOSE: WindowHost->OnWindowClose(); break; - case SDL_WINDOWEVENT_MOVED: WindowHost->OnWindowGeometryChanged(); break; - case SDL_WINDOWEVENT_RESIZED: WindowHost->OnWindowGeometryChanged(); break; - case SDL_WINDOWEVENT_SHOWN: WindowHost->OnWindowPaint(); break; - case SDL_WINDOWEVENT_EXPOSED: WindowHost->OnWindowPaint(); break; - case SDL_WINDOWEVENT_FOCUS_GAINED: WindowHost->OnWindowActivated(); break; - case SDL_WINDOWEVENT_FOCUS_LOST: WindowHost->OnWindowDeactivated(); break; - } -} - -void SDL2DisplayWindow::OnTextInput(const SDL_TextInputEvent& event) -{ - WindowHost->OnWindowKeyChar(event.text); -} - -void SDL2DisplayWindow::OnKeyUp(const SDL_KeyboardEvent& event) -{ - WindowHost->OnWindowKeyUp(ScancodeToInputKey(event.keysym.scancode)); -} - -void SDL2DisplayWindow::OnKeyDown(const SDL_KeyboardEvent& event) -{ - WindowHost->OnWindowKeyDown(ScancodeToInputKey(event.keysym.scancode)); -} - -EInputKey SDL2DisplayWindow::GetMouseButtonKey(const SDL_MouseButtonEvent& event) -{ - switch (event.button) - { - case SDL_BUTTON_LEFT: return IK_LeftMouse; - case SDL_BUTTON_MIDDLE: return IK_MiddleMouse; - case SDL_BUTTON_RIGHT: return IK_RightMouse; - // case SDL_BUTTON_X1: return IK_XButton1; - // case SDL_BUTTON_X2: return IK_XButton2; - default: return IK_None; - } -} - -void SDL2DisplayWindow::OnMouseButtonUp(const SDL_MouseButtonEvent& event) -{ - EInputKey key = GetMouseButtonKey(event); - if (key != IK_None) - { - WindowHost->OnWindowMouseUp(GetMousePos(event), key); - } -} - -void SDL2DisplayWindow::OnMouseButtonDown(const SDL_MouseButtonEvent& event) -{ - EInputKey key = GetMouseButtonKey(event); - if (key != IK_None) - { - WindowHost->OnWindowMouseDown(GetMousePos(event), key); - } -} - -void SDL2DisplayWindow::OnMouseWheel(const SDL_MouseWheelEvent& event) -{ - EInputKey key = (event.y > 0) ? IK_MouseWheelUp : (event.y < 0) ? IK_MouseWheelDown : IK_None; - if (key != IK_None) - { - WindowHost->OnWindowMouseWheel(GetMousePos(event), key); - } -} - -void SDL2DisplayWindow::OnMouseMotion(const SDL_MouseMotionEvent& event) -{ - WindowHost->OnWindowMouseMove(GetMousePos(event)); -} - -void SDL2DisplayWindow::OnPaintEvent() -{ - WindowHost->OnWindowPaint(); -} - -EInputKey SDL2DisplayWindow::ScancodeToInputKey(SDL_Scancode keycode) -{ - switch (keycode) - { - case SDL_SCANCODE_BACKSPACE: return IK_Backspace; - case SDL_SCANCODE_TAB: return IK_Tab; - case SDL_SCANCODE_CLEAR: return IK_OEMClear; - case SDL_SCANCODE_RETURN: return IK_Enter; - case SDL_SCANCODE_MENU: return IK_Alt; - case SDL_SCANCODE_PAUSE: return IK_Pause; - case SDL_SCANCODE_ESCAPE: return IK_Escape; - case SDL_SCANCODE_SPACE: return IK_Space; - case SDL_SCANCODE_END: return IK_End; - case SDL_SCANCODE_HOME: return IK_Home; - case SDL_SCANCODE_LEFT: return IK_Left; - case SDL_SCANCODE_UP: return IK_Up; - case SDL_SCANCODE_RIGHT: return IK_Right; - case SDL_SCANCODE_DOWN: return IK_Down; - case SDL_SCANCODE_SELECT: return IK_Select; - case SDL_SCANCODE_PRINTSCREEN: return IK_Print; - case SDL_SCANCODE_EXECUTE: return IK_Execute; - case SDL_SCANCODE_INSERT: return IK_Insert; - case SDL_SCANCODE_DELETE: return IK_Delete; - case SDL_SCANCODE_HELP: return IK_Help; - case SDL_SCANCODE_0: return IK_0; - case SDL_SCANCODE_1: return IK_1; - case SDL_SCANCODE_2: return IK_2; - case SDL_SCANCODE_3: return IK_3; - case SDL_SCANCODE_4: return IK_4; - case SDL_SCANCODE_5: return IK_5; - case SDL_SCANCODE_6: return IK_6; - case SDL_SCANCODE_7: return IK_7; - case SDL_SCANCODE_8: return IK_8; - case SDL_SCANCODE_9: return IK_9; - case SDL_SCANCODE_A: return IK_A; - case SDL_SCANCODE_B: return IK_B; - case SDL_SCANCODE_C: return IK_C; - case SDL_SCANCODE_D: return IK_D; - case SDL_SCANCODE_E: return IK_E; - case SDL_SCANCODE_F: return IK_F; - case SDL_SCANCODE_G: return IK_G; - case SDL_SCANCODE_H: return IK_H; - case SDL_SCANCODE_I: return IK_I; - case SDL_SCANCODE_J: return IK_J; - case SDL_SCANCODE_K: return IK_K; - case SDL_SCANCODE_L: return IK_L; - case SDL_SCANCODE_M: return IK_M; - case SDL_SCANCODE_N: return IK_N; - case SDL_SCANCODE_O: return IK_O; - case SDL_SCANCODE_P: return IK_P; - case SDL_SCANCODE_Q: return IK_Q; - case SDL_SCANCODE_R: return IK_R; - case SDL_SCANCODE_S: return IK_S; - case SDL_SCANCODE_T: return IK_T; - case SDL_SCANCODE_U: return IK_U; - case SDL_SCANCODE_V: return IK_V; - case SDL_SCANCODE_W: return IK_W; - case SDL_SCANCODE_X: return IK_X; - case SDL_SCANCODE_Y: return IK_Y; - case SDL_SCANCODE_Z: return IK_Z; - case SDL_SCANCODE_KP_0: return IK_NumPad0; - case SDL_SCANCODE_KP_1: return IK_NumPad1; - case SDL_SCANCODE_KP_2: return IK_NumPad2; - case SDL_SCANCODE_KP_3: return IK_NumPad3; - case SDL_SCANCODE_KP_4: return IK_NumPad4; - case SDL_SCANCODE_KP_5: return IK_NumPad5; - case SDL_SCANCODE_KP_6: return IK_NumPad6; - case SDL_SCANCODE_KP_7: return IK_NumPad7; - case SDL_SCANCODE_KP_8: return IK_NumPad8; - case SDL_SCANCODE_KP_9: return IK_NumPad9; - // case SDL_SCANCODE_KP_ENTER: return IK_NumPadEnter; - // case SDL_SCANCODE_KP_MULTIPLY: return IK_Multiply; - // case SDL_SCANCODE_KP_PLUS: return IK_Add; - case SDL_SCANCODE_SEPARATOR: return IK_Separator; - // case SDL_SCANCODE_KP_MINUS: return IK_Subtract; - case SDL_SCANCODE_KP_PERIOD: return IK_NumPadPeriod; - // case SDL_SCANCODE_KP_DIVIDE: return IK_Divide; - case SDL_SCANCODE_F1: return IK_F1; - case SDL_SCANCODE_F2: return IK_F2; - case SDL_SCANCODE_F3: return IK_F3; - case SDL_SCANCODE_F4: return IK_F4; - case SDL_SCANCODE_F5: return IK_F5; - case SDL_SCANCODE_F6: return IK_F6; - case SDL_SCANCODE_F7: return IK_F7; - case SDL_SCANCODE_F8: return IK_F8; - case SDL_SCANCODE_F9: return IK_F9; - case SDL_SCANCODE_F10: return IK_F10; - case SDL_SCANCODE_F11: return IK_F11; - case SDL_SCANCODE_F12: return IK_F12; - case SDL_SCANCODE_F13: return IK_F13; - case SDL_SCANCODE_F14: return IK_F14; - case SDL_SCANCODE_F15: return IK_F15; - case SDL_SCANCODE_F16: return IK_F16; - case SDL_SCANCODE_F17: return IK_F17; - case SDL_SCANCODE_F18: return IK_F18; - case SDL_SCANCODE_F19: return IK_F19; - case SDL_SCANCODE_F20: return IK_F20; - case SDL_SCANCODE_F21: return IK_F21; - case SDL_SCANCODE_F22: return IK_F22; - case SDL_SCANCODE_F23: return IK_F23; - case SDL_SCANCODE_F24: return IK_F24; - case SDL_SCANCODE_NUMLOCKCLEAR: return IK_NumLock; - case SDL_SCANCODE_SCROLLLOCK: return IK_ScrollLock; - case SDL_SCANCODE_LSHIFT: return IK_LShift; - case SDL_SCANCODE_RSHIFT: return IK_RShift; - case SDL_SCANCODE_LCTRL: return IK_LControl; - case SDL_SCANCODE_RCTRL: return IK_RControl; - case SDL_SCANCODE_GRAVE: return IK_Tilde; - default: return IK_None; - } -} - -SDL_Scancode SDL2DisplayWindow::InputKeyToScancode(EInputKey inputkey) -{ - switch (inputkey) - { - case IK_Backspace: return SDL_SCANCODE_BACKSPACE; - case IK_Tab: return SDL_SCANCODE_TAB; - case IK_OEMClear: return SDL_SCANCODE_CLEAR; - case IK_Enter: return SDL_SCANCODE_RETURN; - case IK_Alt: return SDL_SCANCODE_MENU; - case IK_Pause: return SDL_SCANCODE_PAUSE; - case IK_Escape: return SDL_SCANCODE_ESCAPE; - case IK_Space: return SDL_SCANCODE_SPACE; - case IK_End: return SDL_SCANCODE_END; - case IK_Home: return SDL_SCANCODE_HOME; - case IK_Left: return SDL_SCANCODE_LEFT; - case IK_Up: return SDL_SCANCODE_UP; - case IK_Right: return SDL_SCANCODE_RIGHT; - case IK_Down: return SDL_SCANCODE_DOWN; - case IK_Select: return SDL_SCANCODE_SELECT; - case IK_Print: return SDL_SCANCODE_PRINTSCREEN; - case IK_Execute: return SDL_SCANCODE_EXECUTE; - case IK_Insert: return SDL_SCANCODE_INSERT; - case IK_Delete: return SDL_SCANCODE_DELETE; - case IK_Help: return SDL_SCANCODE_HELP; - case IK_0: return SDL_SCANCODE_0; - case IK_1: return SDL_SCANCODE_1; - case IK_2: return SDL_SCANCODE_2; - case IK_3: return SDL_SCANCODE_3; - case IK_4: return SDL_SCANCODE_4; - case IK_5: return SDL_SCANCODE_5; - case IK_6: return SDL_SCANCODE_6; - case IK_7: return SDL_SCANCODE_7; - case IK_8: return SDL_SCANCODE_8; - case IK_9: return SDL_SCANCODE_9; - case IK_A: return SDL_SCANCODE_A; - case IK_B: return SDL_SCANCODE_B; - case IK_C: return SDL_SCANCODE_C; - case IK_D: return SDL_SCANCODE_D; - case IK_E: return SDL_SCANCODE_E; - case IK_F: return SDL_SCANCODE_F; - case IK_G: return SDL_SCANCODE_G; - case IK_H: return SDL_SCANCODE_H; - case IK_I: return SDL_SCANCODE_I; - case IK_J: return SDL_SCANCODE_J; - case IK_K: return SDL_SCANCODE_K; - case IK_L: return SDL_SCANCODE_L; - case IK_M: return SDL_SCANCODE_M; - case IK_N: return SDL_SCANCODE_N; - case IK_O: return SDL_SCANCODE_O; - case IK_P: return SDL_SCANCODE_P; - case IK_Q: return SDL_SCANCODE_Q; - case IK_R: return SDL_SCANCODE_R; - case IK_S: return SDL_SCANCODE_S; - case IK_T: return SDL_SCANCODE_T; - case IK_U: return SDL_SCANCODE_U; - case IK_V: return SDL_SCANCODE_V; - case IK_W: return SDL_SCANCODE_W; - case IK_X: return SDL_SCANCODE_X; - case IK_Y: return SDL_SCANCODE_Y; - case IK_Z: return SDL_SCANCODE_Z; - case IK_NumPad0: return SDL_SCANCODE_KP_0; - case IK_NumPad1: return SDL_SCANCODE_KP_1; - case IK_NumPad2: return SDL_SCANCODE_KP_2; - case IK_NumPad3: return SDL_SCANCODE_KP_3; - case IK_NumPad4: return SDL_SCANCODE_KP_4; - case IK_NumPad5: return SDL_SCANCODE_KP_5; - case IK_NumPad6: return SDL_SCANCODE_KP_6; - case IK_NumPad7: return SDL_SCANCODE_KP_7; - case IK_NumPad8: return SDL_SCANCODE_KP_8; - case IK_NumPad9: return SDL_SCANCODE_KP_9; - // case IK_NumPadEnter: return SDL_SCANCODE_KP_ENTER; - // case IK_Multiply return SDL_SCANCODE_KP_MULTIPLY:; - // case IK_Add: return SDL_SCANCODE_KP_PLUS; - case IK_Separator: return SDL_SCANCODE_SEPARATOR; - // case IK_Subtract: return SDL_SCANCODE_KP_MINUS; - case IK_NumPadPeriod: return SDL_SCANCODE_KP_PERIOD; - // case IK_Divide: return SDL_SCANCODE_KP_DIVIDE; - case IK_F1: return SDL_SCANCODE_F1; - case IK_F2: return SDL_SCANCODE_F2; - case IK_F3: return SDL_SCANCODE_F3; - case IK_F4: return SDL_SCANCODE_F4; - case IK_F5: return SDL_SCANCODE_F5; - case IK_F6: return SDL_SCANCODE_F6; - case IK_F7: return SDL_SCANCODE_F7; - case IK_F8: return SDL_SCANCODE_F8; - case IK_F9: return SDL_SCANCODE_F9; - case IK_F10: return SDL_SCANCODE_F10; - case IK_F11: return SDL_SCANCODE_F11; - case IK_F12: return SDL_SCANCODE_F12; - case IK_F13: return SDL_SCANCODE_F13; - case IK_F14: return SDL_SCANCODE_F14; - case IK_F15: return SDL_SCANCODE_F15; - case IK_F16: return SDL_SCANCODE_F16; - case IK_F17: return SDL_SCANCODE_F17; - case IK_F18: return SDL_SCANCODE_F18; - case IK_F19: return SDL_SCANCODE_F19; - case IK_F20: return SDL_SCANCODE_F20; - case IK_F21: return SDL_SCANCODE_F21; - case IK_F22: return SDL_SCANCODE_F22; - case IK_F23: return SDL_SCANCODE_F23; - case IK_F24: return SDL_SCANCODE_F24; - case IK_NumLock: return SDL_SCANCODE_NUMLOCKCLEAR; - case IK_ScrollLock: return SDL_SCANCODE_SCROLLLOCK; - case IK_LShift: return SDL_SCANCODE_LSHIFT; - case IK_RShift: return SDL_SCANCODE_RSHIFT; - case IK_LControl: return SDL_SCANCODE_LCTRL; - case IK_RControl: return SDL_SCANCODE_RCTRL; - case IK_Tilde: return SDL_SCANCODE_GRAVE; - default: return (SDL_Scancode)0; - } -} diff --git a/libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp new file mode 100644 index 000000000..b3c61fe0b --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.cpp @@ -0,0 +1,64 @@ + +#include "stub_open_file_dialog.h" + +StubOpenFileDialog::StubOpenFileDialog(DisplayWindow* owner) : owner(owner) +{ +} + +bool StubOpenFileDialog::Show() +{ + return false; +} + +std::string StubOpenFileDialog::Filename() const +{ + return !filenames.empty() ? filenames.front() : std::string(); +} + +std::vector StubOpenFileDialog::Filenames() const +{ + return filenames; +} + +void StubOpenFileDialog::SetMultiSelect(bool new_multi_select) +{ + multi_select = new_multi_select; +} + +void StubOpenFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void StubOpenFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void StubOpenFileDialog::ClearFilters() +{ + filters.clear(); +} + +void StubOpenFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void StubOpenFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void StubOpenFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void StubOpenFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} diff --git a/libraries/ZWidget/src/window/stub/stub_open_file_dialog.h b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.h new file mode 100644 index 000000000..9ffcead2e --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_file_dialog.h @@ -0,0 +1,41 @@ +#pragma once + +#include "systemdialogs/open_file_dialog.h" + +class DisplayWindow; + +class StubOpenFileDialog : public OpenFileDialog +{ +public: + StubOpenFileDialog(DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + std::vector Filenames() const override; + void SetMultiSelect(bool new_multi_select) override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + DisplayWindow* owner = nullptr; + + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + bool multi_select = false; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp new file mode 100644 index 000000000..83ca24469 --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.cpp @@ -0,0 +1,26 @@ + +#include "stub_open_folder_dialog.h" + +StubOpenFolderDialog::StubOpenFolderDialog(DisplayWindow* owner) : owner(owner) +{ +} + +bool StubOpenFolderDialog::Show() +{ + return false; +} + +std::string StubOpenFolderDialog::SelectedPath() const +{ + return selected_path; +} + +void StubOpenFolderDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void StubOpenFolderDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} diff --git a/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h new file mode 100644 index 000000000..4d18708be --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_open_folder_dialog.h @@ -0,0 +1,23 @@ +#pragma once + +#include "systemdialogs/open_folder_dialog.h" + +class DisplayWindow; + +class StubOpenFolderDialog : public OpenFolderDialog +{ +public: + StubOpenFolderDialog(DisplayWindow* owner); + + bool Show() override; + std::string SelectedPath() const override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + +private: + DisplayWindow* owner = nullptr; + + std::string selected_path; + std::string initial_directory; + std::string title; +}; diff --git a/libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp new file mode 100644 index 000000000..64e96a2ff --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.cpp @@ -0,0 +1,54 @@ + +#include "stub_save_file_dialog.h" + +StubSaveFileDialog::StubSaveFileDialog(DisplayWindow* owner) : owner(owner) +{ +} + +bool StubSaveFileDialog::Show() +{ + return false; +} + +std::string StubSaveFileDialog::Filename() const +{ + return filename; +} + +void StubSaveFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void StubSaveFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void StubSaveFileDialog::ClearFilters() +{ + filters.clear(); +} + +void StubSaveFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void StubSaveFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void StubSaveFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void StubSaveFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} diff --git a/libraries/ZWidget/src/window/stub/stub_save_file_dialog.h b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.h new file mode 100644 index 000000000..c57ce2e5e --- /dev/null +++ b/libraries/ZWidget/src/window/stub/stub_save_file_dialog.h @@ -0,0 +1,39 @@ +#pragma once + +#include "systemdialogs/save_file_dialog.h" + +class DisplayWindow; + +class StubSaveFileDialog : public SaveFileDialog +{ +public: + StubSaveFileDialog(DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + DisplayWindow* owner = nullptr; + + std::string filename; + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp b/libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp new file mode 100644 index 000000000..ffb394853 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_backend.cpp @@ -0,0 +1,1150 @@ +#include "wayland_display_backend.h" +#include "wayland_display_window.h" + +#ifdef USE_DBUS +#include "window/dbus/dbus_open_file_dialog.h" +#include "window/dbus/dbus_save_file_dialog.h" +#include "window/dbus/dbus_open_folder_dialog.h" +#endif + +WaylandDisplayBackend::WaylandDisplayBackend() +{ + if (!s_waylandDisplay) + throw std::runtime_error("Wayland Display initialization failed!"); + + s_waylandRegistry = s_waylandDisplay.get_registry(); + + s_waylandRegistry.on_global() = [&](uint32_t name, std::string interface, uint32_t version) { + if (interface == wayland::compositor_t::interface_name) + s_waylandRegistry.bind(name, m_waylandCompositor, 3); + if (interface == wayland::shm_t::interface_name) + s_waylandRegistry.bind(name, m_waylandSHM, version); + if (interface == wayland::output_t::interface_name) + s_waylandRegistry.bind(name, m_waylandOutput, version); + if (interface == wayland::seat_t::interface_name) + s_waylandRegistry.bind(name, m_waylandSeat, 8); + if (interface == wayland::data_device_manager_t::interface_name) + s_waylandRegistry.bind(name, m_DataDeviceManager, 3); + if (interface == wayland::xdg_wm_base_t::interface_name) + s_waylandRegistry.bind(name, m_XDGWMBase, 4); + if (interface == wayland::zxdg_output_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_XDGOutputManager, version); + if (interface == wayland::zxdg_exporter_v2_t::interface_name) + s_waylandRegistry.bind(name, m_XDGExporter, 1); + if (interface == wayland::zwp_pointer_constraints_v1_t::interface_name) + s_waylandRegistry.bind(name, m_PointerConstraints, 1); + if (interface == wayland::xdg_activation_v1_t::interface_name) + s_waylandRegistry.bind(name, m_XDGActivation, 1); + if (interface == wayland::zxdg_decoration_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_XDGDecorationManager, 1); + if (interface == wayland::fractional_scale_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_FractionalScaleManager, 1); + if (interface == wayland::zwp_relative_pointer_manager_v1_t::interface_name) + s_waylandRegistry.bind(name, m_RelativePointerManager, 1); + }; + + s_waylandDisplay.roundtrip(); + + if (!m_XDGWMBase) + throw std::runtime_error("WaylandDisplayBackend: XDG-Shell is required!"); + + if (!m_XDGOutputManager) + throw std::runtime_error("WaylandDisplayBackend: xdg-output-manager-v1 is required!"); + + if (!m_XDGExporter) + throw std::runtime_error("WaylandDisplayBackend: xdg-foreign-unstable-v2 is required!"); + + if (!m_PointerConstraints) + throw std::runtime_error("WaylandDisplayBackend: pointer-constrains-unstable-v1 is required!"); + + if (!m_RelativePointerManager) + throw std::runtime_error("WaylandDisplayBackend: relative-pointer-unstable-v1 is required!"); + + m_waylandOutput.on_mode() = [this] (wayland::output_mode flags, int32_t width, int32_t height, int32_t refresh) { + s_ScreenSize = Size(width, height); + }; + + m_XDGWMBase.on_ping() = [this] (uint32_t serial) { + m_XDGWMBase.pong(serial); + }; + + m_waylandSeat.on_capabilities() = [this] (uint32_t capabilities) { + hasKeyboard = capabilities & wayland::seat_capability::keyboard; + hasPointer = capabilities & wayland::seat_capability::pointer; + }; + + m_XDGOutput = m_XDGOutputManager.get_xdg_output(m_waylandOutput); + + m_XDGOutput.on_logical_position() = [this] (int32_t x, int32_t y) { + //m_WindowGlobalPos = Point(x, y); + }; + + m_XDGOutput.on_logical_size() = [this] (int32_t width, int32_t height) { + s_ScreenSize = Size(width, height); + }; + + if (!m_FractionalScaleManager) + { + m_waylandOutput.on_scale() = [this] (int32_t scale) { + for (WaylandDisplayWindow* w : s_Windows) + { + w->m_ScaleFactor = scale; + w->m_NeedsUpdate = true; + w->windowHost->OnWindowDpiScaleChanged(); + } + }; + } + + s_waylandDisplay.roundtrip(); + + // To do: this shouldn't really be fatal. The user might have forgotten to plug in their keyboard or mouse. + if (!hasKeyboard) + throw std::runtime_error("No keyboard detected!"); + if (!hasPointer) + throw std::runtime_error("No pointer device detected!"); + + m_waylandKeyboard = m_waylandSeat.get_keyboard(); + m_waylandPointer = m_waylandSeat.get_pointer(); + m_RelativePointer = m_RelativePointerManager.get_relative_pointer(m_waylandPointer); + + ConnectDeviceEvents(); + + m_cursorSurface = m_waylandCompositor.create_surface(); + SetCursor(StandardCursor::arrow); + +/* + m_DataDevice = m_DataDeviceManager.get_data_device(m_waylandSeat); + + m_DataSource = m_DataDeviceManager.create_data_source(); + m_DataSource.offer("text/plain"); + + m_DataSource.on_send() = [&] (std::string mime_type, int fd) { + if (mime_type != "text/plain") + return; + + if (!m_ClipboardContents.empty()) + write(fd, m_ClipboardContents.data(), m_ClipboardContents.size()); + close(fd); + }; + + m_DataDevice.on_selection() = [&] (wayland::data_offer_t dataOffer) { + m_ClipboardContents.clear(); + + if (!dataOffer) + // Clipboard is empty + return; + + int fds[2]; + pipe(fds); + + dataOffer.receive("text/plain", fds[1]); + close(fds[1]); + + m_waylandDisplay.roundtrip(); + + while (true) + { + char buf[1024]; + + ssize_t n = read(fds[0], buf, sizeof(buf)); + + if (n <= 0) + break; + + m_ClipboardContents += buf; + } + + close(fds[0]); + + dataOffer.proxy_release(); + }; +*/ +} + +WaylandDisplayBackend::~WaylandDisplayBackend() +{ + if (m_KeymapContext) + xkb_context_unref(m_KeymapContext); + + if (m_KeyboardState) + xkb_state_unref(m_KeyboardState); +} + +void WaylandDisplayBackend::ConnectDeviceEvents() +{ + m_KeymapContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + + m_waylandKeyboard.on_keymap() = [this] (wayland::keyboard_keymap_format format, int fd, uint32_t size) { + if (format != wayland::keyboard_keymap_format::xkb_v1) + throw std::runtime_error("WaylandDisplayBackend: Unrecognized keymap format!"); + + char* mapSHM = (char*)mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (mapSHM == MAP_FAILED) + throw std::runtime_error("WaylandDisplayBackend: Keymap shared memory allocation failed!"); + + if (m_Keymap) + xkb_keymap_unref(m_Keymap); + + m_Keymap = xkb_keymap_new_from_string(m_KeymapContext, mapSHM, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(mapSHM, size); + close(fd); + + if (m_KeyboardState) + xkb_state_unref(m_KeyboardState); + + m_KeyboardState = xkb_state_new(m_Keymap); + }; + + m_waylandKeyboard.on_modifiers() = [this] (uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { + xkb_state_update_mask(m_KeyboardState, mods_depressed, mods_latched, mods_locked, 0, 0, group); + }; + + m_waylandKeyboard.on_enter() = [this] (uint32_t serial, wayland::surface_t surfaceEntered, wayland::array_t keys) { + std::vector keysVec = keys; + + m_KeyboardSerial = serial; + + // Find the window to focus on by checking the surface window owns. + if (!m_FocusWindow || m_FocusWindow->GetWindowSurface() != surfaceEntered) + { + for (auto win: s_Windows) + { + if (win->GetWindowSurface() == surfaceEntered) + m_FocusWindow = win; + } + } + + + for (auto key: keysVec) + { + // keys parameter represents the keys pressed when entering the surface + // key variable is Linux evdev scancode, to translate it to XKB keycode, we must add 8 + xkb_keysym_t sym = xkb_state_key_get_one_sym(m_KeyboardState, key + 8); + OnKeyboardKeyEvent(sym, wayland::keyboard_key_state::pressed); + + // Also cause a Char event + char buf[128]; + xkb_state_key_get_utf8(m_KeyboardState, key + 8, buf, sizeof(buf)); + + OnKeyboardCharEvent(buf, wayland::keyboard_key_state::pressed); + } + }; + + m_waylandKeyboard.on_key() = [this] (uint32_t serial, uint32_t time, uint32_t key, wayland::keyboard_key_state state) { + // key is Linux evdev scancode, to translate it to XKB keycode, we must add 8 + xkb_keysym_t sym = xkb_state_key_get_one_sym(m_KeyboardState, key + 8); + OnKeyboardKeyEvent(sym, state); + + // Also cause a Char event + char buf[128]; + xkb_state_key_get_utf8(m_KeyboardState, key + 8, buf, sizeof(buf)); + + OnKeyboardCharEvent(buf, state); + + //m_DataDevice.set_selection(m_DataSource, m_KeyboardSerial); + }; + + m_waylandPointer.on_enter() = [this](uint32_t serial, wayland::surface_t surfaceEntered, double surfaceX, double surfaceY) { + // Find the window to focus on by checking the surface window owns. + if (!m_MouseFocusWindow || m_MouseFocusWindow->GetWindowSurface() != surfaceEntered) + { + for (auto win: s_Windows) + { + if (win->GetWindowSurface() == surfaceEntered) + { + m_MouseFocusWindow = win; + } + } + } + + currentPointerEvent.event_mask |= POINTER_EVENT_ENTER; + currentPointerEvent.serial = serial; + currentPointerEvent.surfaceX = surfaceX; + currentPointerEvent.surfaceY = surfaceY; + }; + + m_waylandPointer.on_leave() = [this](uint32_t serial, wayland::surface_t surfaceLeft) { + currentPointerEvent.event_mask |= POINTER_EVENT_LEAVE; + currentPointerEvent.serial = serial; + }; + + m_waylandPointer.on_motion() = [this] (uint32_t serial, double surfaceX, double surfaceY) { + currentPointerEvent.event_mask |= POINTER_EVENT_MOTION; + currentPointerEvent.serial = serial; + currentPointerEvent.surfaceX = surfaceX; + currentPointerEvent.surfaceY = surfaceY; + }; + + m_waylandPointer.on_button() = [this] (uint32_t serial, uint32_t time, uint32_t button, wayland::pointer_button_state state) { + currentPointerEvent.event_mask |= POINTER_EVENT_BUTTON; + currentPointerEvent.serial = serial; + currentPointerEvent.time = time; + currentPointerEvent.button = button; + currentPointerEvent.state = state; + }; + + m_waylandPointer.on_axis() = [this] (uint32_t serial, wayland::pointer_axis axis, double value) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS; + currentPointerEvent.serial = serial; + currentPointerEvent.axes[uint32_t(axis)].value = value; + currentPointerEvent.axes[uint32_t(axis)].valid = true; + }; + + // High resolution scroll event + m_waylandPointer.on_axis_value120() = [this] (wayland::pointer_axis axis, int32_t value) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS_120; + currentPointerEvent.axes[uint32_t(axis)].valid = true; + currentPointerEvent.axes[uint32_t(axis)].value120 = value; + }; + + m_waylandPointer.on_axis_source() = [this] (wayland::pointer_axis_source axis) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS_SOURCE; + currentPointerEvent.axis_source = axis; + }; + + m_waylandPointer.on_axis_stop() = [this] (uint32_t time, wayland::pointer_axis axis) { + currentPointerEvent.event_mask |= POINTER_EVENT_AXIS_STOP; + currentPointerEvent.time = time; + currentPointerEvent.axes[uint32_t(axis)].valid = true; + }; + + m_RelativePointer.on_relative_motion() = [this] (uint32_t utime_hi, uint32_t utime_lo, + double dx, double dy, + double dx_unaccel, double dy_unaccel) { + currentPointerEvent.event_mask |= POINTER_EVENT_RELATIVE_MOTION; + currentPointerEvent.time = utime_lo; + currentPointerEvent.dx = dx; + currentPointerEvent.dy = dy; + }; + + m_waylandPointer.on_frame() = [this] () { + if (currentPointerEvent.event_mask & POINTER_EVENT_ENTER) + OnMouseEnterEvent(currentPointerEvent.serial); + + if (currentPointerEvent.event_mask & POINTER_EVENT_LEAVE) + OnMouseLeaveEvent(); + + if (currentPointerEvent.event_mask & POINTER_EVENT_MOTION) + { + OnMouseMoveEvent(Point(currentPointerEvent.surfaceX, currentPointerEvent.surfaceY)); + } + + if (currentPointerEvent.event_mask & POINTER_EVENT_RELATIVE_MOTION) + { + if (hasMouseLock) + OnMouseMoveRawEvent(int(currentPointerEvent.dx), int(currentPointerEvent.dy)); + } + + if (currentPointerEvent.event_mask & POINTER_EVENT_BUTTON) + { + InputKey ikey = LinuxInputEventCodeToInputKey(currentPointerEvent.button); + if (currentPointerEvent.state == wayland::pointer_button_state::pressed) + OnMousePressEvent(ikey); + else // released + OnMouseReleaseEvent(ikey); + } + + uint32_t axisevents = POINTER_EVENT_AXIS | POINTER_EVENT_AXIS_120 | POINTER_EVENT_AXIS_SOURCE | POINTER_EVENT_AXIS_STOP; + + if (currentPointerEvent.event_mask & axisevents) + { + for (size_t idx = 0 ; idx < 2 ; idx++) + { + if (!currentPointerEvent.axes[idx].valid) + continue; + + if (currentPointerEvent.event_mask & POINTER_EVENT_AXIS_120) + { + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value > 0) + OnMouseWheelEvent(InputKey::MouseWheelDown); + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value < 0) + OnMouseWheelEvent(InputKey::MouseWheelUp); + } + else if (currentPointerEvent.event_mask & POINTER_EVENT_AXIS) + { + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value120 > 0) + OnMouseWheelEvent(InputKey::MouseWheelDown); + if (idx == uint32_t(wayland::pointer_axis::vertical_scroll) && currentPointerEvent.axes[idx].value120 < 0) + OnMouseWheelEvent(InputKey::MouseWheelUp); + } + } + } + + // Reset everything once all the events are processed + currentPointerEvent = {0}; + }; + + m_keyboardDelayTimer = ZTimer(); + m_keyboardRepeatTimer = ZTimer(); + + m_waylandKeyboard.on_repeat_info() = [this] (int32_t rate, int32_t delay) { + // rate is characters per second, delay is in milliseconds + m_keyboardDelayTimer.SetDuration(ZTimer::Duration(delay)); + m_keyboardRepeatTimer.SetDuration(ZTimer::Duration(1000.0 / rate)); + }; + + m_keyboardDelayTimer.SetCallback([this] () { OnKeyboardDelayEnd(); }); + m_keyboardRepeatTimer.SetCallback([this] () { OnKeyboardRepeat(); }); + + m_keyboardRepeatTimer.SetRepeating(true); + + m_previousTime = ZTimer::Clock::now(); + m_currentTime = ZTimer::Clock::now(); +} + +void WaylandDisplayBackend::OnKeyboardKeyEvent(xkb_keysym_t xkbKeySym, wayland::keyboard_key_state state) +{ + InputKey inputKey = XKBKeySymToInputKey(xkbKeySym); + + if (state == wayland::keyboard_key_state::pressed) + { + inputKeyStates[inputKey] = true; + m_FocusWindow->windowHost->OnWindowKeyDown(inputKey); + if (inputKey != previousKey) + { + previousKey = inputKey; + m_keyboardDelayTimer.Stop(); + m_keyboardRepeatTimer.Stop(); + } + m_keyboardDelayTimer.Start(); + } + if (state == wayland::keyboard_key_state::released) + { + inputKeyStates[inputKey] = false; + if (m_FocusWindow) + m_FocusWindow->windowHost->OnWindowKeyUp(inputKey); + m_keyboardDelayTimer.Stop(); + m_keyboardRepeatTimer.Stop(); + } + +} + +void WaylandDisplayBackend::OnKeyboardCharEvent(const char* ch, wayland::keyboard_key_state state) +{ + if (state == wayland::keyboard_key_state::pressed) + { + previousChars = std::string(ch); + m_FocusWindow->windowHost->OnWindowKeyChar(previousChars); + } +} + +void WaylandDisplayBackend::OnKeyboardDelayEnd() +{ + if (inputKeyStates[previousKey]) + m_keyboardRepeatTimer.Start(); +} + +void WaylandDisplayBackend::OnKeyboardRepeat() +{ + if (inputKeyStates[previousKey]) + { + m_FocusWindow->windowHost->OnWindowKeyDown(previousKey); + m_FocusWindow->windowHost->OnWindowKeyChar(previousChars); + } +} + +void WaylandDisplayBackend::OnMouseEnterEvent(uint32_t serial) +{ + m_cursorSurface.attach(!hasMouseLock ? m_cursorBuffer : nullptr, 0, 0); + if (!hasMouseLock) + m_cursorSurface.damage(0, 0, m_cursorImage.width(), m_cursorImage.height()); + m_cursorSurface.commit(); + m_waylandPointer.set_cursor(serial, m_cursorSurface, 0, 0); +} + +void WaylandDisplayBackend::OnMouseLeaveEvent() +{ + if (m_MouseFocusWindow) + { + m_MouseFocusWindow->windowHost->OnWindowMouseLeave(); + //m_MouseFocusWindow = nullptr; // Borks up the menus + } +} + +void WaylandDisplayBackend::OnMousePressEvent(InputKey button) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowMouseDown(m_MouseFocusWindow->m_SurfaceMousePos, button); +} + +void WaylandDisplayBackend::OnMouseReleaseEvent(InputKey button) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowMouseUp(m_MouseFocusWindow->m_SurfaceMousePos, button); +} + +void WaylandDisplayBackend::OnMouseMoveEvent(Point surfacePos) +{ + if (m_MouseFocusWindow) + { + m_MouseFocusWindow->m_SurfaceMousePos = surfacePos / m_MouseFocusWindow->m_ScaleFactor; + m_MouseFocusWindow->windowHost->OnWindowMouseMove(m_MouseFocusWindow->m_SurfaceMousePos); + } +} + +void WaylandDisplayBackend::OnMouseMoveRawEvent(int dx, int dy) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowRawMouseMove(dx, dy); +} + +void WaylandDisplayBackend::OnMouseWheelEvent(InputKey button) +{ + if (m_MouseFocusWindow) + m_MouseFocusWindow->windowHost->OnWindowMouseWheel(m_MouseFocusWindow->m_SurfaceMousePos, button); +} + +void WaylandDisplayBackend::SetCursor(StandardCursor cursor) +{ + std::string cursorName = GetWaylandCursorName(cursor); + + // Perhaps the cursor size can be inferred from the user prefs as well? + wayland::cursor_theme_t cursorTheme = wayland::cursor_theme_t("default", 16, m_waylandSHM); + wayland::cursor_t obtainedCursor = cursorTheme.get_cursor(cursorName); + m_cursorImage = obtainedCursor.image(0); + m_cursorBuffer = m_cursorImage.get_buffer(); +} + +void WaylandDisplayBackend::ShowCursor(bool enable) +{ + m_cursorSurface.attach(enable ? m_cursorBuffer : nullptr, 0, 0); + m_cursorSurface.commit(); +} + +std::unique_ptr WaylandDisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(this, windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void WaylandDisplayBackend::OnWindowCreated(WaylandDisplayWindow* window) +{ + s_Windows.push_back(window); + if (!window->m_PopupWindow) + { + m_FocusWindow = window; + m_MouseFocusWindow = window; + } + +} + +void WaylandDisplayBackend::OnWindowDestroyed(WaylandDisplayWindow* window) +{ + auto it = std::find(s_Windows.begin(), s_Windows.end(), window); + if (it != s_Windows.end()) + s_Windows.erase(it); + + if (m_FocusWindow == window) + m_FocusWindow = nullptr; + + if (m_MouseFocusWindow == window) + m_MouseFocusWindow = nullptr; +} + +void WaylandDisplayBackend::ProcessEvents() +{ + while (wl_display_prepare_read(s_waylandDisplay)) + s_waylandDisplay.dispatch_pending(); + + while (wl_display_flush(s_waylandDisplay) < 0 && errno == EAGAIN) + poll_single(s_waylandDisplay.get_fd(), POLLOUT, -1); + + if (poll_single(s_waylandDisplay.get_fd(), POLLIN, 0) & POLLIN) + { + wl_display_read_events(s_waylandDisplay); + s_waylandDisplay.dispatch_pending(); + } + else + wl_display_cancel_read(s_waylandDisplay); + + if (s_waylandDisplay.get_error()) + throw std::runtime_error("Wayland Protocol Error"); +} + +void WaylandDisplayBackend::RunLoop() +{ + exitRunLoop = false; + + while (!exitRunLoop) + { + CheckNeedsUpdate(); + UpdateTimers(); + + ProcessEvents(); + } +} + +void WaylandDisplayBackend::UpdateTimers() +{ + m_currentTime = ZTimer::Clock::now(); + + m_keyboardDelayTimer.Update(m_currentTime - m_previousTime); + m_keyboardRepeatTimer.Update(m_currentTime - m_previousTime); + + m_previousTime = m_currentTime; +} + +void WaylandDisplayBackend::CheckNeedsUpdate() +{ + for (auto window: s_Windows) + { + if (window->m_NeedsUpdate) + { + window->m_NeedsUpdate = false; + window->windowHost->OnWindowPaint(); + } + } +} + +void WaylandDisplayBackend::ExitLoop() +{ + exitRunLoop = true; +} + +Size WaylandDisplayBackend::GetScreenSize() +{ + return s_ScreenSize; +} + +void* WaylandDisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return nullptr; +} + +void WaylandDisplayBackend::StopTimer(void* timerID) +{ +} + +#ifdef USE_DBUS +std::unique_ptr WaylandDisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "wayland:" + static_cast(owner)->GetWaylandWindowID(); + return std::make_unique(ownerHandle); +} + +std::unique_ptr WaylandDisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "wayland:" + static_cast(owner)->GetWaylandWindowID(); + return std::make_unique(ownerHandle); +} + +std::unique_ptr WaylandDisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "wayland:" + static_cast(owner)->GetWaylandWindowID(); + return std::make_unique(ownerHandle); +} +#endif + +bool WaylandDisplayBackend::GetKeyState(InputKey key) +{ + auto it = inputKeyStates.find(key); + + // if the key isn't "registered", then it is not pressed. + if (it == inputKeyStates.end()) + return false; + + return it->second; +} + +InputKey WaylandDisplayBackend::XKBKeySymToInputKey(xkb_keysym_t keySym) +{ + switch (keySym) + { + case XKB_KEY_Escape: + return InputKey::Escape; + case XKB_KEY_1: + return InputKey::_1; + case XKB_KEY_2: + return InputKey::_2; + case XKB_KEY_3: + return InputKey::_3; + case XKB_KEY_4: + return InputKey::_4; + case XKB_KEY_5: + return InputKey::_5; + case XKB_KEY_6: + return InputKey::_6; + case XKB_KEY_7: + return InputKey::_7; + case XKB_KEY_8: + return InputKey::_8; + case XKB_KEY_9: + return InputKey::_9; + case XKB_KEY_0: + return InputKey::_0; + case XKB_KEY_KP_1: + return InputKey::NumPad1; + case XKB_KEY_KP_2: + return InputKey::NumPad2; + case XKB_KEY_KP_3: + return InputKey::NumPad3; + case XKB_KEY_KP_4: + return InputKey::NumPad4; + case XKB_KEY_KP_5: + return InputKey::NumPad5; + case XKB_KEY_KP_6: + return InputKey::NumPad6; + case XKB_KEY_KP_7: + return InputKey::NumPad7; + case XKB_KEY_KP_8: + return InputKey::NumPad8; + case XKB_KEY_KP_9: + return InputKey::NumPad9; + case XKB_KEY_KP_0: + return InputKey::NumPad0; + case XKB_KEY_F1: + return InputKey::F1; + case XKB_KEY_F2: + return InputKey::F2; + case XKB_KEY_F3: + return InputKey::F3; + case XKB_KEY_F4: + return InputKey::F4; + case XKB_KEY_F5: + return InputKey::F5; + case XKB_KEY_F6: + return InputKey::F6; + case XKB_KEY_F7: + return InputKey::F7; + case XKB_KEY_F8: + return InputKey::F8; + case XKB_KEY_F9: + return InputKey::F9; + case XKB_KEY_F10: + return InputKey::F10; + case XKB_KEY_F11: + return InputKey::F11; + case XKB_KEY_F12: + return InputKey::F12; + case XKB_KEY_F13: + return InputKey::F13; + case XKB_KEY_F14: + return InputKey::F14; + case XKB_KEY_F15: + return InputKey::F15; + case XKB_KEY_F16: + return InputKey::F16; + case XKB_KEY_F17: + return InputKey::F17; + case XKB_KEY_F18: + return InputKey::F18; + case XKB_KEY_F19: + return InputKey::F19; + case XKB_KEY_F20: + return InputKey::F20; + case XKB_KEY_F21: + return InputKey::F21; + case XKB_KEY_F22: + return InputKey::F22; + case XKB_KEY_F23: + return InputKey::F23; + case XKB_KEY_F24: + return InputKey::F24; + case XKB_KEY_minus: + case XKB_KEY_KP_Subtract: + return InputKey::Minus; + case XKB_KEY_equal: + return InputKey::Equals; + case XKB_KEY_BackSpace: + return InputKey::Backspace; + case XKB_KEY_backslash: + return InputKey::Backslash; + case XKB_KEY_Tab: + return InputKey::Tab; + case XKB_KEY_braceleft: + return InputKey::LeftBracket; + case XKB_KEY_braceright: + return InputKey::RightBracket; + case XKB_KEY_Control_L: + case XKB_KEY_Control_R: + return InputKey::Ctrl; + case XKB_KEY_Alt_L: + case XKB_KEY_Alt_R: + return InputKey::Alt; + case XKB_KEY_Delete: + return InputKey::Delete; + case XKB_KEY_semicolon: + return InputKey::Semicolon; + case XKB_KEY_comma: + return InputKey::Comma; + case XKB_KEY_period: + return InputKey::Period; + case XKB_KEY_Num_Lock: + return InputKey::NumLock; + case XKB_KEY_Caps_Lock: + return InputKey::CapsLock; + case XKB_KEY_Scroll_Lock: + return InputKey::ScrollLock; + case XKB_KEY_Shift_L: + return InputKey::LShift; + case XKB_KEY_Shift_R: + return InputKey::RShift; + case XKB_KEY_grave: + return InputKey::Tilde; + case XKB_KEY_apostrophe: + return InputKey::SingleQuote; + case XKB_KEY_KP_Enter: + case XKB_KEY_Return: + return InputKey::Enter; + case XKB_KEY_space: + return InputKey::Space; + + case XKB_KEY_Up: + return InputKey::Up; + case XKB_KEY_Down: + return InputKey::Down; + case XKB_KEY_Left: + return InputKey::Left; + case XKB_KEY_Right: + return InputKey::Right; + + case XKB_KEY_A: + case XKB_KEY_a: + return InputKey::A; + case XKB_KEY_B: + case XKB_KEY_b: + return InputKey::B; + case XKB_KEY_C: + case XKB_KEY_c: + return InputKey::C; + case XKB_KEY_D: + case XKB_KEY_d: + return InputKey::D; + case XKB_KEY_E: + case XKB_KEY_e: + return InputKey::E; + case XKB_KEY_F: + case XKB_KEY_f: + return InputKey::F; + case XKB_KEY_G: + case XKB_KEY_g: + return InputKey::G; + case XKB_KEY_H: + case XKB_KEY_h: + return InputKey::H; + case XKB_KEY_I: + case XKB_KEY_i: + return InputKey::I; + case XKB_KEY_J: + case XKB_KEY_j: + return InputKey::J; + case XKB_KEY_K: + case XKB_KEY_k: + return InputKey::K; + case XKB_KEY_L: + case XKB_KEY_l: + return InputKey::L; + case XKB_KEY_M: + case XKB_KEY_m: + return InputKey::M; + case XKB_KEY_N: + case XKB_KEY_n: + return InputKey::N; + case XKB_KEY_O: + case XKB_KEY_o: + return InputKey::O; + case XKB_KEY_P: + case XKB_KEY_p: + return InputKey::P; + case XKB_KEY_Q: + case XKB_KEY_q: + return InputKey::Q; + case XKB_KEY_R: + case XKB_KEY_r: + return InputKey::R; + case XKB_KEY_S: + case XKB_KEY_s: + return InputKey::S; + case XKB_KEY_T: + case XKB_KEY_t: + return InputKey::T; + case XKB_KEY_U: + case XKB_KEY_u: + return InputKey::U; + case XKB_KEY_V: + case XKB_KEY_v: + return InputKey::V; + case XKB_KEY_W: + case XKB_KEY_w: + return InputKey::W; + case XKB_KEY_X: + case XKB_KEY_x: + return InputKey::X; + case XKB_KEY_Y: + case XKB_KEY_y: + return InputKey::Y; + case XKB_KEY_Z: + case XKB_KEY_z: + return InputKey::Z; + + case XKB_KEY_NoSymbol: + case XKB_KEY_VoidSymbol: + return InputKey::None; + default: + return InputKey::None; + } +} + +InputKey WaylandDisplayBackend::LinuxInputEventCodeToInputKey(uint32_t inputCode) +{ + switch (inputCode) + { + // Keyboard + // Probably not needed due to the existence of XKBKeySym + case KEY_ESC: + return InputKey::Escape; + case KEY_1: + return InputKey::_1; + case KEY_2: + return InputKey::_2; + case KEY_3: + return InputKey::_3; + case KEY_4: + return InputKey::_4; + case KEY_5: + return InputKey::_5; + case KEY_6: + return InputKey::_6; + case KEY_7: + return InputKey::_7; + case KEY_8: + return InputKey::_8; + case KEY_9: + return InputKey::_9; + case KEY_0: + return InputKey::_0; + case KEY_KP1: + return InputKey::NumPad1; + case KEY_KP2: + return InputKey::NumPad2; + case KEY_KP3: + return InputKey::NumPad3; + case KEY_KP4: + return InputKey::NumPad4; + case KEY_KP5: + return InputKey::NumPad5; + case KEY_KP6: + return InputKey::NumPad6; + case KEY_KP7: + return InputKey::NumPad7; + case KEY_KP8: + return InputKey::NumPad8; + case KEY_KP9: + return InputKey::NumPad9; + case KEY_KP0: + return InputKey::NumPad0; + case KEY_F1: + return InputKey::F1; + case KEY_F2: + return InputKey::F2; + case KEY_F3: + return InputKey::F3; + case KEY_F4: + return InputKey::F4; + case KEY_F5: + return InputKey::F5; + case KEY_F6: + return InputKey::F6; + case KEY_F7: + return InputKey::F7; + case KEY_F8: + return InputKey::F8; + case KEY_F9: + return InputKey::F9; + case KEY_F10: + return InputKey::F10; + case KEY_F11: + return InputKey::F11; + case KEY_F12: + return InputKey::F12; + case KEY_F13: + return InputKey::F13; + case KEY_F14: + return InputKey::F14; + case KEY_F15: + return InputKey::F15; + case KEY_F16: + return InputKey::F16; + case KEY_F17: + return InputKey::F17; + case KEY_F18: + return InputKey::F18; + case KEY_F19: + return InputKey::F19; + case KEY_F20: + return InputKey::F20; + case KEY_F21: + return InputKey::F21; + case KEY_F22: + return InputKey::F22; + case KEY_F23: + return InputKey::F23; + case KEY_F24: + return InputKey::F24; + case KEY_MINUS: + case KEY_KPMINUS: + return InputKey::Minus; + case KEY_EQUAL: + return InputKey::Equals; + case KEY_BACKSPACE: + return InputKey::Backspace; + case KEY_BACKSLASH: + return InputKey::Backslash; + case KEY_TAB: + return InputKey::Tab; + case KEY_LEFTBRACE: + return InputKey::LeftBracket; + case KEY_RIGHTBRACE: + return InputKey::RightBracket; + case KEY_LEFTCTRL: + return InputKey::LControl; + case KEY_RIGHTCTRL: + return InputKey::RControl; + case KEY_LEFTALT: + case KEY_RIGHTALT: + return InputKey::Alt; + case KEY_DELETE: + return InputKey::Delete; + case KEY_SEMICOLON: + return InputKey::Semicolon; + case KEY_COMMA: + return InputKey::Comma; + case KEY_DOT: + return InputKey::Period; + case KEY_NUMLOCK: + return InputKey::NumLock; + case KEY_CAPSLOCK: + return InputKey::CapsLock; + case KEY_SCROLLDOWN: + return InputKey::ScrollLock; + case KEY_LEFTSHIFT: + return InputKey::LShift; + case KEY_RIGHTSHIFT: + return InputKey::RShift; + case KEY_GRAVE: + return InputKey::Tilde; + case KEY_APOSTROPHE: + return InputKey::SingleQuote; + case KEY_SPACE: + return InputKey::Space; + case KEY_ENTER: + case KEY_KPENTER: + return InputKey::Enter; + + case KEY_UP: + return InputKey::Up; + case KEY_DOWN: + return InputKey::Down; + case KEY_LEFT: + return InputKey::Left; + case KEY_RIGHT: + return InputKey::Right; + + case KEY_A: + return InputKey::A; + case KEY_B: + return InputKey::B; + case KEY_C: + return InputKey::C; + case KEY_D: + return InputKey::D; + case KEY_E: + return InputKey::E; + case KEY_F: + return InputKey::F; + case KEY_G: + return InputKey::G; + case KEY_H: + return InputKey::H; + case KEY_I: + return InputKey::I; + case KEY_J: + return InputKey::J; + case KEY_K: + return InputKey::K; + case KEY_L: + return InputKey::L; + case KEY_M: + return InputKey::M; + case KEY_N: + return InputKey::N; + case KEY_O: + return InputKey::O; + case KEY_P: + return InputKey::P; + case KEY_Q: + return InputKey::Q; + case KEY_R: + return InputKey::R; + case KEY_S: + return InputKey::S; + case KEY_T: + return InputKey::T; + case KEY_U: + return InputKey::U; + case KEY_V: + return InputKey::V; + case KEY_W: + return InputKey::W; + case KEY_X: + return InputKey::X; + case KEY_Y: + return InputKey::Y; + case KEY_Z: + return InputKey::Z; + + // Mouse + case BTN_LEFT: + return InputKey::LeftMouse; + case BTN_RIGHT: + return InputKey::RightMouse; + case BTN_MIDDLE: + return InputKey::MiddleMouse; + default: + return InputKey::None; + } +} + +std::string WaylandDisplayBackend::GetWaylandCursorName(StandardCursor cursor) +{ + // Checked out Adwaita and Breeze cursors for the names. + // Other cursor themes should adhere to the names these two have. + switch (cursor) + { + case StandardCursor::arrow: + return "default"; + case StandardCursor::appstarting: + return "progress"; + case StandardCursor::cross: + return "crosshair"; + case StandardCursor::hand: + return "pointer"; + case StandardCursor::ibeam: + return "text"; + case StandardCursor::no: + return "not-allowed"; + case StandardCursor::size_all: + return "fleur"; + case StandardCursor::size_nesw: + return "nesw-resize"; + case StandardCursor::size_ns: + return "ns-resize"; + case StandardCursor::size_nwse: + return "nwse-resize"; + case StandardCursor::size_we: + return "ew-resize"; + case StandardCursor::uparrow: + // Breeze actually has an up-arrow cursor, but Adwaita doesn't, so the default cursor it is + return "default"; + case StandardCursor::wait: + return "wait"; + default: + return "default"; + } +} + diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_backend.h b/libraries/ZWidget/src/window/wayland/wayland_display_backend.h new file mode 100644 index 000000000..26e3e7f64 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_backend.h @@ -0,0 +1,168 @@ +#pragma once + +#include "window/window.h" +#include "window/ztimer/ztimer.h" + +#include +#include +#include +#include +#include +#include "wl_fractional_scaling_protocol.hpp" +#include +#include +#include +#include + +static short poll_single(int fd, short events, int timeout) { + pollfd pfd { .fd = fd, .events = events, .revents = 0 }; + if (0 > poll(&pfd, 1, timeout)) { + throw std::runtime_error("poll() failed"); + } + + return pfd.revents; +} + +enum pointer_event_mask { + POINTER_EVENT_ENTER = 1 << 0, + POINTER_EVENT_LEAVE = 1 << 1, + POINTER_EVENT_MOTION = 1 << 2, + POINTER_EVENT_BUTTON = 1 << 3, + POINTER_EVENT_AXIS = 1 << 4, + POINTER_EVENT_AXIS_SOURCE = 1 << 5, + POINTER_EVENT_AXIS_STOP = 1 << 6, + POINTER_EVENT_AXIS_DISCRETE = 1 << 7, + POINTER_EVENT_AXIS_120 = 1 << 8, + POINTER_EVENT_RELATIVE_MOTION = 1 << 9, +}; + +struct WaylandPointerEvent +{ + uint32_t event_mask; + double surfaceX, surfaceY; + double dx, dy; + uint32_t button; + wayland::pointer_button_state state; + uint32_t time; + uint32_t serial; + struct { + bool valid; + double value; + int32_t discrete; + int32_t value120; + } axes[2]; + wayland::pointer_axis_source axis_source; +}; + +class WaylandDisplayWindow; + +class WaylandDisplayBackend : public DisplayBackend +{ +public: + WaylandDisplayBackend(); + ~WaylandDisplayBackend(); + + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + bool IsWayland() override { return true; } + + void OnWindowCreated(WaylandDisplayWindow* window); + void OnWindowDestroyed(WaylandDisplayWindow* window); + + void SetCursor(StandardCursor cursor); + void ShowCursor(bool enable); + bool GetKeyState(InputKey key); + +#ifdef USE_DBUS + std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner) override; +#endif + + bool exitRunLoop = false; + Size s_ScreenSize = Size(0, 0); + wayland::display_t s_waylandDisplay = wayland::display_t(); + wayland::registry_t s_waylandRegistry; + std::vector s_Windows; + WaylandDisplayWindow* m_FocusWindow = nullptr; + WaylandDisplayWindow* m_MouseFocusWindow = nullptr; // Mouse focus should be tracked separately. + + wayland::compositor_t m_waylandCompositor; + wayland::shm_t m_waylandSHM; + wayland::seat_t m_waylandSeat; + wayland::output_t m_waylandOutput; + wayland::data_device_manager_t m_DataDeviceManager; + wayland::xdg_wm_base_t m_XDGWMBase; + wayland::zwp_pointer_constraints_v1_t m_PointerConstraints; + wayland::xdg_activation_v1_t m_XDGActivation; + wayland::zxdg_decoration_manager_v1_t m_XDGDecorationManager; + wayland::fractional_scale_manager_v1_t m_FractionalScaleManager; + wayland::zxdg_output_manager_v1_t m_XDGOutputManager; + wayland::zxdg_output_v1_t m_XDGOutput; + wayland::zxdg_exporter_v2_t m_XDGExporter; + + wayland::keyboard_t m_waylandKeyboard; + wayland::pointer_t m_waylandPointer; + + wayland::zwp_relative_pointer_manager_v1_t m_RelativePointerManager; + wayland::zwp_relative_pointer_v1_t m_RelativePointer; + + wayland::cursor_image_t m_cursorImage; + wayland::surface_t m_cursorSurface; + wayland::buffer_t m_cursorBuffer; + + std::map inputKeyStates; // True when the key is pressed, false when isn't + + bool IsMouseLocked() { return hasMouseLock; } + void SetMouseLocked(bool val) { hasMouseLock = val; } + +private: + void CheckNeedsUpdate(); + void UpdateTimers(); + void ConnectDeviceEvents(); + void OnKeyboardKeyEvent(xkb_keysym_t xkbKeySym, wayland::keyboard_key_state state); + void OnKeyboardCharEvent(const char* ch, wayland::keyboard_key_state state); + void OnKeyboardDelayEnd(); + void OnKeyboardRepeat(); + void OnMouseEnterEvent(uint32_t serial); + void OnMouseLeaveEvent(); + void OnMousePressEvent(InputKey button); + void OnMouseReleaseEvent(InputKey button); + void OnMouseMoveEvent(Point surfacePos); + void OnMouseMoveRawEvent(int surfaceX, int surfaceY); + void OnMouseWheelEvent(InputKey button); + + InputKey XKBKeySymToInputKey(xkb_keysym_t keySym); + InputKey LinuxInputEventCodeToInputKey(uint32_t inputCode); + + std::string GetWaylandCursorName(StandardCursor cursor); + + bool hasKeyboard = false; + bool hasPointer = false; + bool hasMouseLock = false; + + ZTimer::TimePoint m_previousTime; + ZTimer::TimePoint m_currentTime; + + ZTimer m_keyboardDelayTimer; + ZTimer m_keyboardRepeatTimer; + + InputKey previousKey = {}; + std::string previousChars; + + uint32_t m_KeyboardSerial = 0; + + xkb_context* m_KeymapContext = nullptr; + xkb_keymap* m_Keymap = nullptr; + xkb_state* m_KeyboardState = nullptr; + + WaylandPointerEvent currentPointerEvent = {0}; +}; diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_window.cpp b/libraries/ZWidget/src/window/wayland/wayland_display_window.cpp new file mode 100644 index 000000000..f41b3317e --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_window.cpp @@ -0,0 +1,472 @@ +#include "wayland_display_window.h" + +#include +#include + +WaylandDisplayWindow::WaylandDisplayWindow(WaylandDisplayBackend* backend, DisplayWindowHost* windowHost, bool popupWindow, WaylandDisplayWindow* owner, RenderAPI renderAPI) + : backend(backend), m_owner(owner), windowHost(windowHost), m_PopupWindow(popupWindow), m_renderAPI(renderAPI) +{ + m_AppSurface = backend->m_waylandCompositor.create_surface(); + + m_NativeHandle.display = backend->s_waylandDisplay; + m_NativeHandle.surface = m_AppSurface; + + if (backend->m_FractionalScaleManager) + { + m_FractionalScale = backend->m_FractionalScaleManager.get_fractional_scale(m_AppSurface); + + m_FractionalScale.on_preferred_scale() = [&](uint32_t scale_numerator) { + // parameter is the numerator of a fraction with the denominator of 120 + m_ScaleFactor = scale_numerator / 120.0; + + m_NeedsUpdate = true; + windowHost->OnWindowDpiScaleChanged(); + }; + } + + m_XDGSurface = backend->m_XDGWMBase.get_xdg_surface(m_AppSurface); + m_XDGSurface.on_configure() = [&] (uint32_t serial) { + m_XDGSurface.ack_configure(serial); + }; + + if (popupWindow) + { + InitializePopup(); + } + else + { + InitializeToplevel(); + } + + backend->OnWindowCreated(this); + + this->DrawSurface(); +} + +WaylandDisplayWindow::~WaylandDisplayWindow() +{ + backend->OnWindowDestroyed(this); +} + +void WaylandDisplayWindow::InitializeToplevel() +{ + m_XDGToplevel = m_XDGSurface.get_toplevel(); + m_XDGToplevel.set_title("ZWidget Window"); + + if (m_owner) + m_XDGToplevel.set_parent(m_owner->m_XDGToplevel); + + if (backend->m_XDGDecorationManager) + { + // Force server side decorations if possible + m_XDGToplevelDecoration = backend->m_XDGDecorationManager.get_toplevel_decoration(m_XDGToplevel); + m_XDGToplevelDecoration.set_mode(wayland::zxdg_toplevel_decoration_v1_mode::server_side); + } + + m_AppSurface.commit(); + + backend->s_waylandDisplay.roundtrip(); + + // These have to be added after the roundtrip + m_XDGToplevel.on_configure() = [&] (int32_t width, int32_t height, wayland::array_t states) { + OnXDGToplevelConfigureEvent(width, height); + }; + + m_XDGToplevel.on_close() = [&] () { + OnExitEvent(); + }; + + m_XDGToplevel.on_configure_bounds() = [this] (int32_t width, int32_t height) + { + + }; + + m_XDGExported = backend->m_XDGExporter.export_toplevel(m_AppSurface); + + m_XDGExported.on_handle() = [&] (std::string handleStr) { + OnExportHandleEvent(handleStr); + }; +} + +void WaylandDisplayWindow::InitializePopup() +{ + if (!m_owner) + throw std::runtime_error("Popup window must have an owner!"); + + wayland::xdg_positioner_t popupPositioner = backend->m_XDGWMBase.create_positioner(); + + popupPositioner.set_anchor(wayland::xdg_positioner_anchor::bottom); + popupPositioner.set_anchor_rect(0, 0, 1, 30); + popupPositioner.set_size(1, 1); + + m_XDGPopup = m_XDGSurface.get_popup(m_owner->m_XDGSurface, popupPositioner); + + m_XDGPopup.on_configure() = [&] (int32_t x, int32_t y, int32_t width, int32_t height) { + SetClientFrame(Rect::xywh(x, y, width, height)); + }; + + //m_XDGPopup.on_repositioned() + + m_XDGPopup.on_popup_done() = [&] () { + OnExitEvent(); + }; + + m_AppSurface.commit(); + + backend->s_waylandDisplay.roundtrip(); +} + + +void WaylandDisplayWindow::SetWindowTitle(const std::string& text) +{ + if (m_XDGToplevel) + m_XDGToplevel.set_title(text); +} + +void WaylandDisplayWindow::SetWindowFrame(const Rect& box) +{ + // Resizing will be shown on the next commit + CreateBuffers(box.width, box.height); + windowHost->OnWindowGeometryChanged(); + m_NeedsUpdate = true; + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::SetClientFrame(const Rect& box) +{ + SetWindowFrame(box); +} + +void WaylandDisplayWindow::Show() +{ + m_AppSurface.attach(m_AppSurfaceBuffer, 0, 0); + m_AppSurface.damage(0, 0, m_WindowSize.width, m_WindowSize.height); + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::ShowFullscreen() +{ + if (m_XDGToplevel) + { + m_XDGToplevel.set_fullscreen(backend->m_waylandOutput); + isFullscreen = true; + } +} + +void WaylandDisplayWindow::ShowMaximized() +{ + if (m_XDGToplevel) + m_XDGToplevel.set_maximized(); +} + +void WaylandDisplayWindow::ShowMinimized() +{ + if (m_XDGToplevel) + m_XDGToplevel.set_minimized(); +} + +void WaylandDisplayWindow::ShowNormal() +{ + if (m_XDGToplevel) + m_XDGToplevel.unset_fullscreen(); +} + +bool WaylandDisplayWindow::IsWindowFullscreen() +{ + return isFullscreen; +} + +void WaylandDisplayWindow::Hide() +{ + // Apparently this is how hiding a window works + // By attaching a null buffer to the surface + // See: https://lists.freedesktop.org/archives/wayland-devel/2017-November/035963.html + m_AppSurface.attach(nullptr, 0, 0); + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::Activate() +{ + // To do: this needs to be in the backend instance if all windows share the activation token + wayland::xdg_activation_token_v1_t xdgActivationToken = backend->m_XDGActivation.get_activation_token(); + + std::string tokenString; + + xdgActivationToken.on_done() = [&tokenString] (std::string obtainedString) { + tokenString = obtainedString; + }; + + xdgActivationToken.set_surface(m_AppSurface); + xdgActivationToken.commit(); // This will set our token string + + backend->m_XDGActivation.activate(tokenString, m_AppSurface); + backend->m_FocusWindow = this; + backend->m_MouseFocusWindow = this; + windowHost->OnWindowActivated(); +} + +void WaylandDisplayWindow::ShowCursor(bool enable) +{ + backend->ShowCursor(enable); +} + +void WaylandDisplayWindow::LockCursor() +{ + m_LockedPointer = backend->m_PointerConstraints.lock_pointer(m_AppSurface, backend->m_waylandPointer, nullptr, wayland::zwp_pointer_constraints_v1_lifetime::persistent); + backend->SetMouseLocked(true); + ShowCursor(false); +} + +void WaylandDisplayWindow::UnlockCursor() +{ + if (m_LockedPointer) + m_LockedPointer.proxy_release(); + backend->SetMouseLocked(false); + ShowCursor(true); +} + +void WaylandDisplayWindow::CaptureMouse() +{ + m_ConfinedPointer = backend->m_PointerConstraints.confine_pointer(GetWindowSurface(), backend->m_waylandPointer, nullptr, wayland::zwp_pointer_constraints_v1_lifetime::persistent); + ShowCursor(false); +} + +void WaylandDisplayWindow::ReleaseMouseCapture() +{ + if (m_ConfinedPointer) + m_ConfinedPointer.proxy_release(); + ShowCursor(true); +} + +void WaylandDisplayWindow::Update() +{ + m_NeedsUpdate = true; +} + +bool WaylandDisplayWindow::GetKeyState(InputKey key) +{ + return backend->GetKeyState(key); +} + +void WaylandDisplayWindow::SetCursor(StandardCursor cursor) +{ + backend->SetCursor(cursor); +} + +Rect WaylandDisplayWindow::GetWindowFrame() const +{ + return Rect(m_WindowGlobalPos.x, m_WindowGlobalPos.y, m_WindowSize.width, m_WindowSize.height); +} + +Size WaylandDisplayWindow::GetClientSize() const +{ + return m_WindowSize / m_ScaleFactor; +} + +int WaylandDisplayWindow::GetPixelWidth() const +{ + return m_WindowSize.width; +} + +int WaylandDisplayWindow::GetPixelHeight() const +{ + return m_WindowSize.height; +} + +double WaylandDisplayWindow::GetDpiScale() const +{ + return m_ScaleFactor; +} + +void WaylandDisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + // Make new buffers if the sizes don't match + if (width != m_WindowSize.width || height != m_WindowSize.height) + CreateBuffers(width, height); + + std::memcpy(shared_mem->get_mem(), (void*)pixels, width * height * 4); +} + +void WaylandDisplayWindow::SetBorderColor(uint32_t bgra8) +{ + +} + +void WaylandDisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + +} + +void WaylandDisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + +} + +std::string WaylandDisplayWindow::GetClipboardText() +{ + return m_ClipboardContents; +} + +void WaylandDisplayWindow::SetClipboardText(const std::string& text) +{ + m_ClipboardContents = text; +} + +Point WaylandDisplayWindow::MapFromGlobal(const Point& pos) const +{ + return (pos - m_WindowGlobalPos) / m_ScaleFactor; +} + +Point WaylandDisplayWindow::MapToGlobal(const Point& pos) const +{ + return (m_WindowGlobalPos + pos) / m_ScaleFactor; +} + +void WaylandDisplayWindow::OnXDGToplevelConfigureEvent(int32_t width, int32_t height) +{ + Rect rect = GetWindowFrame(); + rect.width = width / m_ScaleFactor; + rect.height = height / m_ScaleFactor; + SetWindowFrame(rect); + windowHost->OnWindowGeometryChanged(); +} + +void WaylandDisplayWindow::OnExportHandleEvent(std::string exportedHandle) +{ + m_windowID = exportedHandle; +} + +void WaylandDisplayWindow::OnExitEvent() +{ + windowHost->OnWindowClose(); +} + +void WaylandDisplayWindow::DrawSurface(uint32_t serial) +{ + m_AppSurface.attach(m_AppSurfaceBuffer, 0, 0); + m_AppSurface.damage(0, 0, m_WindowSize.width, m_WindowSize.height); + + if (m_renderAPI == RenderAPI::Unspecified || m_renderAPI == RenderAPI::Bitmap) + { + m_FrameCallback = m_AppSurface.frame(); + + m_FrameCallback.on_done() = bind_mem_fn(&WaylandDisplayWindow::DrawSurface, this); + } + m_AppSurface.commit(); +} + +void WaylandDisplayWindow::CreateBuffers(int32_t width, int32_t height) +{ + if (width == 0 || height == 0) + return; + + if (shared_mem) + shared_mem.reset(); + + int scaled_width = width * m_ScaleFactor; + int scaled_height = height * m_ScaleFactor; + + shared_mem = std::make_shared(scaled_width * scaled_height * 4); + auto pool = backend->m_waylandSHM.create_pool(shared_mem->get_fd(), scaled_width * scaled_height * 4); + + m_AppSurfaceBuffer = pool.create_buffer(0, scaled_width, scaled_height, scaled_width * 4, wayland::shm_format::xrgb8888); + + m_WindowSize = Size(scaled_width, scaled_height); +} + +std::string WaylandDisplayWindow::GetWaylandWindowID() +{ + return m_windowID; +} + +// This is to avoid needing all the Vulkan headers and the volk binding library just for this: +#ifndef VK_VERSION_1_0 + +#define VKAPI_CALL +#define VKAPI_PTR VKAPI_CALL + +typedef uint32_t VkFlags; +typedef enum VkStructureType { VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR = 1000006000, VK_OBJECT_TYPE_MAX_ENUM = 0x7FFFFFFF } VkStructureType; +typedef enum VkResult { VK_SUCCESS = 0, VK_RESULT_MAX_ENUM = 0x7FFFFFFF } VkResult; +typedef struct VkAllocationCallbacks VkAllocationCallbacks; + +typedef void (VKAPI_PTR* PFN_vkVoidFunction)(void); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName); + +#ifndef VK_KHR_wayland_surface + +typedef VkFlags VkWaylandSurfaceCreateFlagsKHR; +typedef struct VkWaylandSurfaceCreateInfoKHR +{ + VkStructureType sType; + const void* pNext; + VkWaylandSurfaceCreateFlagsKHR flags; + struct wl_display* display; + struct wl_surface* surface; +} VkWaylandSurfaceCreateInfoKHR; + +typedef VkResult(VKAPI_PTR* PFN_vkCreateWaylandSurfaceKHR)(VkInstance instance, const VkWaylandSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); + +#endif +#endif + +class ZWidgetWaylandVulkanLoader +{ +public: + ZWidgetWaylandVulkanLoader() + { +#if defined(__APPLE__) + module = dlopen("libvulkan.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.1.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libMoltenVK.dylib", RTLD_NOW | RTLD_LOCAL); +#else + module = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); +#endif + + if (!module) + throw std::runtime_error("Could not load vulkan"); + + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(module, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) + { + dlclose(module); + throw std::runtime_error("vkGetInstanceProcAddr not found"); + } + } + + ~ZWidgetWaylandVulkanLoader() + { + dlclose(module); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; + void* module = nullptr; +}; + +VkSurfaceKHR WaylandDisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + static ZWidgetWaylandVulkanLoader loader; + + auto vkCreateWaylandSurfaceKHR = (PFN_vkCreateWaylandSurfaceKHR)loader.vkGetInstanceProcAddr(instance, "vkCreateWaylandSurfaceKHR"); + if (!vkCreateWaylandSurfaceKHR) + throw std::runtime_error("Could not create vulkan surface"); + + VkWaylandSurfaceCreateInfoKHR createInfo = { VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR }; + createInfo.display = m_NativeHandle.display; + createInfo.surface = m_NativeHandle.surface; + + VkSurfaceKHR surface = {}; + VkResult result = vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, &surface); + if (result != VK_SUCCESS) + throw std::runtime_error("Could not create vulkan surface"); + return surface; +} + +std::vector WaylandDisplayWindow::GetVulkanInstanceExtensions() +{ + return { "VK_KHR_surface", "VK_KHR_wayland_surface" }; +} diff --git a/libraries/ZWidget/src/window/wayland/wayland_display_window.h b/libraries/ZWidget/src/window/wayland/wayland_display_window.h new file mode 100644 index 000000000..5553ece84 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wayland_display_window.h @@ -0,0 +1,198 @@ +#pragma once + +#include "wayland_display_backend.h" +#include "window/ztimer/ztimer.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "zwidget/window/window.h" +#include "zwidget/window/waylandnativehandle.h" + +#include +#include +#include +#include + +template +std::function bind_mem_fn(R(T::* func)(Args...), T *t) +{ + return [func, t] (Args... args) + { + return (t->*func)(args...); + }; +} + +class SharedMemHelper +{ +public: + SharedMemHelper(size_t size) + : len(size) + { + std::stringstream ss; + std::random_device device; + std::default_random_engine engine(device()); + std::uniform_int_distribution distribution(0, std::numeric_limits::max()); + ss << distribution(engine); + name = ss.str(); + + fd = memfd_create(name.c_str(), 0); + if(fd < 0) + throw std::runtime_error("shm_open failed."); + + if(ftruncate(fd, size) < 0) + throw std::runtime_error("ftruncate failed."); + + mem = mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if(mem == MAP_FAILED) // NOLINT + throw std::runtime_error("mmap failed with len " + std::to_string(len) + "."); + } + + ~SharedMemHelper() noexcept + { + if(fd) + { + munmap(mem, len); + close(fd); + shm_unlink(name.c_str()); + } + } + + int get_fd() const + { + return fd; + } + + void *get_mem() + { + return mem; + } + +private: + std::string name; + int fd = 0; + size_t len = 0; + void *mem = nullptr; +}; + +class WaylandDisplayWindow : public DisplayWindow +{ +public: + WaylandDisplayWindow(WaylandDisplayBackend* backend, DisplayWindowHost* windowHost, bool popupWindow, WaylandDisplayWindow* owner, RenderAPI renderAPI); + ~WaylandDisplayWindow(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(InputKey key) override; + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + void* GetNativeHandle() override { return (void*)&m_NativeHandle; } + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + + wayland::surface_t GetWindowSurface() { return m_AppSurface; } + + bool IsWindowFullscreen() override; + +private: + // Event handlers as otherwise linking DisplayWindowHost On...() functions with Wayland events directly crashes the app + // Alternatively to avoid crashes one can capture by value ([=]) instead of reference ([&]) + void OnXDGToplevelConfigureEvent(int32_t width, int32_t height); + void OnExportHandleEvent(std::string exportedHandle); + void OnExitEvent(); + + void DrawSurface(uint32_t serial = 0); + + void InitializeToplevel(); + void InitializePopup(); + + WaylandDisplayBackend* backend = nullptr; + WaylandDisplayWindow* m_owner = nullptr; + DisplayWindowHost* windowHost = nullptr; + bool m_PopupWindow = false; + + bool m_NeedsUpdate = true; + + Point m_WindowGlobalPos = Point(0, 0); + Size m_WindowSize = Size(0, 0); + double m_ScaleFactor = 1.0; + + Point m_SurfaceMousePos = Point(0, 0); + + WaylandNativeHandle m_NativeHandle; + RenderAPI m_renderAPI; + + wayland::data_device_t m_DataDevice; + wayland::data_source_t m_DataSource; + + wayland::zxdg_toplevel_decoration_v1_t m_XDGToplevelDecoration; + + wayland::fractional_scale_v1_t m_FractionalScale; + + wayland::surface_t m_AppSurface; + wayland::buffer_t m_AppSurfaceBuffer; + + wayland::xdg_surface_t m_XDGSurface; + wayland::xdg_toplevel_t m_XDGToplevel; + wayland::xdg_popup_t m_XDGPopup; + + wayland::zxdg_exported_v2_t m_XDGExported; + + wayland::zwp_locked_pointer_v1_t m_LockedPointer; + wayland::zwp_confined_pointer_v1_t m_ConfinedPointer; + + wayland::callback_t m_FrameCallback; + + std::string m_windowID; + std::string m_ClipboardContents; + + std::shared_ptr shared_mem; + + bool isFullscreen = false; + + // Helper functions + void CreateBuffers(int32_t width, int32_t height); + std::string GetWaylandWindowID(); + + friend WaylandDisplayBackend; +}; diff --git a/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp new file mode 100644 index 000000000..536ae765d --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.cpp @@ -0,0 +1,205 @@ +#include "wl_fractional_scaling_protocol.hpp" + +using namespace wayland; +using namespace wayland::detail; + +const wl_interface* fractional_scale_manager_v1_interface_destroy_request[0] = { +}; + +const wl_interface* fractional_scale_manager_v1_interface_get_fractional_scale_request[2] = { + &fractional_scale_v1_interface, + &surface_interface, +}; + +const wl_message fractional_scale_manager_v1_interface_requests[2] = { + { + "destroy", + "", + fractional_scale_manager_v1_interface_destroy_request, + }, + { + "get_fractional_scale", + "no", + fractional_scale_manager_v1_interface_get_fractional_scale_request, + }, +}; + +const wl_message fractional_scale_manager_v1_interface_events[0] = { +}; + +const wl_interface wayland::detail::fractional_scale_manager_v1_interface = + { + "wp_fractional_scale_manager_v1", + 1, + 2, + fractional_scale_manager_v1_interface_requests, + 0, + fractional_scale_manager_v1_interface_events, + }; + +const wl_interface* fractional_scale_v1_interface_destroy_request[0] = { +}; + +const wl_interface* fractional_scale_v1_interface_preferred_scale_event[1] = { + nullptr, +}; + +const wl_message fractional_scale_v1_interface_requests[1] = { + { + "destroy", + "", + fractional_scale_v1_interface_destroy_request, + }, +}; + +const wl_message fractional_scale_v1_interface_events[1] = { + { + "preferred_scale", + "u", + fractional_scale_v1_interface_preferred_scale_event, + }, +}; + +const wl_interface wayland::detail::fractional_scale_v1_interface = + { + "wp_fractional_scale_v1", + 1, + 1, + fractional_scale_v1_interface_requests, + 1, + fractional_scale_v1_interface_events, + }; + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t(const proxy_t &p) + : proxy_t(p) +{ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t() +{ + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t(wp_fractional_scale_manager_v1 *p, wrapper_type t) + : proxy_t(reinterpret_cast (p), t){ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t::fractional_scale_manager_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/) + : proxy_t(wrapped_proxy, construct_proxy_wrapper_tag()){ + set_interface(&fractional_scale_manager_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_manager_v1_t(p); }); +} + +fractional_scale_manager_v1_t fractional_scale_manager_v1_t::proxy_create_wrapper() +{ + return {*this, construct_proxy_wrapper_tag()}; +} + +const std::string fractional_scale_manager_v1_t::interface_name = "wp_fractional_scale_manager_v1"; + +fractional_scale_manager_v1_t::operator wp_fractional_scale_manager_v1*() const +{ + return reinterpret_cast (c_ptr()); +} + +fractional_scale_v1_t fractional_scale_manager_v1_t::get_fractional_scale(surface_t const& surface) +{ + proxy_t p = marshal_constructor(1U, &fractional_scale_v1_interface, nullptr, surface.proxy_has_object() ? reinterpret_cast(surface.c_ptr()) : nullptr); + return fractional_scale_v1_t(p); +} + + +int fractional_scale_manager_v1_t::dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e) +{ + return 0; +} + + +fractional_scale_v1_t::fractional_scale_v1_t(const proxy_t &p) + : proxy_t(p) +{ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t::fractional_scale_v1_t() +{ + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t::fractional_scale_v1_t(wp_fractional_scale_v1 *p, wrapper_type t) + : proxy_t(reinterpret_cast (p), t){ + if(proxy_has_object() && get_wrapper_type() == wrapper_type::standard) + { + set_events(std::shared_ptr(new events_t), dispatcher); + set_destroy_opcode(0U); + } + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t::fractional_scale_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/) + : proxy_t(wrapped_proxy, construct_proxy_wrapper_tag()){ + set_interface(&fractional_scale_v1_interface); + set_copy_constructor([] (const proxy_t &p) -> proxy_t + { return fractional_scale_v1_t(p); }); +} + +fractional_scale_v1_t fractional_scale_v1_t::proxy_create_wrapper() +{ + return {*this, construct_proxy_wrapper_tag()}; +} + +const std::string fractional_scale_v1_t::interface_name = "wp_fractional_scale_v1"; + +fractional_scale_v1_t::operator wp_fractional_scale_v1*() const +{ + return reinterpret_cast (c_ptr()); +} + +std::function &fractional_scale_v1_t::on_preferred_scale() +{ + return std::static_pointer_cast(get_events())->preferred_scale; +} + +int fractional_scale_v1_t::dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e) +{ + std::shared_ptr events = std::static_pointer_cast(e); + switch(opcode) + { + case 0: + if(events->preferred_scale) events->preferred_scale(args[0].get()); + break; + } + return 0; +} + + diff --git a/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp new file mode 100644 index 000000000..859530671 --- /dev/null +++ b/libraries/ZWidget/src/window/wayland/wl_fractional_scaling_protocol.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +struct wp_fractional_scale_manager_v1; +struct wp_fractional_scale_v1; + +namespace wayland +{ +class fractional_scale_manager_v1_t; +enum class fractional_scale_manager_v1_error : uint32_t; +class fractional_scale_v1_t; + +namespace detail +{ + extern const wl_interface fractional_scale_manager_v1_interface; + extern const wl_interface fractional_scale_v1_interface; +} + +/** \brief fractional surface scale information + + A global interface for requesting surfaces to use fractional scales. + +*/ +class fractional_scale_manager_v1_t : public proxy_t +{ +private: + struct events_t : public detail::events_base_t + { + }; + + static int dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e); + + fractional_scale_manager_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/); + +public: + fractional_scale_manager_v1_t(); + explicit fractional_scale_manager_v1_t(const proxy_t &proxy); + fractional_scale_manager_v1_t(wp_fractional_scale_manager_v1 *p, wrapper_type t = wrapper_type::standard); + + fractional_scale_manager_v1_t proxy_create_wrapper(); + + static const std::string interface_name; + + operator wp_fractional_scale_manager_v1*() const; + + /** \brief extend surface interface for scale information + \return the new surface scale info interface id + \param surface the surface + + Create an add-on object for the the wl_surface to let the compositor + request fractional scales. If the given wl_surface already has a + wp_fractional_scale_v1 object associated, the fractional_scale_exists + protocol error is raised. + + */ + fractional_scale_v1_t get_fractional_scale(surface_t const& surface); + + /** \brief Minimum protocol version required for the \ref get_fractional_scale function + */ + static constexpr std::uint32_t get_fractional_scale_since_version = 1; + +}; + +/** \brief + + */ +enum class fractional_scale_manager_v1_error : uint32_t + { + /** \brief the surface already has a fractional_scale object associated */ + fractional_scale_exists = 0 +}; + + +/** \brief fractional scale interface to a wl_surface + + An additional interface to a wl_surface object which allows the compositor + to inform the client of the preferred scale. + +*/ +class fractional_scale_v1_t : public proxy_t +{ +private: + struct events_t : public detail::events_base_t + { + std::function preferred_scale; + }; + + static int dispatcher(uint32_t opcode, const std::vector& args, const std::shared_ptr& e); + + fractional_scale_v1_t(proxy_t const &wrapped_proxy, construct_proxy_wrapper_tag /*unused*/); + +public: + fractional_scale_v1_t(); + explicit fractional_scale_v1_t(const proxy_t &proxy); + fractional_scale_v1_t(wp_fractional_scale_v1 *p, wrapper_type t = wrapper_type::standard); + + fractional_scale_v1_t proxy_create_wrapper(); + + static const std::string interface_name; + + operator wp_fractional_scale_v1*() const; + + /** \brief notify of new preferred scale + \param scale the new preferred scale + + Notification of a new preferred scale for this surface that the + compositor suggests that the client should use. + + The sent scale is the numerator of a fraction with a denominator of 120. + + */ + std::function &on_preferred_scale(); + +}; + + + +} diff --git a/libraries/ZWidget/src/window/win32/win32_display_backend.cpp b/libraries/ZWidget/src/window/win32/win32_display_backend.cpp new file mode 100644 index 000000000..0934da042 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_backend.cpp @@ -0,0 +1,56 @@ + +#include "win32_display_backend.h" +#include "win32_display_window.h" +#include "win32_open_file_dialog.h" +#include "win32_save_file_dialog.h" +#include "win32_open_folder_dialog.h" + +std::unique_ptr Win32DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void Win32DisplayBackend::ProcessEvents() +{ + Win32DisplayWindow::ProcessEvents(); +} + +void Win32DisplayBackend::RunLoop() +{ + Win32DisplayWindow::RunLoop(); +} + +void Win32DisplayBackend::ExitLoop() +{ + Win32DisplayWindow::ExitLoop(); +} + +Size Win32DisplayBackend::GetScreenSize() +{ + return Win32DisplayWindow::GetScreenSize(); +} + +void* Win32DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return Win32DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void Win32DisplayBackend::StopTimer(void* timerID) +{ + Win32DisplayWindow::StopTimer(timerID); +} + +std::unique_ptr Win32DisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + return std::make_unique(static_cast(owner)); +} + +std::unique_ptr Win32DisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + return std::make_unique(static_cast(owner)); +} + +std::unique_ptr Win32DisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + return std::make_unique(static_cast(owner)); +} diff --git a/libraries/ZWidget/src/window/win32/win32_display_backend.h b/libraries/ZWidget/src/window/win32/win32_display_backend.h new file mode 100644 index 000000000..f641249c2 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_backend.h @@ -0,0 +1,23 @@ +#pragma once + +#include "window/window.h" + +class Win32DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner) override; + + bool IsWin32() override { return true; } +}; diff --git a/libraries/ZWidget/src/window/win32/win32_display_window.cpp b/libraries/ZWidget/src/window/win32/win32_display_window.cpp new file mode 100644 index 000000000..f45da1a14 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_display_window.cpp @@ -0,0 +1,788 @@ + +#include "win32_display_window.h" +#include +#include +#include +#include +#include + +#pragma comment(lib, "dwmapi.lib") + +#ifndef HID_USAGE_PAGE_GENERIC +#define HID_USAGE_PAGE_GENERIC ((USHORT) 0x01) +#endif + +#ifndef HID_USAGE_GENERIC_MOUSE +#define HID_USAGE_GENERIC_MOUSE ((USHORT) 0x02) +#endif + +#ifndef HID_USAGE_GENERIC_JOYSTICK +#define HID_USAGE_GENERIC_JOYSTICK ((USHORT) 0x04) +#endif + +#ifndef HID_USAGE_GENERIC_GAMEPAD +#define HID_USAGE_GENERIC_GAMEPAD ((USHORT) 0x05) +#endif + +#ifndef RIDEV_INPUTSINK +#define RIDEV_INPUTSINK (0x100) +#endif + +Win32DisplayWindow::Win32DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, Win32DisplayWindow* owner, RenderAPI renderAPI) : WindowHost(windowHost), PopupWindow(popupWindow) +{ + Windows.push_front(this); + WindowsIterator = Windows.begin(); + + WNDCLASSEX classdesc = {}; + classdesc.cbSize = sizeof(WNDCLASSEX); + classdesc.hInstance = GetModuleHandle(0); + classdesc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; + classdesc.lpszClassName = L"ZWidgetWindow"; + classdesc.lpfnWndProc = &Win32DisplayWindow::WndProc; + RegisterClassEx(&classdesc); + + // Microsoft logic at its finest: + // WS_EX_DLGMODALFRAME hides the sysmenu icon + // WS_CAPTION shows the caption (yay! actually a flag that does what it says it does!) + // WS_SYSMENU shows the min/max/close buttons + // WS_THICKFRAME makes the window resizable + + DWORD style = 0, exstyle = 0; + if (popupWindow) + { + exstyle = WS_EX_NOACTIVATE; + style = WS_POPUP; + } + else + { + exstyle = WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME; + style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; + } + CreateWindowEx(exstyle, L"ZWidgetWindow", L"", style, 0, 0, 100, 100, owner ? owner->WindowHandle.hwnd : 0, 0, GetModuleHandle(0), this); +} + +Win32DisplayWindow::~Win32DisplayWindow() +{ + if (WindowHandle.hwnd) + { + DestroyWindow(WindowHandle.hwnd); + WindowHandle.hwnd = 0; + } + + Windows.erase(WindowsIterator); +} + +void Win32DisplayWindow::SetWindowTitle(const std::string& text) +{ + SetWindowText(WindowHandle.hwnd, to_utf16(text).c_str()); +} + +void Win32DisplayWindow::SetBorderColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle.hwnd, 34/*DWMWA_BORDER_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle.hwnd, 35/*DWMWA_CAPTION_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ + bgra8 = bgra8 & 0x00ffffff; + DwmSetWindowAttribute(WindowHandle.hwnd, 36/*DWMWA_TEXT_COLOR*/, &bgra8, sizeof(uint32_t)); +} + +void Win32DisplayWindow::SetWindowFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + SetWindowPos(WindowHandle.hwnd, nullptr, (int)std::round(box.x * dpiscale), (int)std::round(box.y * dpiscale), (int)std::round(box.width * dpiscale), (int)std::round(box.height * dpiscale), SWP_NOACTIVATE | SWP_NOZORDER); +} + +void Win32DisplayWindow::SetClientFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + + RECT rect = {}; + rect.left = (int)std::round(box.x * dpiscale); + rect.top = (int)std::round(box.y * dpiscale); + rect.right = rect.left + (int)std::round(box.width * dpiscale); + rect.bottom = rect.top + (int)std::round(box.height * dpiscale); + + DWORD style = (DWORD)GetWindowLongPtr(WindowHandle.hwnd, GWL_STYLE); + DWORD exstyle = (DWORD)GetWindowLongPtr(WindowHandle.hwnd, GWL_EXSTYLE); + AdjustWindowRectExForDpi(&rect, style, FALSE, exstyle, GetDpiForWindow(WindowHandle.hwnd)); + + SetWindowPos(WindowHandle.hwnd, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); +} + +void Win32DisplayWindow::Show() +{ + ShowWindow(WindowHandle.hwnd, PopupWindow ? SW_SHOWNA : SW_SHOW); +} + +void Win32DisplayWindow::ShowFullscreen() +{ + HDC screenDC = GetDC(0); + int width = GetDeviceCaps(screenDC, HORZRES); + int height = GetDeviceCaps(screenDC, VERTRES); + ReleaseDC(0, screenDC); + DWORD dwStyle = GetWindowLong(WindowHandle.hwnd, GWL_STYLE); + SetWindowLongPtr(WindowHandle.hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW); + SetWindowLongPtr(WindowHandle.hwnd, GWL_STYLE, dwStyle & ~WS_OVERLAPPEDWINDOW); + SetWindowPos(WindowHandle.hwnd, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED | SWP_SHOWWINDOW); + Fullscreen = true; +} + +void Win32DisplayWindow::ShowMaximized() +{ + ShowWindow(WindowHandle.hwnd, SW_SHOWMAXIMIZED); +} + +void Win32DisplayWindow::ShowMinimized() +{ + ShowWindow(WindowHandle.hwnd, SW_SHOWMINIMIZED); +} + +void Win32DisplayWindow::ShowNormal() +{ + if (Fullscreen) + { + SetWindowLongPtr(WindowHandle.hwnd, GWL_STYLE, WS_OVERLAPPEDWINDOW); + Fullscreen = false; + } + ShowWindow(WindowHandle.hwnd, SW_NORMAL); +} + +bool Win32DisplayWindow::IsWindowFullscreen() +{ + return Fullscreen; +} + +void Win32DisplayWindow::Hide() +{ + ShowWindow(WindowHandle.hwnd, SW_HIDE); +} + +void Win32DisplayWindow::Activate() +{ + if (!PopupWindow) + SetFocus(WindowHandle.hwnd); +} + +void Win32DisplayWindow::ShowCursor(bool enable) +{ +} + +void Win32DisplayWindow::LockCursor() +{ + if (!MouseLocked) + { + MouseLocked = true; + GetCursorPos(&MouseLockPos); + ::ShowCursor(FALSE); + + RAWINPUTDEVICE rid = {}; + rid.usUsagePage = HID_USAGE_PAGE_GENERIC; + rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.dwFlags = RIDEV_INPUTSINK; + rid.hwndTarget = WindowHandle.hwnd; + RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)); + } +} + +void Win32DisplayWindow::UnlockCursor() +{ + if (MouseLocked) + { + RAWINPUTDEVICE rid = {}; + rid.usUsagePage = HID_USAGE_PAGE_GENERIC; + rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.dwFlags = RIDEV_REMOVE; + rid.hwndTarget = 0; + RegisterRawInputDevices(&rid, 1, sizeof(rid)); + + MouseLocked = false; + SetCursorPos(MouseLockPos.x, MouseLockPos.y); + ::ShowCursor(TRUE); + } +} + +void Win32DisplayWindow::CaptureMouse() +{ + SetCapture(WindowHandle.hwnd); +} + +void Win32DisplayWindow::ReleaseMouseCapture() +{ + ReleaseCapture(); +} + +void Win32DisplayWindow::Update() +{ + InvalidateRect(WindowHandle.hwnd, nullptr, FALSE); +} + +bool Win32DisplayWindow::GetKeyState(InputKey key) +{ + return ::GetKeyState((int)key) & 0x8000; // High bit (0x8000) means key is down, Low bit (0x0001) means key is sticky on (like Caps Lock, Num Lock, etc.) +} + +void Win32DisplayWindow::SetCursor(StandardCursor cursor) +{ + if (cursor != CurrentCursor) + { + CurrentCursor = cursor; + UpdateCursor(); + } +} + +Rect Win32DisplayWindow::GetWindowFrame() const +{ + RECT box = {}; + GetWindowRect(WindowHandle.hwnd, &box); + double dpiscale = GetDpiScale(); + return Rect(box.left / dpiscale, box.top / dpiscale, (box.right - box.left) / dpiscale, (box.bottom - box.top) / dpiscale); +} + +Point Win32DisplayWindow::MapFromGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + POINT point = {}; + point.x = (LONG)std::round(pos.x / dpiscale); + point.y = (LONG)std::round(pos.y / dpiscale); + ScreenToClient(WindowHandle.hwnd, &point); + return Point(point.x * dpiscale, point.y * dpiscale); +} + +Point Win32DisplayWindow::MapToGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + POINT point = {}; + point.x = (LONG)std::round(pos.x * dpiscale); + point.y = (LONG)std::round(pos.y * dpiscale); + ClientToScreen(WindowHandle.hwnd, &point); + return Point(point.x / dpiscale, point.y / dpiscale); +} + +Size Win32DisplayWindow::GetClientSize() const +{ + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + double dpiscale = GetDpiScale(); + return Size(box.right / dpiscale, box.bottom / dpiscale); +} + +int Win32DisplayWindow::GetPixelWidth() const +{ + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + return box.right; +} + +int Win32DisplayWindow::GetPixelHeight() const +{ + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + return box.bottom; +} + +double Win32DisplayWindow::GetDpiScale() const +{ + return GetDpiForWindow(WindowHandle.hwnd) / 96.0; +} + +std::string Win32DisplayWindow::GetClipboardText() +{ + BOOL result = OpenClipboard(WindowHandle.hwnd); + if (result == FALSE) + throw std::runtime_error("Unable to open clipboard"); + + HANDLE handle = GetClipboardData(CF_UNICODETEXT); + if (handle == 0) + { + CloseClipboard(); + return std::string(); + } + + std::wstring::value_type* data = (std::wstring::value_type*)GlobalLock(handle); + if (data == 0) + { + CloseClipboard(); + return std::string(); + } + std::string str = from_utf16(data); + GlobalUnlock(handle); + + CloseClipboard(); + return str; +} + +void Win32DisplayWindow::SetClipboardText(const std::string& text) +{ + std::wstring text16 = to_utf16(text); + + BOOL result = OpenClipboard(WindowHandle.hwnd); + if (result == FALSE) + throw std::runtime_error("Unable to open clipboard"); + + result = EmptyClipboard(); + if (result == FALSE) + { + CloseClipboard(); + throw std::runtime_error("Unable to empty clipboard"); + } + + unsigned int length = (unsigned int)((text16.length() + 1) * sizeof(std::wstring::value_type)); + HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, length); + if (handle == 0) + { + CloseClipboard(); + throw std::runtime_error("Unable to allocate clipboard memory"); + } + + void* data = GlobalLock(handle); + if (data == 0) + { + GlobalFree(handle); + CloseClipboard(); + throw std::runtime_error("Unable to lock clipboard memory"); + } + memcpy(data, text16.c_str(), length); + GlobalUnlock(handle); + + HANDLE data_result = SetClipboardData(CF_UNICODETEXT, handle); + + if (data_result == 0) + { + GlobalFree(handle); + CloseClipboard(); + throw std::runtime_error("Unable to set clipboard data"); + } + + CloseClipboard(); +} + +void Win32DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + BITMAPV5HEADER header = {}; + header.bV5Size = sizeof(BITMAPV5HEADER); + header.bV5Width = width; + header.bV5Height = -height; + header.bV5Planes = 1; + header.bV5BitCount = 32; + header.bV5Compression = BI_BITFIELDS; + header.bV5AlphaMask = 0xff000000; + header.bV5RedMask = 0x00ff0000; + header.bV5GreenMask = 0x0000ff00; + header.bV5BlueMask = 0x000000ff; + header.bV5SizeImage = width * height * sizeof(uint32_t); + header.bV5CSType = LCS_sRGB; + + HDC dc = PaintDC; + if (dc != 0) + { + SetDIBitsToDevice(dc, 0, 0, width, height, 0, 0, 0, height, pixels, (const BITMAPINFO*)&header, BI_RGB); + } +} + +LRESULT Win32DisplayWindow::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) +{ + LPARAM result = 0; + if (DwmDefWindowProc(WindowHandle.hwnd, msg, wparam, lparam, &result)) + return result; + + if (msg == WM_INPUT) + { + bool hasFocus = GetFocus() != 0; + + HRAWINPUT handle = (HRAWINPUT)lparam; + UINT size = 0; + UINT result = GetRawInputData(handle, RID_INPUT, 0, &size, sizeof(RAWINPUTHEADER)); + if (result == 0 && size > 0) + { + size *= 2; + std::vector buffer(size); + result = GetRawInputData(handle, RID_INPUT, buffer.data(), &size, sizeof(RAWINPUTHEADER)); + if (result >= 0) + { + RAWINPUT* rawinput = (RAWINPUT*)buffer.data(); + if (rawinput->header.dwType == RIM_TYPEMOUSE) + { + if (hasFocus) + WindowHost->OnWindowRawMouseMove(rawinput->data.mouse.lLastX, rawinput->data.mouse.lLastY); + } + } + } + return DefWindowProc(WindowHandle.hwnd, msg, wparam, lparam); + } + else if (msg == WM_PAINT) + { + PAINTSTRUCT paintStruct = {}; + PaintDC = BeginPaint(WindowHandle.hwnd, &paintStruct); + if (PaintDC) + { + WindowHost->OnWindowPaint(); + EndPaint(WindowHandle.hwnd, &paintStruct); + PaintDC = 0; + } + return 0; + } + else if (msg == WM_ACTIVATE) + { + WindowHost->OnWindowActivated(); + } + else if (msg == WM_MOUSEACTIVATE) + { + // We don't want to activate the window on mouse clicks as that changes the focus from the popup owner to the popup itself + if (PopupWindow) + return MA_NOACTIVATE; + } + else if (msg == WM_MOUSEMOVE) + { + if (MouseLocked && GetFocus() != 0) + { + RECT box = {}; + GetClientRect(WindowHandle.hwnd, &box); + + POINT center = {}; + center.x = box.right / 2; + center.y = box.bottom / 2; + ClientToScreen(WindowHandle.hwnd, ¢er); + + SetCursorPos(center.x, center.y); + } + else + { + UpdateCursor(); + } + + if (!TrackMouseActive) + { + TRACKMOUSEEVENT eventTrack = {}; + eventTrack.cbSize = sizeof(TRACKMOUSEEVENT); + eventTrack.hwndTrack = WindowHandle.hwnd; + eventTrack.dwFlags = TME_LEAVE; + if (TrackMouseEvent(&eventTrack)) + TrackMouseActive = true; + } + + WindowHost->OnWindowMouseMove(GetLParamPos(lparam)); + } + else if (msg == WM_MOUSELEAVE) + { + TrackMouseActive = false; + WindowHost->OnWindowMouseLeave(); + } + else if (msg == WM_LBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), InputKey::LeftMouse); + } + else if (msg == WM_LBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), InputKey::LeftMouse); + } + else if (msg == WM_LBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), InputKey::LeftMouse); + } + else if (msg == WM_MBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), InputKey::MiddleMouse); + } + else if (msg == WM_MBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), InputKey::MiddleMouse); + } + else if (msg == WM_MBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), InputKey::MiddleMouse); + } + else if (msg == WM_RBUTTONDOWN) + { + WindowHost->OnWindowMouseDown(GetLParamPos(lparam), InputKey::RightMouse); + } + else if (msg == WM_RBUTTONDBLCLK) + { + WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), InputKey::RightMouse); + } + else if (msg == WM_RBUTTONUP) + { + WindowHost->OnWindowMouseUp(GetLParamPos(lparam), InputKey::RightMouse); + } + else if (msg == WM_MOUSEWHEEL) + { + double delta = GET_WHEEL_DELTA_WPARAM(wparam) / (double)WHEEL_DELTA; + + // Note: WM_MOUSEWHEEL uses screen coordinates. GetLParamPos assumes client coordinates. + double dpiscale = GetDpiScale(); + POINT pos; + pos.x = GET_X_LPARAM(lparam); + pos.y = GET_Y_LPARAM(lparam); + ScreenToClient(WindowHandle.hwnd, &pos); + + WindowHost->OnWindowMouseWheel(Point(pos.x / dpiscale, pos.y / dpiscale), delta < 0.0 ? InputKey::MouseWheelDown : InputKey::MouseWheelUp); + } + else if (msg == WM_CHAR) + { + wchar_t buf[2] = { (wchar_t)wparam, 0 }; + WindowHost->OnWindowKeyChar(from_utf16(buf)); + } + else if (msg == WM_KEYDOWN) + { + WindowHost->OnWindowKeyDown((InputKey)wparam); + } + else if (msg == WM_KEYUP) + { + WindowHost->OnWindowKeyUp((InputKey)wparam); + } + else if (msg == WM_SETFOCUS) + { + if (MouseLocked) + { + ::ShowCursor(FALSE); + } + } + else if (msg == WM_KILLFOCUS) + { + if (MouseLocked) + { + ::ShowCursor(TRUE); + } + } + else if (msg == WM_CLOSE) + { + WindowHost->OnWindowClose(); + return 0; + } + else if (msg == WM_SIZE) + { + WindowHost->OnWindowGeometryChanged(); + return 0; + } + /*else if (msg == WM_NCCALCSIZE && wparam == TRUE) // calculate client area for the window + { + NCCALCSIZE_PARAMS* calcsize = (NCCALCSIZE_PARAMS*)lparam; + return WVR_REDRAW; + }*/ + + return DefWindowProc(WindowHandle.hwnd, msg, wparam, lparam); +} + +void Win32DisplayWindow::UpdateCursor() +{ + LPCWSTR cursor = IDC_ARROW; + switch (CurrentCursor) + { + case StandardCursor::arrow: cursor = IDC_ARROW; break; + case StandardCursor::appstarting: cursor = IDC_APPSTARTING; break; + case StandardCursor::cross: cursor = IDC_CROSS; break; + case StandardCursor::hand: cursor = IDC_HAND; break; + case StandardCursor::ibeam: cursor = IDC_IBEAM; break; + case StandardCursor::no: cursor = IDC_NO; break; + case StandardCursor::size_all: cursor = IDC_SIZEALL; break; + case StandardCursor::size_nesw: cursor = IDC_SIZENESW; break; + case StandardCursor::size_ns: cursor = IDC_SIZENS; break; + case StandardCursor::size_nwse: cursor = IDC_SIZENWSE; break; + case StandardCursor::size_we: cursor = IDC_SIZEWE; break; + case StandardCursor::uparrow: cursor = IDC_UPARROW; break; + case StandardCursor::wait: cursor = IDC_WAIT; break; + default: break; + } + + ::SetCursor((HCURSOR)LoadImage(0, cursor, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); +} + +Point Win32DisplayWindow::GetLParamPos(LPARAM lparam) const +{ + double dpiscale = GetDpiScale(); + return Point(GET_X_LPARAM(lparam) / dpiscale, GET_Y_LPARAM(lparam) / dpiscale); +} + +LRESULT Win32DisplayWindow::WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam) +{ + if (msg == WM_CREATE) + { + CREATESTRUCT* createstruct = (CREATESTRUCT*)lparam; + Win32DisplayWindow* viewport = (Win32DisplayWindow*)createstruct->lpCreateParams; + viewport->WindowHandle.hwnd = windowhandle; + SetWindowLongPtr(windowhandle, GWLP_USERDATA, (LONG_PTR)viewport); + return viewport->OnWindowMessage(msg, wparam, lparam); + } + else + { + Win32DisplayWindow* viewport = (Win32DisplayWindow*)GetWindowLongPtr(windowhandle, GWLP_USERDATA); + if (viewport) + { + LRESULT result = viewport->OnWindowMessage(msg, wparam, lparam); + if (msg == WM_DESTROY) + { + SetWindowLongPtr(windowhandle, GWLP_USERDATA, 0); + viewport->WindowHandle.hwnd = 0; + } + return result; + } + else + { + return DefWindowProc(windowhandle, msg, wparam, lparam); + } + } +} + +void Win32DisplayWindow::ProcessEvents() +{ + while (true) + { + MSG msg = {}; + if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE) <= 0) + break; + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +void Win32DisplayWindow::RunLoop() +{ + while (!ExitRunLoop && !Windows.empty()) + { + MSG msg = {}; + if (GetMessage(&msg, 0, 0, 0) <= 0) + break; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + ExitRunLoop = false; +} + +void Win32DisplayWindow::ExitLoop() +{ + ExitRunLoop = true; +} + +Size Win32DisplayWindow::GetScreenSize() +{ + HDC screenDC = GetDC(0); + int screenWidth = GetDeviceCaps(screenDC, HORZRES); + int screenHeight = GetDeviceCaps(screenDC, VERTRES); + double dpiScale = GetDeviceCaps(screenDC, LOGPIXELSX) / 96.0; + ReleaseDC(0, screenDC); + + return Size(screenWidth / dpiScale, screenHeight / dpiScale); +} + +// This is to avoid needing all the Vulkan headers and the volk binding library just for this: +#ifndef VK_VERSION_1_0 + +#define VKAPI_CALL __stdcall +#define VKAPI_PTR VKAPI_CALL + +typedef uint32_t VkFlags; +typedef enum VkStructureType { VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR = 1000009000, VK_OBJECT_TYPE_MAX_ENUM = 0x7FFFFFFF } VkStructureType; +typedef enum VkResult { VK_SUCCESS = 0, VK_RESULT_MAX_ENUM = 0x7FFFFFFF } VkResult; +typedef struct VkAllocationCallbacks VkAllocationCallbacks; + +typedef void (VKAPI_PTR* PFN_vkVoidFunction)(void); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName); + +#ifndef VK_KHR_win32_surface + +typedef VkFlags VkWin32SurfaceCreateFlagsKHR; +typedef struct VkWin32SurfaceCreateInfoKHR +{ + VkStructureType sType; + const void* pNext; + VkWin32SurfaceCreateFlagsKHR flags; + HINSTANCE hinstance; + HWND hwnd; +} VkWin32SurfaceCreateInfoKHR; + +typedef VkResult(VKAPI_PTR* PFN_vkCreateWin32SurfaceKHR)(VkInstance instance, const VkWin32SurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); + +#endif +#endif + +class ZWidgetVulkanLoader +{ +public: + ZWidgetVulkanLoader() + { + module = LoadLibraryA("vulkan-1.dll"); + if (!module) + throw std::runtime_error("Could not load vulkan-1.dll"); + + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)(void(*)(void))GetProcAddress(module, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) + { + FreeLibrary(module); + throw std::runtime_error("vkGetInstanceProcAddr not found in vulkan-1.dll"); + } + } + + ~ZWidgetVulkanLoader() + { + FreeLibrary(module); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; + HMODULE module = {}; +}; + +VkSurfaceKHR Win32DisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + static ZWidgetVulkanLoader loader; + + auto vkCreateWin32SurfaceKHR = (PFN_vkCreateWin32SurfaceKHR)loader.vkGetInstanceProcAddr(instance, "vkCreateWin32SurfaceKHR"); + if (!vkCreateWin32SurfaceKHR) + throw std::runtime_error("Could not create vulkan surface"); + + VkWin32SurfaceCreateInfoKHR createInfo = { VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR }; + createInfo.hwnd = WindowHandle.hwnd; + createInfo.hinstance = GetModuleHandle(nullptr); + + VkSurfaceKHR surface = {}; + VkResult result = vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface); + if (result != VK_SUCCESS) + throw std::runtime_error("Could not create vulkan surface"); + return surface; +} + +std::vector Win32DisplayWindow::GetVulkanInstanceExtensions() +{ + return { "VK_KHR_surface", "VK_KHR_win32_surface" }; +} + +static void CALLBACK Win32TimerCallback(HWND handle, UINT message, UINT_PTR timerID, DWORD timestamp) +{ + auto it = Win32DisplayWindow::Timers.find(timerID); + if (it != Win32DisplayWindow::Timers.end()) + { + auto callback = it->second; + callback(); + } +} + +void* Win32DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + UINT_PTR result = SetTimer(0, 0, timeoutMilliseconds, Win32TimerCallback); + if (result == 0) + throw std::runtime_error("Could not create timer"); + Timers[result] = std::move(onTimer); + return (void*)result; +} + +void Win32DisplayWindow::StopTimer(void* timerID) +{ + auto it = Timers.find((UINT_PTR)timerID); + if (it != Timers.end()) + { + Timers.erase(it); + KillTimer(0, (UINT_PTR)timerID); + } +} + +std::list Win32DisplayWindow::Windows; +bool Win32DisplayWindow::ExitRunLoop; + +std::unordered_map> Win32DisplayWindow::Timers; diff --git a/libraries/ZWidget/src/window/win32/win32window.h b/libraries/ZWidget/src/window/win32/win32_display_window.h similarity index 70% rename from libraries/ZWidget/src/window/win32/win32window.h rename to libraries/ZWidget/src/window/win32/win32_display_window.h index a0f493c8b..7666274bd 100644 --- a/libraries/ZWidget/src/window/win32/win32window.h +++ b/libraries/ZWidget/src/window/win32/win32_display_window.h @@ -1,21 +1,17 @@ #pragma once -#define NOMINMAX -#define WIN32_MEAN_AND_LEAN -#ifndef WINVER -#define WINVER 0x0605 -#endif -#include +#include "win32_util.h" #include #include #include +#include -class Win32Window : public DisplayWindow +class Win32DisplayWindow : public DisplayWindow { public: - Win32Window(DisplayWindowHost* windowHost); - ~Win32Window(); + Win32DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, Win32DisplayWindow* owner, RenderAPI renderAPI); + ~Win32DisplayWindow(); void SetWindowTitle(const std::string& text) override; void SetWindowFrame(const Rect& box) override; @@ -25,6 +21,7 @@ public: void ShowMaximized() override; void ShowMinimized() override; void ShowNormal() override; + bool IsWindowFullscreen() override; void Hide() override; void Activate() override; void ShowCursor(bool enable) override; @@ -33,7 +30,7 @@ public: void CaptureMouse() override; void ReleaseMouseCapture() override; void Update() override; - bool GetKeyState(EInputKey key) override; + bool GetKeyState(InputKey key) override; void SetCursor(StandardCursor cursor) override; void UpdateCursor(); @@ -53,8 +50,16 @@ public: std::string GetClipboardText() override; void SetClipboardText(const std::string& text) override; + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + Point GetLParamPos(LPARAM lparam) const; + void* GetNativeHandle() override { return &WindowHandle; } + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + static void ProcessEvents(); static void RunLoop(); static void ExitLoop(); @@ -64,8 +69,8 @@ public: static void StopTimer(void* timerID); static bool ExitRunLoop; - static std::list Windows; - std::list::iterator WindowsIterator; + static std::list Windows; + std::list::iterator WindowsIterator; static std::unordered_map> Timers; @@ -73,13 +78,16 @@ public: static LRESULT CALLBACK WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam); DisplayWindowHost* WindowHost = nullptr; + bool PopupWindow = false; - HWND WindowHandle = 0; + Win32NativeHandle WindowHandle; bool Fullscreen = false; bool MouseLocked = false; POINT MouseLockPos = {}; + bool TrackMouseActive = false; + HDC PaintDC = 0; StandardCursor CurrentCursor = StandardCursor::arrow; diff --git a/libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp new file mode 100644 index 000000000..092a78da1 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.cpp @@ -0,0 +1,227 @@ + +#include "win32_open_file_dialog.h" +#include "win32_display_window.h" +#include "core/widget.h" +#include +#include +#include +#include +#include + +Win32OpenFileDialog::Win32OpenFileDialog(Win32DisplayWindow* owner) : owner(owner) +{ +} + +bool Win32OpenFileDialog::Show() +{ + std::wstring title16 = to_utf16(title); + std::wstring initial_directory16 = to_utf16(initial_directory); + + HRESULT result; + ComPtr open_dialog; + + result = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, open_dialog.GetIID(), open_dialog.InitPtr()); + throw_if_failed(result, "CoCreateInstance(FileOpenDialog) failed"); + + result = open_dialog->SetTitle(title16.c_str()); + throw_if_failed(result, "IFileOpenDialog.SetTitle failed"); + + if (!initial_filename.empty()) + { + result = open_dialog->SetFileName(to_utf16(initial_filename).c_str()); + throw_if_failed(result, "IFileOpenDialog.SetFileName failed"); + } + + FILEOPENDIALOGOPTIONS options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST; + if (multi_select) + options |= FOS_ALLOWMULTISELECT; + result = open_dialog->SetOptions(options); + throw_if_failed(result, "IFileOpenDialog.SetOptions() failed"); + + if (!filters.empty()) + { + std::vector filterspecs(filters.size()); + std::vector descriptions(filters.size()); + std::vector extensions(filters.size()); + for (size_t i = 0; i < filters.size(); i++) + { + descriptions[i] = to_utf16(filters[i].description); + extensions[i] = to_utf16(filters[i].extension); + COMDLG_FILTERSPEC& spec = filterspecs[i]; + spec.pszName = descriptions[i].c_str(); + spec.pszSpec = extensions[i].c_str(); + } + result = open_dialog->SetFileTypes((UINT)filterspecs.size(), filterspecs.data()); + throw_if_failed(result, "IFileOpenDialog.SetFileTypes() failed"); + + if ((size_t)filterindex < filters.size()) + { + result = open_dialog->SetFileTypeIndex(filterindex); + throw_if_failed(result, "IFileOpenDialog.SetFileTypeIndex() failed"); + } + } + + if (!defaultext.empty()) + { + result = open_dialog->SetDefaultExtension(to_utf16(defaultext).c_str()); + throw_if_failed(result, "IFileOpenDialog.SetDefaultExtension() failed"); + } + + if (initial_directory16.length() > 0) + { + LPITEMIDLIST item_id_list = nullptr; + SFGAOF flags = 0; + result = SHParseDisplayName(initial_directory16.c_str(), nullptr, &item_id_list, SFGAO_FILESYSTEM, &flags); + throw_if_failed(result, "SHParseDisplayName failed"); + + ComPtr folder_item; + result = SHCreateShellItem(nullptr, nullptr, item_id_list, folder_item.TypedInitPtr()); + ILFree(item_id_list); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + + /* This code requires Windows Vista or newer: + ComPtr folder_item; + result = SHCreateItemFromParsingName(initial_directory16.c_str(), nullptr, folder_item.GetIID(), folder_item.InitPtr()); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + */ + + if (folder_item) + { + result = open_dialog->SetFolder(folder_item); + throw_if_failed(result, "IFileOpenDialog.SetFolder failed"); + } + } + + // For some reason this can hang deep inside Win32 if we do it on the calling thread! + { + bool done = false; + std::mutex mutex; + std::condition_variable condvar; + + std::thread thread([&]() { + + if (owner) + result = open_dialog->Show(owner->WindowHandle.hwnd); + else + result = open_dialog->Show(0); + + std::unique_lock lock(mutex); + done = true; + condvar.notify_all(); + + }); + + std::unique_lock lock(mutex); + while (!done) + { + DisplayBackend::Get()->ProcessEvents(); + using namespace std::chrono_literals; + condvar.wait_for(lock, 50ms, [&]() { return done; }); + } + lock.unlock(); + thread.join(); + } + + if (SUCCEEDED(result)) + { + ComPtr items; + result = open_dialog->GetResults(items.TypedInitPtr()); + throw_if_failed(result, "IFileOpenDialog.GetSelectedItems failed"); + + DWORD num_items = 0; + result = items->GetCount(&num_items); + throw_if_failed(result, "IShellItemArray.GetCount failed"); + + for (DWORD i = 0; i < num_items; i++) + { + ComPtr item; + result = items->GetItemAt(i, item.TypedInitPtr()); + throw_if_failed(result, "IShellItemArray.GetItemAt failed"); + + WCHAR* buffer = nullptr; + result = item->GetDisplayName(SIGDN_FILESYSPATH, &buffer); + throw_if_failed(result, "IShellItem.GetDisplayName failed"); + + std::wstring output16; + if (buffer) + { + try + { + output16 = buffer; + } + catch (...) + { + CoTaskMemFree(buffer); + throw; + } + } + + CoTaskMemFree(buffer); + filenames.push_back(from_utf16(output16)); + } + return true; + } + else + { + return false; + } +} + +std::string Win32OpenFileDialog::Filename() const +{ + return !filenames.empty() ? filenames.front() : std::string(); +} + +std::vector Win32OpenFileDialog::Filenames() const +{ + return filenames; +} + +void Win32OpenFileDialog::SetMultiSelect(bool new_multi_select) +{ + multi_select = new_multi_select; +} + +void Win32OpenFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void Win32OpenFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void Win32OpenFileDialog::ClearFilters() +{ + filters.clear(); +} + +void Win32OpenFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void Win32OpenFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void Win32OpenFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void Win32OpenFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} + +void Win32OpenFileDialog::throw_if_failed(HRESULT result, const std::string& error) +{ + if (FAILED(result)) + throw std::runtime_error(error); +} diff --git a/libraries/ZWidget/src/window/win32/win32_open_file_dialog.h b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.h new file mode 100644 index 000000000..52ac858f0 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_file_dialog.h @@ -0,0 +1,44 @@ +#pragma once + +#include "systemdialogs/open_file_dialog.h" +#include "win32_util.h" + +class Win32DisplayWindow; + +class Win32OpenFileDialog : public OpenFileDialog +{ +public: + Win32OpenFileDialog(Win32DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + std::vector Filenames() const override; + void SetMultiSelect(bool new_multi_select) override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + void throw_if_failed(HRESULT result, const std::string& error); + + Win32DisplayWindow* owner = nullptr; + + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + bool multi_select = false; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp new file mode 100644 index 000000000..f579f0ce7 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.cpp @@ -0,0 +1,111 @@ + +#include "win32_open_folder_dialog.h" +#include "win32_display_window.h" +#include "core/widget.h" +#include + +Win32OpenFolderDialog::Win32OpenFolderDialog(Win32DisplayWindow* owner) : owner(owner) +{ +} + +bool Win32OpenFolderDialog::Show() +{ + std::wstring title16 = to_utf16(title); + std::wstring initial_directory16 = to_utf16(initial_directory); + + HRESULT result; + ComPtr open_dialog; + + result = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, open_dialog.GetIID(), open_dialog.InitPtr()); + throw_if_failed(result, "CoCreateInstance(FileOpenDialog) failed"); + + result = open_dialog->SetTitle(title16.c_str()); + throw_if_failed(result, "IFileOpenDialog.SetTitle failed"); + + result = open_dialog->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST); + throw_if_failed(result, "IFileOpenDialog.SetOptions((FOS_PICKFOLDERS) failed"); + + if (initial_directory16.length() > 0) + { + LPITEMIDLIST item_id_list = nullptr; + SFGAOF flags = 0; + result = SHParseDisplayName(initial_directory16.c_str(), nullptr, &item_id_list, SFGAO_FILESYSTEM, &flags); + throw_if_failed(result, "SHParseDisplayName failed"); + + ComPtr folder_item; + result = SHCreateShellItem(nullptr, nullptr, item_id_list, folder_item.TypedInitPtr()); + ILFree(item_id_list); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + + /* This code requires Windows Vista or newer: + ComPtr folder_item; + result = SHCreateItemFromParsingName(initial_directory16.c_str(), nullptr, folder_item.GetIID(), folder_item.InitPtr()); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + */ + + if (folder_item) + { + result = open_dialog->SetFolder(folder_item); + throw_if_failed(result, "IFileOpenDialog.SetFolder failed"); + } + } + + if (owner) + result = open_dialog->Show(owner->WindowHandle.hwnd); + else + result = open_dialog->Show(0); + + if (SUCCEEDED(result)) + { + ComPtr chosen_folder; + result = open_dialog->GetResult(chosen_folder.TypedInitPtr()); + throw_if_failed(result, "IFileOpenDialog.GetResult failed"); + + WCHAR* buffer = nullptr; + result = chosen_folder->GetDisplayName(SIGDN_FILESYSPATH, &buffer); + throw_if_failed(result, "IShellItem.GetDisplayName failed"); + + std::wstring output_directory16; + if (buffer) + { + try + { + output_directory16 = buffer; + } + catch (...) + { + CoTaskMemFree(buffer); + throw; + } + } + + CoTaskMemFree(buffer); + selected_path = from_utf16(output_directory16); + return true; + } + else + { + return false; + } +} + +std::string Win32OpenFolderDialog::SelectedPath() const +{ + return selected_path; +} + +void Win32OpenFolderDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void Win32OpenFolderDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void Win32OpenFolderDialog::throw_if_failed(HRESULT result, const std::string& error) +{ + if (FAILED(result)) + throw std::runtime_error(error); +} diff --git a/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h new file mode 100644 index 000000000..45f05a26b --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_open_folder_dialog.h @@ -0,0 +1,26 @@ +#pragma once + +#include "systemdialogs/open_folder_dialog.h" +#include "win32_util.h" + +class Win32DisplayWindow; + +class Win32OpenFolderDialog : public OpenFolderDialog +{ +public: + Win32OpenFolderDialog(Win32DisplayWindow* owner); + + bool Show() override; + std::string SelectedPath() const override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + +private: + void throw_if_failed(HRESULT result, const std::string& error); + + Win32DisplayWindow* owner = nullptr; + + std::string selected_path; + std::string initial_directory; + std::string title; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp new file mode 100644 index 000000000..02568f1ff --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.cpp @@ -0,0 +1,174 @@ + +#include "win32_save_file_dialog.h" +#include "win32_display_window.h" +#include "core/widget.h" + +Win32SaveFileDialog::Win32SaveFileDialog(Win32DisplayWindow* owner) : owner(owner) +{ +} + +bool Win32SaveFileDialog::Show() +{ + std::wstring title16 = to_utf16(title); + std::wstring initial_directory16 = to_utf16(initial_directory); + + HRESULT result; + ComPtr save_dialog; + + result = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_ALL, save_dialog.GetIID(), save_dialog.InitPtr()); + throw_if_failed(result, "CoCreateInstance(FileSaveDialog) failed"); + + result = save_dialog->SetTitle(title16.c_str()); + throw_if_failed(result, "IFileSaveDialog.SetTitle failed"); + + if (!initial_filename.empty()) + { + result = save_dialog->SetFileName(to_utf16(initial_filename).c_str()); + throw_if_failed(result, "IFileSaveDialog.SetFileName failed"); + } + + FILEOPENDIALOGOPTIONS options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST; + result = save_dialog->SetOptions(options); + throw_if_failed(result, "IFileSaveDialog.SetOptions() failed"); + + if (!filters.empty()) + { + std::vector filterspecs(filters.size()); + std::vector descriptions(filters.size()); + std::vector extensions(filters.size()); + for (size_t i = 0; i < filters.size(); i++) + { + descriptions[i] = to_utf16(filters[i].description); + extensions[i] = to_utf16(filters[i].extension); + COMDLG_FILTERSPEC& spec = filterspecs[i]; + spec.pszName = descriptions[i].c_str(); + spec.pszSpec = extensions[i].c_str(); + } + result = save_dialog->SetFileTypes((UINT)filterspecs.size(), filterspecs.data()); + throw_if_failed(result, "IFileOpenDialog.SetFileTypes() failed"); + + if ((size_t)filterindex < filters.size()) + { + result = save_dialog->SetFileTypeIndex(filterindex); + throw_if_failed(result, "IFileOpenDialog.SetFileTypeIndex() failed"); + } + } + + if (!defaultext.empty()) + { + result = save_dialog->SetDefaultExtension(to_utf16(defaultext).c_str()); + throw_if_failed(result, "IFileOpenDialog.SetDefaultExtension() failed"); + } + + if (initial_directory16.length() > 0) + { + LPITEMIDLIST item_id_list = nullptr; + SFGAOF flags = 0; + result = SHParseDisplayName(initial_directory16.c_str(), nullptr, &item_id_list, SFGAO_FILESYSTEM, &flags); + throw_if_failed(result, "SHParseDisplayName failed"); + + ComPtr folder_item; + result = SHCreateShellItem(nullptr, nullptr, item_id_list, folder_item.TypedInitPtr()); + ILFree(item_id_list); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + + /* This code requires Windows Vista or newer: + ComPtr folder_item; + result = SHCreateItemFromParsingName(initial_directory16.c_str(), nullptr, folder_item.GetIID(), folder_item.InitPtr()); + throw_if_failed(result, "SHCreateItemFromParsingName failed"); + */ + + if (folder_item) + { + result = save_dialog->SetFolder(folder_item); + throw_if_failed(result, "IFileSaveDialog.SetFolder failed"); + } + } + + if (owner) + result = save_dialog->Show(owner->WindowHandle.hwnd); + else + result = save_dialog->Show(0); + + if (SUCCEEDED(result)) + { + ComPtr chosen_folder; + result = save_dialog->GetResult(chosen_folder.TypedInitPtr()); + throw_if_failed(result, "IFileSaveDialog.GetResult failed"); + + WCHAR* buffer = nullptr; + result = chosen_folder->GetDisplayName(SIGDN_FILESYSPATH, &buffer); + throw_if_failed(result, "IShellItem.GetDisplayName failed"); + + std::wstring output16; + if (buffer) + { + try + { + output16 = buffer; + } + catch (...) + { + CoTaskMemFree(buffer); + throw; + } + } + + CoTaskMemFree(buffer); + filename = from_utf16(output16); + return true; + } + else + { + return false; + } +} + +std::string Win32SaveFileDialog::Filename() const +{ + return filename; +} + +void Win32SaveFileDialog::SetFilename(const std::string& filename) +{ + initial_filename = filename; +} + +void Win32SaveFileDialog::AddFilter(const std::string& filter_description, const std::string& filter_extension) +{ + Filter f; + f.description = filter_description; + f.extension = filter_extension; + filters.push_back(std::move(f)); +} + +void Win32SaveFileDialog::ClearFilters() +{ + filters.clear(); +} + +void Win32SaveFileDialog::SetFilterIndex(int filter_index) +{ + filterindex = filter_index; +} + +void Win32SaveFileDialog::SetInitialDirectory(const std::string& path) +{ + initial_directory = path; +} + +void Win32SaveFileDialog::SetTitle(const std::string& newtitle) +{ + title = newtitle; +} + +void Win32SaveFileDialog::SetDefaultExtension(const std::string& extension) +{ + defaultext = extension; +} + +void Win32SaveFileDialog::throw_if_failed(HRESULT result, const std::string& error) +{ + if (FAILED(result)) + throw std::runtime_error(error); +} diff --git a/libraries/ZWidget/src/window/win32/win32_save_file_dialog.h b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.h new file mode 100644 index 000000000..5a5071423 --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_save_file_dialog.h @@ -0,0 +1,42 @@ +#pragma once + +#include "systemdialogs/save_file_dialog.h" +#include "win32_util.h" + +class Win32DisplayWindow; + +class Win32SaveFileDialog : public SaveFileDialog +{ +public: + Win32SaveFileDialog(Win32DisplayWindow* owner); + + bool Show() override; + std::string Filename() const override; + void SetFilename(const std::string& filename) override; + void AddFilter(const std::string& filter_description, const std::string& filter_extension) override; + void ClearFilters() override; + void SetFilterIndex(int filter_index) override; + void SetInitialDirectory(const std::string& path) override; + void SetTitle(const std::string& newtitle) override; + void SetDefaultExtension(const std::string& extension) override; + +private: + void throw_if_failed(HRESULT result, const std::string& error); + + Win32DisplayWindow* owner = nullptr; + + std::string filename; + std::string initial_directory; + std::string initial_filename; + std::string title; + std::vector filenames; + + struct Filter + { + std::string description; + std::string extension; + }; + std::vector filters; + int filterindex = 0; + std::string defaultext; +}; diff --git a/libraries/ZWidget/src/window/win32/win32_util.h b/libraries/ZWidget/src/window/win32/win32_util.h new file mode 100644 index 000000000..06f89540e --- /dev/null +++ b/libraries/ZWidget/src/window/win32/win32_util.h @@ -0,0 +1,60 @@ +#pragma once + +#define NOMINMAX +#define WIN32_MEAN_AND_LEAN +#ifndef WINVER +#define WINVER 0x0605 +#endif +#include +#include +#include + +namespace +{ + static std::string from_utf16(const std::wstring& str) + { + if (str.empty()) return {}; + int needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0, nullptr, nullptr); + if (needed == 0) + throw std::runtime_error("WideCharToMultiByte failed"); + std::string result; + result.resize(needed); + needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size(), nullptr, nullptr); + if (needed == 0) + throw std::runtime_error("WideCharToMultiByte failed"); + return result; + } + + static std::wstring to_utf16(const std::string& str) + { + if (str.empty()) return {}; + int needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0); + if (needed == 0) + throw std::runtime_error("MultiByteToWideChar failed"); + std::wstring result; + result.resize(needed); + needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size()); + if (needed == 0) + throw std::runtime_error("MultiByteToWideChar failed"); + return result; + } + + template + class ComPtr + { + public: + ComPtr() { Ptr = nullptr; } + ComPtr(const ComPtr& other) { Ptr = other.Ptr; if (Ptr) Ptr->AddRef(); } + ComPtr(ComPtr&& move) { Ptr = move.Ptr; move.Ptr = nullptr; } + ~ComPtr() { reset(); } + ComPtr& operator=(const ComPtr& other) { if (this != &other) { if (Ptr) Ptr->Release(); Ptr = other.Ptr; if (Ptr) Ptr->AddRef(); } return *this; } + void reset() { if (Ptr) Ptr->Release(); Ptr = nullptr; } + T* get() { return Ptr; } + static IID GetIID() { return __uuidof(T); } + void** InitPtr() { return (void**)TypedInitPtr(); } + T** TypedInitPtr() { reset(); return &Ptr; } + operator T* () const { return Ptr; } + T* operator ->() const { return Ptr; } + T* Ptr; + }; +} diff --git a/libraries/ZWidget/src/window/win32/win32window.cpp b/libraries/ZWidget/src/window/win32/win32window.cpp deleted file mode 100644 index 6ad09ef60..000000000 --- a/libraries/ZWidget/src/window/win32/win32window.cpp +++ /dev/null @@ -1,702 +0,0 @@ - -#include "win32window.h" -#include -#include -#include -#include -#include - -#pragma comment(lib, "dwmapi.lib") - -#ifndef HID_USAGE_PAGE_GENERIC -#define HID_USAGE_PAGE_GENERIC ((USHORT) 0x01) -#endif - -#ifndef HID_USAGE_GENERIC_MOUSE -#define HID_USAGE_GENERIC_MOUSE ((USHORT) 0x02) -#endif - -#ifndef HID_USAGE_GENERIC_JOYSTICK -#define HID_USAGE_GENERIC_JOYSTICK ((USHORT) 0x04) -#endif - -#ifndef HID_USAGE_GENERIC_GAMEPAD -#define HID_USAGE_GENERIC_GAMEPAD ((USHORT) 0x05) -#endif - -#ifndef RIDEV_INPUTSINK -#define RIDEV_INPUTSINK (0x100) -#endif - -#ifdef MINGW -// MinGW's library doesn't contain a thunk for DwmDefWindowProc, so we need to create our own - -BOOL DwmDefWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, LRESULT *plResult ) -{ - typedef BOOL(* dwmdwp)(HWND, UINT, WPARAM, LPARAM, LRESULT* ); - BOOL result(FALSE); - HMODULE module = LoadLibrary( _T( "dwmapi.dll" ) ); - if( module ) { - dwmdwp proc = reinterpret_cast( GetProcAddress( module, "DwmDefWindowProc" ) ); - if( proc ) { - result = proc( hWnd, msg, wParam, lParam, plResult ); - } - FreeLibrary(module); - } - return result; -} - -#endif - -static std::string from_utf16(const std::wstring& str) -{ - if (str.empty()) return {}; - int needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0, nullptr, nullptr); - if (needed == 0) - throw std::runtime_error("WideCharToMultiByte failed"); - std::string result; - result.resize(needed); - needed = WideCharToMultiByte(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size(), nullptr, nullptr); - if (needed == 0) - throw std::runtime_error("WideCharToMultiByte failed"); - return result; -} - -static std::wstring to_utf16(const std::string& str) -{ - if (str.empty()) return {}; - int needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0); - if (needed == 0) - throw std::runtime_error("MultiByteToWideChar failed"); - std::wstring result; - result.resize(needed); - needed = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &result[0], (int)result.size()); - if (needed == 0) - throw std::runtime_error("MultiByteToWideChar failed"); - return result; -} - -Win32Window::Win32Window(DisplayWindowHost* windowHost) : WindowHost(windowHost) -{ - Windows.push_front(this); - WindowsIterator = Windows.begin(); - - WNDCLASSEXW classdesc = {}; - classdesc.cbSize = sizeof(WNDCLASSEX); - classdesc.hInstance = GetModuleHandle(0); - classdesc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; - classdesc.lpszClassName = L"ZWidgetWindow"; - classdesc.lpfnWndProc = &Win32Window::WndProc; - RegisterClassEx(&classdesc); - - // Microsoft logic at its finest: - // WS_EX_DLGMODALFRAME hides the sysmenu icon - // WS_CAPTION shows the caption (yay! actually a flag that does what it says it does!) - // WS_SYSMENU shows the min/max/close buttons - // WS_THICKFRAME makes the window resizable - CreateWindowExW(WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME, L"ZWidgetWindow", L"", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, 0, 0, 100, 100, 0, 0, GetModuleHandle(0), this); - - /* - RAWINPUTDEVICE rid; - rid.usUsagePage = HID_USAGE_PAGE_GENERIC; - rid.usUsage = HID_USAGE_GENERIC_MOUSE; - rid.dwFlags = RIDEV_INPUTSINK; - rid.hwndTarget = WindowHandle; - BOOL result = RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)); - */ -} - -Win32Window::~Win32Window() -{ - if (WindowHandle) - { - DestroyWindow(WindowHandle); - WindowHandle = 0; - } - - Windows.erase(WindowsIterator); -} - -void Win32Window::SetWindowTitle(const std::string& text) -{ - SetWindowText(WindowHandle, to_utf16(text).c_str()); -} - -void Win32Window::SetBorderColor(uint32_t bgra8) -{ - bgra8 = bgra8 & 0x00ffffff; - DwmSetWindowAttribute(WindowHandle, 34/*DWMWA_BORDER_COLOR*/, &bgra8, sizeof(uint32_t)); -} - -void Win32Window::SetCaptionColor(uint32_t bgra8) -{ - bgra8 = bgra8 & 0x00ffffff; - DwmSetWindowAttribute(WindowHandle, 35/*DWMWA_CAPTION_COLOR*/, &bgra8, sizeof(uint32_t)); -} - -void Win32Window::SetCaptionTextColor(uint32_t bgra8) -{ - bgra8 = bgra8 & 0x00ffffff; - DwmSetWindowAttribute(WindowHandle, 36/*DWMWA_TEXT_COLOR*/, &bgra8, sizeof(uint32_t)); -} - -void Win32Window::SetWindowFrame(const Rect& box) -{ - double dpiscale = GetDpiScale(); - SetWindowPos(WindowHandle, nullptr, (int)std::round(box.x * dpiscale), (int)std::round(box.y * dpiscale), (int)std::round(box.width * dpiscale), (int)std::round(box.height * dpiscale), SWP_NOACTIVATE | SWP_NOZORDER); -} - -void Win32Window::SetClientFrame(const Rect& box) -{ - // This function is currently unused but needs to be disabled because it contains Windows API calls that were only added in Windows 10. -#if 0 - double dpiscale = GetDpiScale(); - - RECT rect = {}; - rect.left = (int)std::round(box.x * dpiscale); - rect.top = (int)std::round(box.y * dpiscale); - rect.right = rect.left + (int)std::round(box.width * dpiscale); - rect.bottom = rect.top + (int)std::round(box.height * dpiscale); - - DWORD style = (DWORD)GetWindowLongPtr(WindowHandle, GWL_STYLE); - DWORD exstyle = (DWORD)GetWindowLongPtr(WindowHandle, GWL_EXSTYLE); - AdjustWindowRectExForDpi(&rect, style, FALSE, exstyle, GetDpiForWindow(WindowHandle)); - - SetWindowPos(WindowHandle, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOACTIVATE | SWP_NOZORDER); -#endif -} - -void Win32Window::Show() -{ - ShowWindow(WindowHandle, SW_SHOW); -} - -void Win32Window::ShowFullscreen() -{ - HDC screenDC = GetDC(0); - int width = GetDeviceCaps(screenDC, HORZRES); - int height = GetDeviceCaps(screenDC, VERTRES); - ReleaseDC(0, screenDC); - SetWindowLongPtr(WindowHandle, GWL_EXSTYLE, WS_EX_APPWINDOW); - SetWindowLongPtr(WindowHandle, GWL_STYLE, WS_OVERLAPPED); - SetWindowPos(WindowHandle, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED | SWP_SHOWWINDOW); - Fullscreen = true; -} - -void Win32Window::ShowMaximized() -{ - ShowWindow(WindowHandle, SW_SHOWMAXIMIZED); -} - -void Win32Window::ShowMinimized() -{ - ShowWindow(WindowHandle, SW_SHOWMINIMIZED); -} - -void Win32Window::ShowNormal() -{ - ShowWindow(WindowHandle, SW_NORMAL); -} - -void Win32Window::Hide() -{ - ShowWindow(WindowHandle, SW_HIDE); -} - -void Win32Window::Activate() -{ - SetFocus(WindowHandle); -} - -void Win32Window::ShowCursor(bool enable) -{ -} - -void Win32Window::LockCursor() -{ - if (!MouseLocked) - { - MouseLocked = true; - GetCursorPos(&MouseLockPos); - ::ShowCursor(FALSE); - } -} - -void Win32Window::UnlockCursor() -{ - if (MouseLocked) - { - MouseLocked = false; - SetCursorPos(MouseLockPos.x, MouseLockPos.y); - ::ShowCursor(TRUE); - } -} - -void Win32Window::CaptureMouse() -{ - SetCapture(WindowHandle); -} - -void Win32Window::ReleaseMouseCapture() -{ - ReleaseCapture(); -} - -void Win32Window::Update() -{ - InvalidateRect(WindowHandle, nullptr, FALSE); -} - -bool Win32Window::GetKeyState(EInputKey key) -{ - return ::GetKeyState((int)key) & 0x8000; // High bit (0x8000) means key is down, Low bit (0x0001) means key is sticky on (like Caps Lock, Num Lock, etc.) -} - -void Win32Window::SetCursor(StandardCursor cursor) -{ - if (cursor != CurrentCursor) - { - CurrentCursor = cursor; - UpdateCursor(); - } -} - -Rect Win32Window::GetWindowFrame() const -{ - RECT box = {}; - GetWindowRect(WindowHandle, &box); - double dpiscale = GetDpiScale(); - return Rect(box.left / dpiscale, box.top / dpiscale, box.right / dpiscale, box.bottom / dpiscale); -} - -Size Win32Window::GetClientSize() const -{ - RECT box = {}; - GetClientRect(WindowHandle, &box); - double dpiscale = GetDpiScale(); - return Size(box.right / dpiscale, box.bottom / dpiscale); -} - -int Win32Window::GetPixelWidth() const -{ - RECT box = {}; - GetClientRect(WindowHandle, &box); - return box.right; -} - -int Win32Window::GetPixelHeight() const -{ - RECT box = {}; - GetClientRect(WindowHandle, &box); - return box.bottom; -} - -typedef UINT(WINAPI* GetDpiForWindow_t)(HWND); -double Win32Window::GetDpiScale() const -{ - static GetDpiForWindow_t pGetDpiForWindow = nullptr; - static bool done = false; - if (!done) - { - HMODULE hMod = GetModuleHandleA("User32.dll"); - if (hMod != nullptr) pGetDpiForWindow = reinterpret_cast(GetProcAddress(hMod, "GetDpiForWindow")); - done = true; - } - - if (pGetDpiForWindow) - return pGetDpiForWindow(WindowHandle) / 96.0; - else - return 1.0; -} - -std::string Win32Window::GetClipboardText() -{ - BOOL result = OpenClipboard(WindowHandle); - if (result == FALSE) - throw std::runtime_error("Unable to open clipboard"); - - HANDLE handle = GetClipboardData(CF_UNICODETEXT); - if (handle == 0) - { - CloseClipboard(); - return std::string(); - } - - std::wstring::value_type* data = (std::wstring::value_type*)GlobalLock(handle); - if (data == 0) - { - CloseClipboard(); - return std::string(); - } - std::string str = from_utf16(data); - GlobalUnlock(handle); - - CloseClipboard(); - return str; -} - -void Win32Window::SetClipboardText(const std::string& text) -{ - std::wstring text16 = to_utf16(text); - - BOOL result = OpenClipboard(WindowHandle); - if (result == FALSE) - throw std::runtime_error("Unable to open clipboard"); - - result = EmptyClipboard(); - if (result == FALSE) - { - CloseClipboard(); - throw std::runtime_error("Unable to empty clipboard"); - } - - unsigned int length = (text16.length() + 1) * sizeof(std::wstring::value_type); - HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, length); - if (handle == 0) - { - CloseClipboard(); - throw std::runtime_error("Unable to allocate clipboard memory"); - } - - void* data = GlobalLock(handle); - if (data == 0) - { - GlobalFree(handle); - CloseClipboard(); - throw std::runtime_error("Unable to lock clipboard memory"); - } - memcpy(data, text16.c_str(), length); - GlobalUnlock(handle); - - HANDLE data_result = SetClipboardData(CF_UNICODETEXT, handle); - - if (data_result == 0) - { - GlobalFree(handle); - CloseClipboard(); - throw std::runtime_error("Unable to set clipboard data"); - } - - CloseClipboard(); -} - -void Win32Window::PresentBitmap(int width, int height, const uint32_t* pixels) -{ - BITMAPV5HEADER header = {}; - header.bV5Size = sizeof(BITMAPV5HEADER); - header.bV5Width = width; - header.bV5Height = -height; - header.bV5Planes = 1; - header.bV5BitCount = 32; - header.bV5Compression = BI_BITFIELDS; - header.bV5AlphaMask = 0xff000000; - header.bV5RedMask = 0x00ff0000; - header.bV5GreenMask = 0x0000ff00; - header.bV5BlueMask = 0x000000ff; - header.bV5SizeImage = width * height * sizeof(uint32_t); - header.bV5CSType = LCS_sRGB; - - HDC dc = PaintDC; - if (dc != 0) - { - int result = SetDIBitsToDevice(dc, 0, 0, width, height, 0, 0, 0, height, pixels, (const BITMAPINFO*)&header, BI_RGB); - ReleaseDC(WindowHandle, dc); - } -} - -LRESULT Win32Window::OnWindowMessage(UINT msg, WPARAM wparam, LPARAM lparam) -{ - LPARAM result = 0; - - if (DwmDefWindowProc(WindowHandle, msg, wparam, lparam, &result)) - return result; - - if (msg == WM_INPUT) - { - bool hasFocus = GetFocus() != 0; - - HRAWINPUT handle = (HRAWINPUT)lparam; - UINT size = 0; - UINT result = GetRawInputData(handle, RID_INPUT, 0, &size, sizeof(RAWINPUTHEADER)); - if (result == 0 && size > 0) - { - size *= 2; - std::vector buffer(size); - result = GetRawInputData(handle, RID_INPUT, buffer.data(), &size, sizeof(RAWINPUTHEADER)); - if (result >= 0) - { - RAWINPUT* rawinput = (RAWINPUT*)buffer.data(); - if (rawinput->header.dwType == RIM_TYPEMOUSE) - { - if (hasFocus) - WindowHost->OnWindowRawMouseMove(rawinput->data.mouse.lLastX, rawinput->data.mouse.lLastY); - } - } - } - return DefWindowProc(WindowHandle, msg, wparam, lparam); - } - else if (msg == WM_PAINT) - { - PAINTSTRUCT paintStruct = {}; - PaintDC = BeginPaint(WindowHandle, &paintStruct); - if (PaintDC) - { - WindowHost->OnWindowPaint(); - EndPaint(WindowHandle, &paintStruct); - PaintDC = 0; - } - return 0; - } - else if (msg == WM_ACTIVATE) - { - WindowHost->OnWindowActivated(); - } - else if (msg == WM_MOUSEMOVE) - { - if (MouseLocked && GetFocus() != 0) - { - RECT box = {}; - GetClientRect(WindowHandle, &box); - - POINT center = {}; - center.x = box.right / 2; - center.y = box.bottom / 2; - ClientToScreen(WindowHandle, ¢er); - - SetCursorPos(center.x, center.y); - } - else - { - UpdateCursor(); - } - - WindowHost->OnWindowMouseMove(GetLParamPos(lparam)); - } - else if (msg == WM_LBUTTONDOWN) - { - WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_LeftMouse); - } - else if (msg == WM_LBUTTONDBLCLK) - { - WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_LeftMouse); - } - else if (msg == WM_LBUTTONUP) - { - WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_LeftMouse); - } - else if (msg == WM_MBUTTONDOWN) - { - WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_MiddleMouse); - } - else if (msg == WM_MBUTTONDBLCLK) - { - WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_MiddleMouse); - } - else if (msg == WM_MBUTTONUP) - { - WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_MiddleMouse); - } - else if (msg == WM_RBUTTONDOWN) - { - WindowHost->OnWindowMouseDown(GetLParamPos(lparam), IK_RightMouse); - } - else if (msg == WM_RBUTTONDBLCLK) - { - WindowHost->OnWindowMouseDoubleclick(GetLParamPos(lparam), IK_RightMouse); - } - else if (msg == WM_RBUTTONUP) - { - WindowHost->OnWindowMouseUp(GetLParamPos(lparam), IK_RightMouse); - } - else if (msg == WM_MOUSEWHEEL) - { - double delta = GET_WHEEL_DELTA_WPARAM(wparam) / (double)WHEEL_DELTA; - - // Note: WM_MOUSEWHEEL uses screen coordinates. GetLParamPos assumes client coordinates. - double dpiscale = GetDpiScale(); - POINT pos; - pos.x = GET_X_LPARAM(lparam); - pos.y = GET_Y_LPARAM(lparam); - ScreenToClient(WindowHandle, &pos); - - WindowHost->OnWindowMouseWheel(Point(pos.x / dpiscale, pos.y / dpiscale), delta < 0.0 ? IK_MouseWheelDown : IK_MouseWheelUp); - } - else if (msg == WM_CHAR) - { - wchar_t buf[2] = { (wchar_t)wparam, 0 }; - WindowHost->OnWindowKeyChar(from_utf16(buf)); - } - else if (msg == WM_KEYDOWN) - { - WindowHost->OnWindowKeyDown((EInputKey)wparam); - } - else if (msg == WM_KEYUP) - { - WindowHost->OnWindowKeyUp((EInputKey)wparam); - } - else if (msg == WM_SETFOCUS) - { - if (MouseLocked) - { - ::ShowCursor(FALSE); - } - } - else if (msg == WM_KILLFOCUS) - { - if (MouseLocked) - { - ::ShowCursor(TRUE); - } - } - else if (msg == WM_CLOSE) - { - WindowHost->OnWindowClose(); - return 0; - } - else if (msg == WM_SIZE) - { - WindowHost->OnWindowGeometryChanged(); - return 0; - } - /*else if (msg == WM_NCCALCSIZE && wparam == TRUE) // calculate client area for the window - { - NCCALCSIZE_PARAMS* calcsize = (NCCALCSIZE_PARAMS*)lparam; - return WVR_REDRAW; - }*/ - - return DefWindowProc(WindowHandle, msg, wparam, lparam); -} - -void Win32Window::UpdateCursor() -{ - LPCWSTR cursor = IDC_ARROW; - switch (CurrentCursor) - { - case StandardCursor::arrow: cursor = IDC_ARROW; break; - case StandardCursor::appstarting: cursor = IDC_APPSTARTING; break; - case StandardCursor::cross: cursor = IDC_CROSS; break; - case StandardCursor::hand: cursor = IDC_HAND; break; - case StandardCursor::ibeam: cursor = IDC_IBEAM; break; - case StandardCursor::no: cursor = IDC_NO; break; - case StandardCursor::size_all: cursor = IDC_SIZEALL; break; - case StandardCursor::size_nesw: cursor = IDC_SIZENESW; break; - case StandardCursor::size_ns: cursor = IDC_SIZENS; break; - case StandardCursor::size_nwse: cursor = IDC_SIZENWSE; break; - case StandardCursor::size_we: cursor = IDC_SIZEWE; break; - case StandardCursor::uparrow: cursor = IDC_UPARROW; break; - case StandardCursor::wait: cursor = IDC_WAIT; break; - default: break; - } - - ::SetCursor((HCURSOR)LoadImage(0, cursor, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_DEFAULTSIZE, LR_SHARED)); -} - -Point Win32Window::GetLParamPos(LPARAM lparam) const -{ - double dpiscale = GetDpiScale(); - return Point(GET_X_LPARAM(lparam) / dpiscale, GET_Y_LPARAM(lparam) / dpiscale); -} - -LRESULT Win32Window::WndProc(HWND windowhandle, UINT msg, WPARAM wparam, LPARAM lparam) -{ - if (msg == WM_CREATE) - { - CREATESTRUCT* createstruct = (CREATESTRUCT*)lparam; - Win32Window* viewport = (Win32Window*)createstruct->lpCreateParams; - viewport->WindowHandle = windowhandle; - SetWindowLongPtr(windowhandle, GWLP_USERDATA, (LONG_PTR)viewport); - return viewport->OnWindowMessage(msg, wparam, lparam); - } - else - { - Win32Window* viewport = (Win32Window*)GetWindowLongPtr(windowhandle, GWLP_USERDATA); - if (viewport) - { - LRESULT result = viewport->OnWindowMessage(msg, wparam, lparam); - if (msg == WM_DESTROY) - { - SetWindowLongPtr(windowhandle, GWLP_USERDATA, 0); - viewport->WindowHandle = 0; - } - return result; - } - else - { - return DefWindowProc(windowhandle, msg, wparam, lparam); - } - } -} - -void Win32Window::ProcessEvents() -{ - while (true) - { - MSG msg = {}; - if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE) <= 0) - break; - TranslateMessage(&msg); - DispatchMessage(&msg); - } -} - -void Win32Window::RunLoop() -{ - while (!ExitRunLoop && !Windows.empty()) - { - MSG msg = {}; - if (GetMessage(&msg, 0, 0, 0) <= 0) - break; - TranslateMessage(&msg); - DispatchMessage(&msg); - } - ExitRunLoop = false; -} - -void Win32Window::ExitLoop() -{ - ExitRunLoop = true; -} - -Size Win32Window::GetScreenSize() -{ - HDC screenDC = GetDC(0); - int screenWidth = GetDeviceCaps(screenDC, HORZRES); - int screenHeight = GetDeviceCaps(screenDC, VERTRES); - double dpiScale = GetDeviceCaps(screenDC, LOGPIXELSX) / 96.0; - ReleaseDC(0, screenDC); - - return Size(screenWidth / dpiScale, screenHeight / dpiScale); -} - -static void CALLBACK Win32TimerCallback(HWND handle, UINT message, UINT_PTR timerID, DWORD timestamp) -{ - auto it = Win32Window::Timers.find(timerID); - if (it != Win32Window::Timers.end()) - { - it->second(); - } -} - -void* Win32Window::StartTimer(int timeoutMilliseconds, std::function onTimer) -{ - UINT_PTR result = SetTimer(0, 0, timeoutMilliseconds, Win32TimerCallback); - if (result == 0) - throw std::runtime_error("Could not create timer"); - Timers[result] = std::move(onTimer); - return (void*)result; -} - -void Win32Window::StopTimer(void* timerID) -{ - auto it = Timers.find((UINT_PTR)timerID); - if (it != Timers.end()) - { - Timers.erase(it); - KillTimer(0, (UINT_PTR)timerID); - } -} - -std::list Win32Window::Windows; -bool Win32Window::ExitRunLoop; - -std::unordered_map> Win32Window::Timers; diff --git a/libraries/ZWidget/src/window/window.cpp b/libraries/ZWidget/src/window/window.cpp index b67b1a296..5fc35b230 100644 --- a/libraries/ZWidget/src/window/window.cpp +++ b/libraries/ZWidget/src/window/window.cpp @@ -1,120 +1,201 @@ #include "window/window.h" +#include "window/stub/stub_open_folder_dialog.h" +#include "window/stub/stub_open_file_dialog.h" +#include "window/stub/stub_save_file_dialog.h" +#include "window/sdl2nativehandle.h" +#include "core/widget.h" #include -#ifdef _WIN32 - -#include "win32/win32window.h" - -std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) +std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) { - return std::make_unique(windowHost); + return DisplayBackend::Get()->Create(windowHost, popupWindow, owner, renderAPI); } void DisplayWindow::ProcessEvents() { - Win32Window::ProcessEvents(); + DisplayBackend::Get()->ProcessEvents(); } void DisplayWindow::RunLoop() { - Win32Window::RunLoop(); + DisplayBackend::Get()->RunLoop(); } void DisplayWindow::ExitLoop() { - Win32Window::ExitLoop(); -} - -Size DisplayWindow::GetScreenSize() -{ - return Win32Window::GetScreenSize(); + DisplayBackend::Get()->ExitLoop(); } void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) { - return Win32Window::StartTimer(timeoutMilliseconds, std::move(onTimer)); + return DisplayBackend::Get()->StartTimer(timeoutMilliseconds, onTimer); } void DisplayWindow::StopTimer(void* timerID) { - Win32Window::StopTimer(timerID); -} - -#elif defined(__APPLE__) - -std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) -{ - throw std::runtime_error("DisplayWindow::Create not implemented"); -} - -void DisplayWindow::ProcessEvents() -{ - throw std::runtime_error("DisplayWindow::ProcessEvents not implemented"); -} - -void DisplayWindow::RunLoop() -{ - throw std::runtime_error("DisplayWindow::RunLoop not implemented"); -} - -void DisplayWindow::ExitLoop() -{ - throw std::runtime_error("DisplayWindow::ExitLoop not implemented"); + DisplayBackend::Get()->StopTimer(timerID); } Size DisplayWindow::GetScreenSize() { - throw std::runtime_error("DisplayWindow::GetScreenSize not implemented"); + return DisplayBackend::Get()->GetScreenSize(); } -void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +///////////////////////////////////////////////////////////////////////////// + +static std::unique_ptr& GetBackendVar() { - throw std::runtime_error("DisplayWindow::StartTimer not implemented"); + // In C++, static variables in functions are constructed on first encounter and is destructed in the reverse order when main() ends. + static std::unique_ptr p; + return p; } -void DisplayWindow::StopTimer(void* timerID) +DisplayBackend* DisplayBackend::Get() { - throw std::runtime_error("DisplayWindow::StopTimer not implemented"); + return GetBackendVar().get(); +} + +void DisplayBackend::Set(std::unique_ptr instance) +{ + GetBackendVar() = std::move(instance); +} + +std::unique_ptr DisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + return std::make_unique(owner); +} + +std::unique_ptr DisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + return std::make_unique(owner); +} + +std::unique_ptr DisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + return std::make_unique(owner); +} + +#ifdef _MSC_VER +#pragma warning(disable: 4996) // warning C4996 : 'getenv' : This function or variable may be unsafe.Consider using _dupenv_s instead.To disable deprecation, use _CRT_SECURE_NO_WARNINGS.See online help for details. +#endif + +std::unique_ptr DisplayBackend::TryCreateBackend() +{ + std::unique_ptr backend; + + // Check if there is an environment variable specified for the desired backend + const char* backendSelectionEnv = std::getenv("ZWIDGET_DISPLAY_BACKEND"); + if (backendSelectionEnv) + { + std::string backendSelectionStr(backendSelectionEnv); + if (backendSelectionStr == "Win32") + { + backend = TryCreateWin32(); + } + else if (backendSelectionStr == "X11") + { + backend = TryCreateX11(); + } + else if (backendSelectionStr == "SDL2") + { + backend = TryCreateSDL2(); + } + } + + if (!backend) + { + backend = TryCreateWin32(); + if (!backend) backend = TryCreateWayland(); + if (!backend) backend = TryCreateX11(); + if (!backend) backend = TryCreateSDL2(); + } + + return backend; +} + +#ifdef WIN32 + +#include "win32/win32_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateWin32() +{ + return std::make_unique(); } #else -#include "sdl2/sdl2displaywindow.h" - -std::unique_ptr DisplayWindow::Create(DisplayWindowHost* windowHost) +std::unique_ptr DisplayBackend::TryCreateWin32() { - return std::make_unique(windowHost); + return nullptr; } -void DisplayWindow::ProcessEvents() +#endif + +#ifdef USE_SDL2 + +#include "sdl2/sdl2_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateSDL2() { - SDL2DisplayWindow::ProcessEvents(); + return std::make_unique(); } -void DisplayWindow::RunLoop() +#else + +std::unique_ptr DisplayBackend::TryCreateSDL2() { - SDL2DisplayWindow::RunLoop(); + return nullptr; } + +#endif + +#ifdef USE_X11 -void DisplayWindow::ExitLoop() +#include "x11/x11_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateX11() { - SDL2DisplayWindow::ExitLoop(); + try + { + return std::make_unique(); + } + catch (...) + { + return nullptr; + } } + +#else -Size DisplayWindow::GetScreenSize() +std::unique_ptr DisplayBackend::TryCreateX11() { - return SDL2DisplayWindow::GetScreenSize(); + return nullptr; } + +#endif -void* DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +#ifdef USE_WAYLAND + +#include "wayland/wayland_display_backend.h" + +std::unique_ptr DisplayBackend::TryCreateWayland() { - return SDL2DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); + try + { + return std::make_unique(); + } + catch (...) + { + return nullptr; + } } + +#else -void DisplayWindow::StopTimer(void* timerID) +std::unique_ptr DisplayBackend::TryCreateWayland() { - SDL2DisplayWindow::StopTimer(timerID); + return nullptr; } #endif diff --git a/libraries/ZWidget/src/window/x11/x11_display_backend.cpp b/libraries/ZWidget/src/window/x11/x11_display_backend.cpp new file mode 100644 index 000000000..b97760b3a --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_backend.cpp @@ -0,0 +1,70 @@ + +#include "x11_display_backend.h" +#include "x11_display_window.h" + +#ifdef USE_DBUS +#include "window/dbus/dbus_open_file_dialog.h" +#include "window/dbus/dbus_save_file_dialog.h" +#include "window/dbus/dbus_open_folder_dialog.h" +#endif + +std::unique_ptr X11DisplayBackend::Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) +{ + return std::make_unique(windowHost, popupWindow, static_cast(owner), renderAPI); +} + +void X11DisplayBackend::ProcessEvents() +{ + X11DisplayWindow::ProcessEvents(); +} + +void X11DisplayBackend::RunLoop() +{ + X11DisplayWindow::RunLoop(); +} + +void X11DisplayBackend::ExitLoop() +{ + X11DisplayWindow::ExitLoop(); +} + +Size X11DisplayBackend::GetScreenSize() +{ + return X11DisplayWindow::GetScreenSize(); +} + +void* X11DisplayBackend::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return X11DisplayWindow::StartTimer(timeoutMilliseconds, std::move(onTimer)); +} + +void X11DisplayBackend::StopTimer(void* timerID) +{ + X11DisplayWindow::StopTimer(timerID); +} + +#ifdef USE_DBUS +std::unique_ptr X11DisplayBackend::CreateOpenFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "x11:" + std::to_string(static_cast(owner)->window); + return std::make_unique(ownerHandle); +} + +std::unique_ptr X11DisplayBackend::CreateSaveFileDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "x11:" + std::to_string(static_cast(owner)->window); + return std::make_unique(ownerHandle); +} + +std::unique_ptr X11DisplayBackend::CreateOpenFolderDialog(DisplayWindow* owner) +{ + std::string ownerHandle; + if (owner) + ownerHandle = "x11:" + std::to_string(static_cast(owner)->window); + return std::make_unique(ownerHandle); +} +#endif diff --git a/libraries/ZWidget/src/window/x11/x11_display_backend.h b/libraries/ZWidget/src/window/x11/x11_display_backend.h new file mode 100644 index 000000000..9d8046353 --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_backend.h @@ -0,0 +1,25 @@ +#pragma once + +#include "window/window.h" + +class X11DisplayBackend : public DisplayBackend +{ +public: + std::unique_ptr Create(DisplayWindowHost* windowHost, bool popupWindow, DisplayWindow* owner, RenderAPI renderAPI) override; + void ProcessEvents() override; + void RunLoop() override; + void ExitLoop() override; + + void* StartTimer(int timeoutMilliseconds, std::function onTimer) override; + void StopTimer(void* timerID) override; + + Size GetScreenSize() override; + + bool IsX11() override { return true; } + +#ifdef USE_DBUS + std::unique_ptr CreateOpenFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateSaveFileDialog(DisplayWindow* owner) override; + std::unique_ptr CreateOpenFolderDialog(DisplayWindow* owner) override; +#endif +}; diff --git a/libraries/ZWidget/src/window/x11/x11_display_window.cpp b/libraries/ZWidget/src/window/x11/x11_display_window.cpp new file mode 100644 index 000000000..5797b0b4f --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_window.cpp @@ -0,0 +1,1184 @@ + +#include "x11_display_window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class X11Connection +{ +public: + X11Connection() + { + // If we ever want to support windows on multiple threads: + // XInitThreads(); + + display = XOpenDisplay(nullptr); + if (!display) + throw std::runtime_error("Could not open X11 display"); + + // Make auto-repeat keys detectable + Bool supports_detectable_autorepeat = {}; + XkbSetDetectableAutoRepeat(display, True, &supports_detectable_autorepeat); + + // Loads the XMODIFIERS environment variable to see what IME to use + XSetLocaleModifiers(""); + xim = XOpenIM(display, 0, 0, 0); + if (!xim) + { + // fallback to internal input method + XSetLocaleModifiers("@im=none"); + xim = XOpenIM(display, 0, 0, 0); + } + } + + ~X11Connection() + { + for (auto& it : standardCursors) + XFreeCursor(display, it.second); + if (xim) + XCloseIM(xim); + XCloseDisplay(display); + } + + Display* display = nullptr; + std::map atoms; + std::map windows; + std::map standardCursors; + bool ExitRunLoop = false; + + XIM xim = nullptr; +}; + +static X11Connection* GetX11Connection() +{ + static X11Connection connection; + return &connection; +} + +static Atom GetAtom(const std::string& name) +{ + auto connection = GetX11Connection(); + auto it = connection->atoms.find(name); + if (it != connection->atoms.end()) + return it->second; + + Atom atom = XInternAtom(connection->display, name.c_str(), True); + connection->atoms[name] = atom; + return atom; +} + +X11DisplayWindow::X11DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, X11DisplayWindow* owner, RenderAPI renderAPI) : windowHost(windowHost), owner(owner) +{ + display = GetX11Connection()->display; + + screen = XDefaultScreen(display); + depth = XDefaultDepth(display, screen); + visual = XDefaultVisual(display, screen); + colormap = XDefaultColormap(display, screen); + + int disp_width_px = XDisplayWidth(display, screen); + int disp_height_px = XDisplayHeight(display, screen); + int disp_width_mm = XDisplayWidthMM(display, screen); + double ppi = (disp_width_mm < 24) ? 96.0 : (25.4 * static_cast(disp_width_px) / static_cast(disp_width_mm)); + dpiScale = std::round(ppi / 96.0 * 4.0) / 4.0; // 100%, 125%, 150%, 175%, 200%, etc. + + XSetWindowAttributes attributes = {}; + attributes.backing_store = Always; + attributes.override_redirect = popupWindow ? True : False; + attributes.save_under = popupWindow ? True : False; + attributes.colormap = colormap; + attributes.event_mask = + KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | + EnterWindowMask | LeaveWindowMask | PointerMotionMask | KeymapStateMask | + ExposureMask | StructureNotifyMask | FocusChangeMask | PropertyChangeMask; + + unsigned long mask = CWBackingStore | CWSaveUnder | CWEventMask | CWOverrideRedirect; + + window = XCreateWindow(display, XRootWindow(display, screen), 0, 0, 100, 100, 0, depth, InputOutput, visual, mask, &attributes); + GetX11Connection()->windows[window] = this; + + if (owner) + { + XSetTransientForHint(display, window, owner->window); + } + + // Tell window manager which process this window came from + if (GetAtom("_NET_WM_PID") != None) + { + int32_t pid = getpid(); + if (pid != 0) + { + XChangeProperty(display, window, GetAtom("_NET_WM_PID"), XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&pid, 1); + } + } + + // Tell window manager which machine this window came from + if (GetAtom("WM_CLIENT_MACHINE") != None) + { + std::vector hostname(256); + if (gethostname(hostname.data(), hostname.size()) >= 0) + { + hostname.push_back(0); + XChangeProperty(display, window, GetAtom("WM_CLIENT_MACHINE"), XA_STRING, 8, PropModeReplace, (unsigned char *)hostname.data(), strlen(hostname.data())); + } + } + + // Tell window manager we want to listen to close events + if (GetAtom("WM_DELETE_WINDOW") != None) + { + Atom protocol = GetAtom("WM_DELETE_WINDOW"); + XSetWMProtocols(display, window, &protocol, 1); + } + + // Tell window manager what type of window we are + if (GetAtom("_NET_WM_WINDOW_TYPE") != None) + { + Atom type = None; + if (popupWindow) + { + type = GetAtom("_NET_WM_WINDOW_TYPE_DROPDOWN_MENU"); + if (type == None) + type = GetAtom("_NET_WM_WINDOW_TYPE_POPUP_MENU"); + if (type == None) + type = GetAtom("_NET_WM_WINDOW_TYPE_COMBO"); + } + if (type == None) + type = GetAtom("_NET_WM_WINDOW_TYPE_NORMAL"); + + if (type != None) + { + XChangeProperty(display, window, GetAtom("_NET_WM_WINDOW_TYPE"), XA_ATOM, 32, PropModeReplace, (unsigned char *)&type, 1); + } + } + + // Create input context + if (GetX11Connection()->xim) + { + xic = XCreateIC( + GetX11Connection()->xim, + XNInputStyle, + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, window, + XNFocusWindow, window, + nullptr); + } +} + +X11DisplayWindow::~X11DisplayWindow() +{ + if (hidden_cursor != None) + { + XFreeCursor(display, hidden_cursor); + XFreePixmap(display, cursor_bitmap); + } + + DestroyBackbuffer(); + XDestroyWindow(display, window); + GetX11Connection()->windows.erase(GetX11Connection()->windows.find(window)); +} + +void X11DisplayWindow::SetWindowTitle(const std::string& text) +{ + XSetStandardProperties(display, window, text.c_str(), text.c_str(), None, nullptr, 0, nullptr); +} + +void X11DisplayWindow::SetWindowFrame(const Rect& box) +{ + // To do: this requires cooperation with the window manager + + SetClientFrame(box); +} + +void X11DisplayWindow::SetClientFrame(const Rect& box) +{ + double dpiscale = GetDpiScale(); + int x = (int)std::round(box.x * dpiscale); + int y = (int)std::round(box.y * dpiscale); + int width = (int)std::round(box.width * dpiscale); + int height = (int)std::round(box.height * dpiscale); + + XWindowChanges changes = {}; + changes.x = x; + changes.y = y; + changes.width = width; + changes.height = height; + unsigned int mask = CWX | CWY | CWWidth | CWHeight; + + XConfigureWindow(display, window, mask, &changes); +} + +void X11DisplayWindow::Show() +{ + if (!isMapped) + { + XMapRaised(display, window); + isMapped = true; + } +} + +void X11DisplayWindow::ShowFullscreen() +{ + Show(); + + if (GetAtom("_NET_WM_STATE") != None && GetAtom("_NET_WM_STATE_FULLSCREEN") != None) + { + Atom state = GetAtom("_NET_WM_STATE_FULLSCREEN"); + XChangeProperty(display, window, GetAtom("_NET_WM_STATE"), XA_ATOM, 32, PropModeReplace, (unsigned char *)&state, 1); + isFullscreen = true; + } +} + +bool X11DisplayWindow::IsWindowFullscreen() +{ + return isFullscreen; +} + +void X11DisplayWindow::ShowMaximized() +{ + Show(); +} + +void X11DisplayWindow::ShowMinimized() +{ + if (!isMinimized) + { + Show(); // To do: can this be avoided? WMHints has an initial state that can make it show minimized + XIconifyWindow(display, window, screen); + isMinimized = true; + } +} + +void X11DisplayWindow::ShowNormal() +{ + Show(); + isFullscreen = false; +} + +void X11DisplayWindow::Hide() +{ + if (isMapped) + { + XUnmapWindow(display, window); + isMapped = false; + } +} + +void X11DisplayWindow::Activate() +{ + XRaiseWindow(display, window); +} + +void X11DisplayWindow::ShowCursor(bool enable) +{ + if (isCursorEnabled != enable) + { + isCursorEnabled = enable; + UpdateCursor(); + } +} + +void X11DisplayWindow::LockCursor() +{ + ShowCursor(false); +} + +void X11DisplayWindow::UnlockCursor() +{ + ShowCursor(true); +} + +void X11DisplayWindow::CaptureMouse() +{ + ShowCursor(false); +} + +void X11DisplayWindow::ReleaseMouseCapture() +{ + ShowCursor(true); +} + +void X11DisplayWindow::Update() +{ + needsUpdate = true; +} + +bool X11DisplayWindow::GetKeyState(InputKey key) +{ + auto it = keyState.find(key); + return it != keyState.end() ? it->second : false; +} + +void X11DisplayWindow::SetCursor(StandardCursor newcursor) +{ + if (cursor != newcursor) + { + cursor = newcursor; + UpdateCursor(); + } +} + +void X11DisplayWindow::UpdateCursor() +{ + if (isCursorEnabled) + { + Cursor& x11cursor = GetX11Connection()->standardCursors[cursor]; + if (x11cursor == None) + { + unsigned int index = XC_left_ptr; + switch (cursor) + { + default: + case StandardCursor::arrow: index = XC_left_ptr; break; + case StandardCursor::appstarting: index = XC_watch; break; + case StandardCursor::cross: index = XC_cross; break; + case StandardCursor::hand: index = XC_hand2; break; + case StandardCursor::ibeam: index = XC_xterm; break; + case StandardCursor::size_all: index = XC_fleur; break; + case StandardCursor::size_ns: index = XC_double_arrow; break; + case StandardCursor::size_we: index = XC_sb_h_double_arrow; break; + case StandardCursor::uparrow: index = XC_sb_up_arrow; break; + case StandardCursor::wait: index = XC_watch; break; + case StandardCursor::no: index = XC_X_cursor; break; + case StandardCursor::size_nesw: break; // To do: need to map this + case StandardCursor::size_nwse: break; + } + x11cursor = XCreateFontCursor(display, index); + } + XDefineCursor(display, window, x11cursor); + } + else + { + if (hidden_cursor == None) + { + char data[64] = {}; + XColor black_color = {}; + cursor_bitmap = XCreateBitmapFromData(display, window, data, 8, 8); + hidden_cursor = XCreatePixmapCursor(display, cursor_bitmap, cursor_bitmap, &black_color, &black_color, 0, 0); + } + XDefineCursor(display, window, hidden_cursor); + } +} + +Rect X11DisplayWindow::GetWindowFrame() const +{ + // To do: this needs to include the window manager frame + + double dpiscale = GetDpiScale(); + + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + + return Rect::xywh(x / dpiscale, y / dpiscale, width / dpiscale, height / dpiscale); +} + +Size X11DisplayWindow::GetClientSize() const +{ + double dpiscale = GetDpiScale(); + + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + + return Size(width / dpiscale, height / dpiscale); +} + +int X11DisplayWindow::GetPixelWidth() const +{ + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + return width; +} + +int X11DisplayWindow::GetPixelHeight() const +{ + Window root = {}; + int x = 0; + int y = 0; + unsigned int width = 0; + unsigned int height = 0; + unsigned int borderwidth = 0; + unsigned int depth = 0; + Status status = XGetGeometry(display, window, &root, &x, &y, &width, &height, &borderwidth, &depth); + return height; +} + +double X11DisplayWindow::GetDpiScale() const +{ + return dpiScale; +} + +void X11DisplayWindow::CreateBackbuffer(int width, int height) +{ + backbuffer.pixels = malloc(width * height * sizeof(uint32_t)); + backbuffer.image = XCreateImage(display, DefaultVisual(display, screen), depth, ZPixmap, 0, (char*)backbuffer.pixels, width, height, 32, 0); + backbuffer.pixmap = XCreatePixmap(display, window, width, height, depth); + backbuffer.width = width; + backbuffer.height = height; +} + +void X11DisplayWindow::DestroyBackbuffer() +{ + if (backbuffer.width > 0 && backbuffer.height > 0) + { + XDestroyImage(backbuffer.image); + XFreePixmap(display, backbuffer.pixmap); + backbuffer.width = 0; + backbuffer.height = 0; + backbuffer.pixmap = None; + backbuffer.image = nullptr; + backbuffer.pixels = nullptr; + } +} + +void X11DisplayWindow::PresentBitmap(int width, int height, const uint32_t* pixels) +{ + if (backbuffer.width != width || backbuffer.height != height) + { + DestroyBackbuffer(); + if (width > 0 && height > 0) + CreateBackbuffer(width, height); + } + + if (backbuffer.width == width && backbuffer.height == height) + { + memcpy(backbuffer.pixels, pixels, width * height * sizeof(uint32_t)); + GC gc = XDefaultGC(display, screen); + XPutImage(display, backbuffer.pixmap, gc, backbuffer.image, 0, 0, 0, 0, width, height); + XCopyArea(display, backbuffer.pixmap, window, gc, 0, 0, width, height, BlackPixel(display, screen), WhitePixel(display, screen)); + } +} + +void X11DisplayWindow::SetBorderColor(uint32_t bgra8) +{ +} + +void X11DisplayWindow::SetCaptionColor(uint32_t bgra8) +{ +} + +void X11DisplayWindow::SetCaptionTextColor(uint32_t bgra8) +{ +} + +std::vector X11DisplayWindow::GetWindowProperty(Atom property, Atom &actual_type, int &actual_format, unsigned long &item_count) +{ + long read_bytes = 0; + Atom _actual_type = actual_type; + int _actual_format = actual_format; + unsigned long _item_count = item_count; + unsigned long bytes_remaining = 0; + unsigned char *read_data = nullptr; + do + { + int result = XGetWindowProperty( + display, window, property, 0ul, read_bytes, + False, AnyPropertyType, &actual_type, &actual_format, + &_item_count, &bytes_remaining, &read_data); + if (result != Success) + { + actual_type = None; + actual_format = 0; + item_count = 0; + return {}; + } + } while (bytes_remaining > 0); + + item_count = _item_count; + if (!read_data) + return {}; + std::vector buffer(read_data, read_data + read_bytes); + XFree(read_data); + return buffer; +} + +std::string X11DisplayWindow::GetClipboardText() +{ + Atom clipboard = GetAtom("CLIPBOARD"); + if (clipboard == None) + return {}; + + XConvertSelection(display, clipboard, XA_STRING, clipboard, window, CurrentTime); + XFlush(display); + + // Wait 500 ms for a response + XEvent event = {}; + while (true) + { + if (XCheckTypedWindowEvent(display, window, SelectionNotify, &event)) + break; + if (!WaitForEvents(500)) + return {}; + } + + Atom type = None; + int format = 0; + unsigned long count = 0; + std::vector data = GetWindowProperty(clipboard, type, format, count); + if (type != XA_STRING || format != 8 || count <= 0 || data.empty()) + return {}; + + data.push_back(0); + return (char*)data.data(); +} + +void X11DisplayWindow::SetClipboardText(const std::string& text) +{ + clipboardText = text; + + Atom clipboard = GetAtom("CLIPBOARD"); + if (clipboard == None) + return; + + XSetSelectionOwner(display, XA_PRIMARY, window, CurrentTime); + XSetSelectionOwner(display, clipboard, window, CurrentTime); +} + +Point X11DisplayWindow::MapFromGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + Window root = XRootWindow(display, screen); + Window child = {}; + int srcx = (int)std::round(pos.x * dpiscale); + int srcy = (int)std::round(pos.y * dpiscale); + int destx = 0; + int desty = 0; + Bool result = XTranslateCoordinates(display, root, window, srcx, srcy, &destx, &desty, &child); + return Point(destx / dpiscale, desty / dpiscale); +} + +Point X11DisplayWindow::MapToGlobal(const Point& pos) const +{ + double dpiscale = GetDpiScale(); + Window root = XRootWindow(display, screen); + Window child = {}; + int srcx = (int)std::round(pos.x * dpiscale); + int srcy = (int)std::round(pos.y * dpiscale); + int destx = 0; + int desty = 0; + Bool result = XTranslateCoordinates(display, window, root, srcx, srcy, &destx, &desty, &child); + return Point(destx / dpiscale, desty / dpiscale); +} + +void* X11DisplayWindow::GetNativeHandle() +{ + return reinterpret_cast(window); +} + +bool X11DisplayWindow::WaitForEvents(int timeout) +{ + Display* display = GetX11Connection()->display; + int fd = XConnectionNumber(display); + + struct timeval tv; + if (timeout > 0) + { + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) / 1000; + } + + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + int result = select(fd + 1, &rfds, nullptr, nullptr, timeout >= 0 ? &tv : nullptr); + return result > 0 && FD_ISSET(fd, &rfds); +} + +void X11DisplayWindow::CheckNeedsUpdate() +{ + for (auto& it : GetX11Connection()->windows) + { + if (it.second->needsUpdate) + { + it.second->needsUpdate = false; + it.second->windowHost->OnWindowPaint(); + } + } +} + +void X11DisplayWindow::ProcessEvents() +{ + CheckNeedsUpdate(); + Display* display = GetX11Connection()->display; + while (XPending(display) > 0) + { + XEvent event = {}; + XNextEvent(display, &event); + DispatchEvent(&event); + } +} + +void X11DisplayWindow::RunLoop() +{ + X11Connection* connection = GetX11Connection(); + connection->ExitRunLoop = false; + while (!connection->ExitRunLoop && !connection->windows.empty()) + { + CheckNeedsUpdate(); + XEvent event = {}; + XNextEvent(connection->display, &event); + DispatchEvent(&event); + } +} + +void X11DisplayWindow::ExitLoop() +{ + X11Connection* connection = GetX11Connection(); + connection->ExitRunLoop = true; +} + +void X11DisplayWindow::DispatchEvent(XEvent* event) +{ + X11Connection* connection = GetX11Connection(); + auto it = connection->windows.find(event->xany.window); + if (it != connection->windows.end()) + { + X11DisplayWindow* window = it->second; + window->OnEvent(event); + } +} + +void X11DisplayWindow::OnEvent(XEvent* event) +{ + if (event->type == ConfigureNotify) + OnConfigureNotify(event); + else if (event->type == ClientMessage) + OnClientMessage(event); + else if (event->type == Expose) + OnExpose(event); + else if (event->type == FocusIn) + OnFocusIn(event); + else if (event->type == FocusOut) + OnFocusOut(event); + else if (event->type == PropertyNotify) + OnPropertyNotify(event); + else if (event->type == KeyPress) + OnKeyPress(event); + else if (event->type == KeyRelease) + OnKeyRelease(event); + else if (event->type == ButtonPress) + OnButtonPress(event); + else if (event->type == ButtonRelease) + OnButtonRelease(event); + else if (event->type == MotionNotify) + OnMotionNotify(event); + else if (event->type == LeaveNotify) + OnLeaveNotify(event); + else if (event->type == SelectionClear) + OnSelectionClear(event); + else if (event->type == SelectionNotify) + OnSelectionNotify(event); + else if (event->type == SelectionRequest) + OnSelectionRequest(event); +} + +void X11DisplayWindow::OnConfigureNotify(XEvent* event) +{ + ClientSizeX = event->xconfigure.width; + ClientSizeY = event->xconfigure.height; + windowHost->OnWindowGeometryChanged(); +} + +void X11DisplayWindow::OnClientMessage(XEvent* event) +{ + Atom protocolsAtom = GetAtom("WM_PROTOCOLS"); + if (protocolsAtom != None && event->xclient.message_type == protocolsAtom) + { + Atom deleteAtom = GetAtom("WM_DELETE_WINDOW"); + Atom pingAtom = GetAtom("_NET_WM_PING"); + + Atom protocol = event->xclient.data.l[0]; + if (deleteAtom != None && protocol == deleteAtom) + { + windowHost->OnWindowClose(); + } + else if (pingAtom != None && protocol == pingAtom) + { + XSendEvent(display, RootWindow(display, screen), False, SubstructureNotifyMask | SubstructureRedirectMask, event); + } + } +} + +void X11DisplayWindow::OnExpose(XEvent* event) +{ + windowHost->OnWindowPaint(); +} + +void X11DisplayWindow::OnFocusIn(XEvent* event) +{ + if (xic) + XSetICFocus(xic); + + windowHost->OnWindowActivated(); +} + +void X11DisplayWindow::OnFocusOut(XEvent* event) +{ + windowHost->OnWindowDeactivated(); +} + +void X11DisplayWindow::OnPropertyNotify(XEvent* event) +{ + // Sent when window is minimized, maximized, etc. +} + +InputKey X11DisplayWindow::GetInputKey(XEvent* event) +{ + if (event->type == KeyPress || event->type == KeyRelease) + { + KeySym keysymbol = XkbKeycodeToKeysym(display, event->xkey.keycode, 0, 0); + switch (keysymbol) + { + case XK_BackSpace: return InputKey::Backspace; + case XK_Tab: return InputKey::Tab; + case XK_Return: return InputKey::Enter; + // To do: should we merge them or not? Windows merges them + case XK_Shift_L: return InputKey::Shift; // InputKey::LShift + case XK_Shift_R: return InputKey::Shift; // InputKey::RShift + case XK_Control_L: return InputKey::Ctrl; // InputKey::LControl + case XK_Control_R: return InputKey::Ctrl; // InputKey::RControl + case XK_Meta_L: return InputKey::Alt; + case XK_Meta_R: return InputKey::Alt; + case XK_Pause: return InputKey::Pause; + case XK_Caps_Lock: return InputKey::CapsLock; + case XK_Escape: return InputKey::Escape; + case XK_space: return InputKey::Space; + case XK_Page_Up: return InputKey::PageUp; + case XK_Page_Down: return InputKey::PageDown; + case XK_End: return InputKey::End; + case XK_Home: return InputKey::Home; + case XK_Left: return InputKey::Left; + case XK_Up: return InputKey::Up; + case XK_Right: return InputKey::Right; + case XK_Down: return InputKey::Down; + case XK_Print: return InputKey::Print; + case XK_Execute: return InputKey::Execute; + // case XK_Print_Screen: return InputKey::PrintScrn; + case XK_Insert: return InputKey::Insert; + case XK_Delete: return InputKey::Delete; + case XK_Help: return InputKey::Help; + case XK_0: return InputKey::_0; + case XK_1: return InputKey::_1; + case XK_2: return InputKey::_2; + case XK_3: return InputKey::_3; + case XK_4: return InputKey::_4; + case XK_5: return InputKey::_5; + case XK_6: return InputKey::_6; + case XK_7: return InputKey::_7; + case XK_8: return InputKey::_8; + case XK_9: return InputKey::_9; + case XK_A: case XK_a: return InputKey::A; + case XK_B: case XK_b: return InputKey::B; + case XK_C: case XK_c: return InputKey::C; + case XK_D: case XK_d: return InputKey::D; + case XK_E: case XK_e: return InputKey::E; + case XK_F: case XK_f: return InputKey::F; + case XK_G: case XK_g: return InputKey::G; + case XK_H: case XK_h: return InputKey::H; + case XK_I: case XK_i: return InputKey::I; + case XK_J: case XK_j: return InputKey::J; + case XK_K: case XK_k: return InputKey::K; + case XK_L: case XK_l: return InputKey::L; + case XK_M: case XK_m: return InputKey::M; + case XK_N: case XK_n: return InputKey::N; + case XK_O: case XK_o: return InputKey::O; + case XK_P: case XK_p: return InputKey::P; + case XK_Q: case XK_q: return InputKey::Q; + case XK_R: case XK_r: return InputKey::R; + case XK_S: case XK_s: return InputKey::S; + case XK_T: case XK_t: return InputKey::T; + case XK_U: case XK_u: return InputKey::U; + case XK_V: case XK_v: return InputKey::V; + case XK_W: case XK_w: return InputKey::W; + case XK_X: case XK_x: return InputKey::X; + case XK_Y: case XK_y: return InputKey::Y; + case XK_Z: case XK_z: return InputKey::Z; + case XK_KP_0: return InputKey::NumPad0; + case XK_KP_1: return InputKey::NumPad1; + case XK_KP_2: return InputKey::NumPad2; + case XK_KP_3: return InputKey::NumPad3; + case XK_KP_4: return InputKey::NumPad4; + case XK_KP_5: return InputKey::NumPad5; + case XK_KP_6: return InputKey::NumPad6; + case XK_KP_7: return InputKey::NumPad7; + case XK_KP_8: return InputKey::NumPad8; + case XK_KP_9: return InputKey::NumPad9; + case XK_KP_Multiply: return InputKey::GreyStar; + case XK_KP_Add: return InputKey::GreyPlus; + case XK_KP_Separator: return InputKey::Separator; + case XK_KP_Subtract: return InputKey::GreyMinus; + case XK_KP_Decimal: return InputKey::NumPadPeriod; + case XK_KP_Divide: return InputKey::GreySlash; + case XK_F1: return InputKey::F1; + case XK_F2: return InputKey::F2; + case XK_F3: return InputKey::F3; + case XK_F4: return InputKey::F4; + case XK_F5: return InputKey::F5; + case XK_F6: return InputKey::F6; + case XK_F7: return InputKey::F7; + case XK_F8: return InputKey::F8; + case XK_F9: return InputKey::F9; + case XK_F10: return InputKey::F10; + case XK_F11: return InputKey::F11; + case XK_F12: return InputKey::F12; + case XK_F13: return InputKey::F13; + case XK_F14: return InputKey::F14; + case XK_F15: return InputKey::F15; + case XK_F16: return InputKey::F16; + case XK_F17: return InputKey::F17; + case XK_F18: return InputKey::F18; + case XK_F19: return InputKey::F19; + case XK_F20: return InputKey::F20; + case XK_F21: return InputKey::F21; + case XK_F22: return InputKey::F22; + case XK_F23: return InputKey::F23; + case XK_F24: return InputKey::F24; + case XK_Num_Lock: return InputKey::NumLock; + case XK_Scroll_Lock: return InputKey::ScrollLock; + case XK_semicolon: return InputKey::Semicolon; + case XK_equal: return InputKey::Equals; + case XK_comma: return InputKey::Comma; + case XK_minus: return InputKey::Minus; + case XK_period: return InputKey::Period; + case XK_slash: return InputKey::Slash; + case XK_dead_tilde: return InputKey::Tilde; + case XK_bracketleft: return InputKey::LeftBracket; + case XK_backslash: return InputKey::Backslash; + case XK_bracketright: return InputKey::RightBracket; + case XK_apostrophe: return InputKey::SingleQuote; + default: return (InputKey)(((uint32_t)keysymbol) << 8); + } + } + else if (event->type == ButtonPress || event->type == ButtonRelease) + { + switch (event->xbutton.button) + { + case 1: return InputKey::LeftMouse; + case 2: return InputKey::MiddleMouse; + case 3: return InputKey::RightMouse; + case 4: return InputKey::MouseWheelUp; + case 5: return InputKey::MouseWheelDown; + // case 6: return InputKey::XButton1; + // case 7: return InputKey::XButton2; + default: break; + } + } + return {}; +} + +Point X11DisplayWindow::GetMousePos(XEvent* event) +{ + double dpiScale = GetDpiScale(); + int x = event->xbutton.x; + int y = event->xbutton.y; + return Point(x / dpiScale, y / dpiScale); +} + +void X11DisplayWindow::OnKeyPress(XEvent* event) +{ + // If we ever want to track keypress repeat: + // char keyboard_state[32]; + // XQueryKeymap(display, keyboard_state); + // unsigned int keycode = event->xkey.keycode; + // bool isrepeat = event->type == KeyPress && keyboard_state[keycode / 8] & (1 << keycode % 8); + + InputKey key = GetInputKey(event); + keyState[key] = true; + windowHost->OnWindowKeyDown(key); + + std::string text; + if (xic) // utf-8 text input + { + Status status = {}; + KeySym keysym = NoSymbol; + char buffer[32] = {}; + Xutf8LookupString(xic, &event->xkey, buffer, sizeof(buffer) - 1, &keysym, &status); + if (status == XLookupChars || status == XLookupBoth) + text = buffer; + } + else // latin-1 input fallback + { + const int buff_size = 16; + char buff[buff_size]; + int result = XLookupString(&event->xkey, buff, buff_size - 1, nullptr, nullptr); + if (result < 0) result = 0; + if (result > (buff_size - 1)) result = buff_size - 1; + buff[result] = 0; + text = std::string(buff, result); + + // Lazy way to convert to utf-8 + for (char& c : text) + { + if (c < 0) + c = '?'; + } + } + + if (!text.empty()) + windowHost->OnWindowKeyChar(std::move(text)); +} + +void X11DisplayWindow::OnKeyRelease(XEvent* event) +{ + InputKey key = GetInputKey(event); + keyState[key] = false; + windowHost->OnWindowKeyUp(key); +} + +void X11DisplayWindow::OnButtonPress(XEvent* event) +{ + InputKey key = GetInputKey(event); + keyState[key] = true; + windowHost->OnWindowMouseDown(GetMousePos(event), key); + // if (lastClickWithin400ms) + // windowHost->OnWindowMouseDoubleclick(GetMousePos(event), InputKey::LeftMouse); +} + +void X11DisplayWindow::OnButtonRelease(XEvent* event) +{ + InputKey key = GetInputKey(event); + keyState[key] = false; + windowHost->OnWindowMouseUp(GetMousePos(event), key); +} + +void X11DisplayWindow::OnMotionNotify(XEvent* event) +{ + double dpiScale = GetDpiScale(); + int x = event->xmotion.x; + int y = event->xmotion.y; + if (isCursorEnabled) + { + windowHost->OnWindowMouseMove(Point(x / dpiScale, y / dpiScale)); + } + else + { + MouseX = ClientSizeX / 2; + MouseY = ClientSizeY / 2; + + if (MouseX != -1 && MouseY != -1) + { + windowHost->OnWindowRawMouseMove(x - MouseX, y - MouseY); + } + + // Warp pointer to the center of the window + XWarpPointer(display, window, window, 0, 0, ClientSizeX, ClientSizeY, ClientSizeX / 2, ClientSizeY / 2); + } + +} + +void X11DisplayWindow::OnLeaveNotify(XEvent* event) +{ + windowHost->OnWindowMouseLeave(); +} + +void X11DisplayWindow::OnSelectionClear(XEvent* event) +{ + clipboardText.clear(); +} + +void X11DisplayWindow::OnSelectionNotify(XEvent* event) +{ + // This is handled in GetClipboardText +} + +void X11DisplayWindow::OnSelectionRequest(XEvent* event) +{ + Atom requestor = event->xselectionrequest.requestor; + if (requestor == window) + return; + + Atom targetsAtom = GetAtom("TARGETS"); + Atom multipleAtom = GetAtom("MULTIPLE"); + + struct Request { Window target; Atom property; }; + std::vector requests; + + if (event->xselectionrequest.target == multipleAtom) + { + Atom actualType = None; + int actualFormat = 0; + unsigned long itemCount = 0; + std::vector data = GetWindowProperty(requestor, actualType, actualFormat, itemCount); + if (data.size() < itemCount * sizeof(Atom)) + return; + + Atom* atoms = (Atom*)data.data(); + for (unsigned long i = 0; i + 1 < itemCount; i += 2) + { + requests.push_back({ atoms[i], atoms[i + 1]}); + } + } + else + { + requests.push_back({ event->xselectionrequest.target, event->xselectionrequest.property }); + } + + for (const Request& request : requests) + { + Window xtarget = request.target; + Atom xproperty = request.property; + + XEvent response = {}; + response.xselection.type = SelectionNotify; + response.xselection.display = event->xselectionrequest.display; + response.xselection.requestor = event->xselectionrequest.requestor; + response.xselection.selection = event->xselectionrequest.selection; + response.xselection.target = event->xselectionrequest.target; + response.xselection.property = xproperty; + response.xselection.time = event->xselectionrequest.time; + + if (xtarget == targetsAtom) + { + Atom newTargets = XA_STRING; + XChangeProperty(display, requestor, xproperty, targetsAtom, 32, PropModeReplace, (unsigned char *)&newTargets, 1); + } + else if (xtarget == XA_STRING) + { + XChangeProperty(display, requestor, xproperty, xtarget, 8, PropModeReplace, (const unsigned char*)clipboardText.c_str(), clipboardText.size()); + } + else + { + response.xselection.property = None; // Is this correct? + } + + XSendEvent(display, requestor, False, 0, &response); + } +} + +Size X11DisplayWindow::GetScreenSize() +{ + X11Connection* connection = GetX11Connection(); + Display* display = connection->display; + int screen = XDefaultScreen(display); + + int disp_width_px = XDisplayWidth(display, screen); + int disp_height_px = XDisplayHeight(display, screen); + int disp_width_mm = XDisplayWidthMM(display, screen); + double ppi = (disp_width_mm < 24) ? 96.0 : (25.4 * static_cast(disp_width_px) / static_cast(disp_width_mm)); + double dpiScale = ppi / 96.0; + + return Size(disp_width_px / dpiScale, disp_height_px / dpiScale); +} + +void* X11DisplayWindow::StartTimer(int timeoutMilliseconds, std::function onTimer) +{ + return nullptr; +} + +void X11DisplayWindow::StopTimer(void* timerID) +{ +} + +// This is to avoid needing all the Vulkan headers and the volk binding library just for this: +#ifndef VK_VERSION_1_0 + +#define VKAPI_CALL +#define VKAPI_PTR VKAPI_CALL + +typedef uint32_t VkFlags; +typedef enum VkStructureType { VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR = 1000004000, VK_OBJECT_TYPE_MAX_ENUM = 0x7FFFFFFF } VkStructureType; +typedef enum VkResult { VK_SUCCESS = 0, VK_RESULT_MAX_ENUM = 0x7FFFFFFF } VkResult; +typedef struct VkAllocationCallbacks VkAllocationCallbacks; + +typedef void (VKAPI_PTR* PFN_vkVoidFunction)(void); +typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_vkGetInstanceProcAddr)(VkInstance instance, const char* pName); + +#ifndef VK_KHR_xlib_surface + +typedef VkFlags VkXlibSurfaceCreateFlagsKHR; +typedef struct VkXlibSurfaceCreateInfoKHR +{ + VkStructureType sType; + const void* pNext; + VkXlibSurfaceCreateFlagsKHR flags; + Display* dpy; + Window window; +} VkXlibSurfaceCreateInfoKHR; + +typedef VkResult(VKAPI_PTR* PFN_vkCreateXlibSurfaceKHR)(VkInstance instance, const VkXlibSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface); + +#endif +#endif + +class ZWidgetX11VulkanLoader +{ +public: + ZWidgetX11VulkanLoader() + { +#if defined(__APPLE__) + module = dlopen("libvulkan.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.1.dylib", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libMoltenVK.dylib", RTLD_NOW | RTLD_LOCAL); +#else + module = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL); + if (!module) + module = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); +#endif + + if (!module) + throw std::runtime_error("Could not load vulkan"); + + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(module, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) + { + dlclose(module); + throw std::runtime_error("vkGetInstanceProcAddr not found"); + } + } + + ~ZWidgetX11VulkanLoader() + { + dlclose(module); + } + + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = nullptr; + void* module = nullptr; +}; + +VkSurfaceKHR X11DisplayWindow::CreateVulkanSurface(VkInstance instance) +{ + static ZWidgetX11VulkanLoader loader; + + auto vkCreateXlibSurfaceKHR = (PFN_vkCreateXlibSurfaceKHR)loader.vkGetInstanceProcAddr(instance, "vkCreateXlibSurfaceKHR"); + if (!vkCreateXlibSurfaceKHR) + throw std::runtime_error("Could not create vulkan surface"); + + VkXlibSurfaceCreateInfoKHR createInfo = { VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR }; + createInfo.dpy = display; + createInfo.window = window; + + VkSurfaceKHR surface = {}; + VkResult result = vkCreateXlibSurfaceKHR(instance, &createInfo, nullptr, &surface); + if (result != VK_SUCCESS) + throw std::runtime_error("Could not create vulkan surface"); + return surface; +} + +std::vector X11DisplayWindow::GetVulkanInstanceExtensions() +{ + return { "VK_KHR_surface", "VK_KHR_xlib_surface" }; +} diff --git a/libraries/ZWidget/src/window/x11/x11_display_window.h b/libraries/ZWidget/src/window/x11/x11_display_window.h new file mode 100644 index 000000000..62a63f151 --- /dev/null +++ b/libraries/ZWidget/src/window/x11/x11_display_window.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class X11DisplayWindow : public DisplayWindow +{ +public: + X11DisplayWindow(DisplayWindowHost* windowHost, bool popupWindow, X11DisplayWindow* owner, RenderAPI renderAPI); + ~X11DisplayWindow(); + + void SetWindowTitle(const std::string& text) override; + void SetWindowFrame(const Rect& box) override; + void SetClientFrame(const Rect& box) override; + void Show() override; + void ShowFullscreen() override; + void ShowMaximized() override; + void ShowMinimized() override; + void ShowNormal() override; + bool IsWindowFullscreen() override; + void Hide() override; + void Activate() override; + void ShowCursor(bool enable) override; + void LockCursor() override; + void UnlockCursor() override; + void CaptureMouse() override; + void ReleaseMouseCapture() override; + void Update() override; + bool GetKeyState(InputKey key) override; + + void SetCursor(StandardCursor cursor) override; + + Rect GetWindowFrame() const override; + Size GetClientSize() const override; + int GetPixelWidth() const override; + int GetPixelHeight() const override; + double GetDpiScale() const override; + + void PresentBitmap(int width, int height, const uint32_t* pixels) override; + + void SetBorderColor(uint32_t bgra8) override; + void SetCaptionColor(uint32_t bgra8) override; + void SetCaptionTextColor(uint32_t bgra8) override; + + std::string GetClipboardText() override; + void SetClipboardText(const std::string& text) override; + + Point MapFromGlobal(const Point& pos) const override; + Point MapToGlobal(const Point& pos) const override; + + void* GetNativeHandle() override; + + std::vector GetVulkanInstanceExtensions() override; + VkSurfaceKHR CreateVulkanSurface(VkInstance instance) override; + + static void ProcessEvents(); + static void RunLoop(); + static void ExitLoop(); + static Size GetScreenSize(); + static void* StartTimer(int timeoutMilliseconds, std::function onTimer); + static void StopTimer(void* timerID); + +private: + void UpdateCursor(); + + void OnEvent(XEvent* event); + void OnConfigureNotify(XEvent* event); + void OnClientMessage(XEvent* event); + void OnExpose(XEvent* event); + void OnFocusIn(XEvent* event); + void OnFocusOut(XEvent* event); + void OnPropertyNotify(XEvent* event); + void OnKeyPress(XEvent* event); + void OnKeyRelease(XEvent* event); + void OnButtonPress(XEvent* event); + void OnButtonRelease(XEvent* event); + void OnMotionNotify(XEvent* event); + void OnLeaveNotify(XEvent* event); + void OnSelectionClear(XEvent* event); + void OnSelectionNotify(XEvent* event); + void OnSelectionRequest(XEvent* event); + + void CreateBackbuffer(int width, int height); + void DestroyBackbuffer(); + + InputKey GetInputKey(XEvent* event); + Point GetMousePos(XEvent* event); + + static bool WaitForEvents(int timeout); + static void DispatchEvent(XEvent* event); + + static void CheckNeedsUpdate(); + + std::vector GetWindowProperty(Atom property, Atom &actual_type, int &actual_format, unsigned long &item_count); + + DisplayWindowHost* windowHost = nullptr; + X11DisplayWindow* owner = nullptr; + Display* display = nullptr; + Window window = {}; + int screen = 0; + int depth = 0; + Visual* visual = nullptr; + Colormap colormap = {}; + XIC xic = nullptr; + StandardCursor cursor = {}; + bool isCursorEnabled = true; + bool isMapped = false; + bool isMinimized = false; + bool isFullscreen = false; + double dpiScale = 1.0; + + int ClientSizeX = 0; + int ClientSizeY = 0; + int MouseX = -1; + int MouseY = -1; + + Pixmap cursor_bitmap = None; + Cursor hidden_cursor = None; + + std::map keyState; + + std::string clipboardText; + + struct + { + Pixmap pixmap = None; + XImage* image = nullptr; + void* pixels = nullptr; + int width = 0; + int height = 0; + } backbuffer; + + bool needsUpdate = false; + + friend class X11DisplayBackend; +}; diff --git a/libraries/ZWidget/src/window/ztimer/ztimer.cpp b/libraries/ZWidget/src/window/ztimer/ztimer.cpp new file mode 100644 index 000000000..23e82d10a --- /dev/null +++ b/libraries/ZWidget/src/window/ztimer/ztimer.cpp @@ -0,0 +1,73 @@ +#include "ztimer.h" + +ZTimer::ZTimer() +{ +} + +ZTimer::ZTimer(Duration duration_ms) : m_timerDuration(duration_ms) +{ +} + +ZTimer::ZTimer(Duration duration_ms, CallbackFunc callback) + : m_timerDuration(duration_ms), m_callback(callback) +{ +} + +ZTimer::~ZTimer() +{ + Stop(); +} + +void ZTimer::Start() +{ + m_startTime = Clock::now(); + m_currentTime = m_startTime; + m_timerStarted = true; + m_timerFinished = false; +} + +void ZTimer::Stop() +{ + m_timerStarted = false; +} + +void ZTimer::SetDuration(Duration duration_ms) +{ + if (m_timerStarted) + return; + m_timerDuration = duration_ms; +} + +void ZTimer::SetCallback(std::function callback) +{ + if (m_timerStarted) + return; + m_callback = callback; +} + +void ZTimer::SetRepeating(bool value) +{ + if (m_timerStarted) + return; + m_repeatingTimer = value; +} + +void ZTimer::Update(Duration deltaTime) +{ + if (!m_timerStarted) + return; + + m_currentTime += deltaTime; + + if (m_currentTime >= m_startTime + m_timerDuration) + { + m_callback(); + if (!m_repeatingTimer) + { + Stop(); + m_timerFinished = true; + } + else + Start(); + } +} diff --git a/libraries/ZWidget/src/window/ztimer/ztimer.h b/libraries/ZWidget/src/window/ztimer/ztimer.h new file mode 100644 index 000000000..e72489884 --- /dev/null +++ b/libraries/ZWidget/src/window/ztimer/ztimer.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +// ZTimer: A small, independent timer +// Useful for implementing timed events on your backends +class ZTimer +{ +public: + using Duration = std::chrono::duration; + using Clock = std::chrono::system_clock; + using TimePoint = std::chrono::time_point; + using CallbackFunc = std::function; + + ZTimer(); + ZTimer(Duration duration_ms); + ZTimer(Duration duration_ms, CallbackFunc callback); + + ~ZTimer(); + + void Start(); + void Stop(); + void SetDuration(Duration duration_ms); + void SetCallback(CallbackFunc callback); + void SetRepeating(bool value); + void Update(Duration deltaTime); + + bool IsStarted() { return m_timerStarted; } + bool IsFinished() { return m_timerFinished; } + +private: + bool m_timerStarted = false; + bool m_repeatingTimer = false; + bool m_timerFinished = false; + + TimePoint m_startTime; + TimePoint m_currentTime; + Duration m_timerDuration; + CallbackFunc m_callback; +}; diff --git a/src/common/widgets/errorwindow.cpp b/src/common/widgets/errorwindow.cpp index e0af0730b..d74864bb6 100644 --- a/src/common/widgets/errorwindow.cpp +++ b/src/common/widgets/errorwindow.cpp @@ -6,14 +6,16 @@ #include #include #include +#include +#include -bool ErrorWindow::ExecModal(const std::string& text, const std::string& log) +bool ErrorWindow::ExecModal(const std::string& text, const std::string& log, std::vector minidump) { Size screenSize = GetScreenSize(); double windowWidth = 1200.0; double windowHeight = 700.0; - auto window = std::make_unique(); + auto window = std::make_unique(std::move(minidump)); window->SetText(text, log); window->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight); window->Show(); @@ -23,7 +25,7 @@ bool ErrorWindow::ExecModal(const std::string& text, const std::string& log) return window->Restart; } -ErrorWindow::ErrorWindow() : Widget(nullptr, WidgetType::Window) +ErrorWindow::ErrorWindow(std::vector initminidump) : Widget(nullptr, WidgetType::Window), minidump(std::move(initminidump)) { FStringf caption("Fatal Error - " GAMENAME " %s (%s)", GetVersionString(), GetGitTime()); SetWindowTitle(caption.GetChars()); @@ -34,13 +36,21 @@ ErrorWindow::ErrorWindow() : Widget(nullptr, WidgetType::Window) LogView = new LogViewer(this); ClipboardButton = new PushButton(this); - RestartButton = new PushButton(this); - ClipboardButton->OnClick = [=]() { OnClipboardButtonClicked(); }; - RestartButton->OnClick = [=]() { OnRestartButtonClicked(); }; - ClipboardButton->SetText("Copy to clipboard"); - RestartButton->SetText("Restart"); + + if (minidump.empty()) + { + RestartButton = new PushButton(this); + RestartButton->OnClick = [=]() { OnRestartButtonClicked(); }; + RestartButton->SetText("Restart"); + } + else + { + SaveReportButton = new PushButton(this); + SaveReportButton->OnClick = [=]() { OnSaveReportButtonClicked(); }; + SaveReportButton->SetText("Save Report"); + } LogView->SetFocus(); } @@ -83,6 +93,37 @@ void ErrorWindow::OnRestartButtonClicked() DisplayWindow::ExitLoop(); } +void ErrorWindow::OnSaveReportButtonClicked() +{ + auto dialog = SaveFileDialog::Create(this); + dialog->AddFilter("Crash Report Zip Files", "*.zip"); + dialog->AddFilter("All Files", "*.*"); + dialog->SetFilename("CrashReport.zip"); + dialog->SetDefaultExtension("zip"); + if (dialog->Show()) + { + std::string filename = dialog->Filename(); + + mz_zip_archive zip = {}; + if (mz_zip_writer_init_heap(&zip, 0, 16 * 1024 * 1024)) + { + mz_zip_writer_add_mem(&zip, "minidump.dmp", minidump.data(), minidump.size(), MZ_DEFAULT_COMPRESSION); + mz_zip_writer_add_mem(&zip, "log.txt", clipboardtext.data(), clipboardtext.size(), MZ_DEFAULT_COMPRESSION); + } + void* buffer = nullptr; + size_t buffersize = 0; + mz_zip_writer_finalize_heap_archive(&zip, &buffer, &buffersize); + mz_zip_writer_end(&zip); + + std::unique_ptr f(FileWriter::Open(filename.c_str())); + if (f) + { + f->Write(buffer, buffersize); + f->Close(); + } + } +} + void ErrorWindow::OnClose() { Restart = false; @@ -96,7 +137,10 @@ void ErrorWindow::OnGeometryChanged() double y = GetHeight() - 15.0 - ClipboardButton->GetPreferredHeight(); ClipboardButton->SetFrameGeometry(20.0, y, 170.0, ClipboardButton->GetPreferredHeight()); - RestartButton->SetFrameGeometry(GetWidth() - 20.0 - 100.0, y, 100.0, RestartButton->GetPreferredHeight()); + if (RestartButton) + RestartButton->SetFrameGeometry(GetWidth() - 20.0 - 100.0, y, 100.0, RestartButton->GetPreferredHeight()); + else if (SaveReportButton) + SaveReportButton->SetFrameGeometry(GetWidth() - 20.0 - 100.0, y, 100.0, SaveReportButton->GetPreferredHeight()); y -= 20.0; LogView->SetFrameGeometry(Rect::xywh(0.0, 0.0, w, y)); @@ -212,44 +256,44 @@ void LogViewer::OnPaint(Canvas* canvas) } } -bool LogViewer::OnMouseWheel(const Point& pos, EInputKey key) +bool LogViewer::OnMouseWheel(const Point& pos, InputKey key) { - if (key == IK_MouseWheelUp) + if (key == InputKey::MouseWheelUp) { ScrollUp(4); } - else if (key == IK_MouseWheelDown) + else if (key == InputKey::MouseWheelDown) { ScrollDown(4); } return true; } -void LogViewer::OnKeyDown(EInputKey key) +void LogViewer::OnKeyDown(InputKey key) { - if (key == IK_Home) + if (key == InputKey::Home) { scrollbar->SetPosition(0.0f); Update(); } - if (key == IK_End) + if (key == InputKey::End) { scrollbar->SetPosition(scrollbar->GetMax()); Update(); } - else if (key == IK_PageUp) + else if (key == InputKey::PageUp) { ScrollUp(20); } - else if (key == IK_PageDown) + else if (key == InputKey::PageDown) { ScrollDown(20); } - else if (key == IK_Up) + else if (key == InputKey::Up) { ScrollUp(4); } - else if (key == IK_Down) + else if (key == InputKey::Down) { ScrollDown(4); } diff --git a/src/common/widgets/errorwindow.h b/src/common/widgets/errorwindow.h index f38eb676c..1cb9b236d 100644 --- a/src/common/widgets/errorwindow.h +++ b/src/common/widgets/errorwindow.h @@ -10,9 +10,9 @@ class Scrollbar; class ErrorWindow : public Widget { public: - static bool ExecModal(const std::string& text, const std::string& log); + static bool ExecModal(const std::string& text, const std::string& log, std::vector minidump = {}); - ErrorWindow(); + ErrorWindow(std::vector minidump); bool Restart = false; @@ -25,11 +25,14 @@ private: void OnClipboardButtonClicked(); void OnRestartButtonClicked(); + void OnSaveReportButtonClicked(); LogViewer* LogView = nullptr; PushButton* ClipboardButton = nullptr; PushButton* RestartButton = nullptr; + PushButton* SaveReportButton = nullptr; + std::vector minidump; std::string clipboardtext; }; @@ -43,8 +46,8 @@ public: protected: void OnPaintFrame(Canvas* canvas) override; void OnPaint(Canvas* canvas) override; - bool OnMouseWheel(const Point& pos, EInputKey key) override; - void OnKeyDown(EInputKey key) override; + bool OnMouseWheel(const Point& pos, InputKey key) override; + void OnKeyDown(InputKey key) override; void OnGeometryChanged() override; private: diff --git a/src/common/widgets/widgetresourcedata.cpp b/src/common/widgets/widgetresourcedata.cpp index 13f151932..849b6d553 100644 --- a/src/common/widgets/widgetresourcedata.cpp +++ b/src/common/widgets/widgetresourcedata.cpp @@ -1,5 +1,6 @@ #include +#include #include "filesystem.h" #include "printf.h" #include "zstring.h" @@ -11,21 +12,31 @@ FResourceFile* WidgetResources; +bool IsZWidgetAvailable() +{ + return WidgetResources; +} + void InitWidgetResources(const char* filename) { WidgetResources = FResourceFile::OpenResourceFile(filename); if (!WidgetResources) I_FatalError("Unable to open %s", filename); + + WidgetTheme::SetTheme(std::make_unique()); } void CloseWidgetResources() { - if (WidgetResources) delete WidgetResources; + delete WidgetResources; WidgetResources = nullptr; } static std::vector LoadFile(const char* name) { + if (!WidgetResources) + I_FatalError("InitWidgetResources has not been called"); + auto lump = WidgetResources->FindEntry(name); if (lump == -1) I_FatalError("Unable to find %s", name); diff --git a/src/d_main.cpp b/src/d_main.cpp index 28e96e5a6..7ebd1c40c 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -118,6 +118,7 @@ #include "screenjob.h" #include "startscreen.h" #include "shiftstate.h" +#include "common/widgets/errorwindow.h" #ifdef __unix__ #include "i_system.h" // for SHARE_DIR @@ -3750,6 +3751,15 @@ static int D_DoomMain_Internal (void) int GameMain() { + // On Windows, prefer the native win32 backend. + // On other platforms, use SDL until the other backends are more mature. + auto zwidget = DisplayBackend::TryCreateWin32(); + if (!zwidget) + zwidget = DisplayBackend::TryCreateSDL2(); + if (!zwidget) + return -1; + DisplayBackend::Set(std::move(zwidget)); + int ret = 0; GameTicRate = TICRATE; I_InitTime(); diff --git a/src/launcher/launcherwindow.cpp b/src/launcher/launcherwindow.cpp index 11f1079d4..c4b84df82 100644 --- a/src/launcher/launcherwindow.cpp +++ b/src/launcher/launcherwindow.cpp @@ -7,6 +7,7 @@ #include "version.h" #include "i_interface.h" #include "gstrings.h" +#include "c_cvars.h" #include #include #include @@ -31,10 +32,6 @@ int LauncherWindow::ExecModal(WadStuff* wads, int numwads, int defaultiwad, int* LauncherWindow::LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags) : Widget(nullptr, WidgetType::Window) { - SetWindowBackground(Colorf::fromRgba8(51, 51, 51)); - SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51)); - SetWindowCaptionColor(Colorf::fromRgba8(33, 33, 33)); - SetWindowCaptionTextColor(Colorf::fromRgba8(226, 223, 219)); SetWindowTitle(GAMENAME); Banner = new LauncherBanner(this); diff --git a/src/launcher/playgamepage.cpp b/src/launcher/playgamepage.cpp index d561a672b..84ac64ad1 100644 --- a/src/launcher/playgamepage.cpp +++ b/src/launcher/playgamepage.cpp @@ -40,16 +40,16 @@ PlayGamePage::PlayGamePage(LauncherWindow* launcher, WadStuff* wads, int numwads GamesList->OnActivated = [=]() { OnGamesListActivated(); }; } -std::string PlayGamePage::GetExtraArgs() -{ - return ParametersEdit->GetText(); -} - void PlayGamePage::SetExtraArgs(const std::string& args) { ParametersEdit->SetText(args); } +std::string PlayGamePage::GetExtraArgs() +{ + return ParametersEdit->GetText(); +} + int PlayGamePage::GetSelectedGame() { return GamesList->GetSelectedItem(); diff --git a/src/launcher/playgamepage.h b/src/launcher/playgamepage.h index d54ba2ca1..79e92e67b 100644 --- a/src/launcher/playgamepage.h +++ b/src/launcher/playgamepage.h @@ -16,6 +16,7 @@ public: void SetExtraArgs(const std::string& args); std::string GetExtraArgs(); + int GetSelectedGame(); private: diff --git a/src/launcher/settingspage.h b/src/launcher/settingspage.h index a65f4f164..77a6ff048 100644 --- a/src/launcher/settingspage.h +++ b/src/launcher/settingspage.h @@ -3,7 +3,7 @@ #include #include "gstrings.h" -#define RENDER_BACKENDS +// #define RENDER_BACKENDS class LauncherWindow; class TextLabel; From 7cac623ecbe8b8e44e3cf4f55288d65ad669681f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 18 Jun 2025 10:33:16 -0400 Subject: [PATCH 214/384] Fix SMF_PRECISE flag for seekers Use the player's actual eye position when calculating. --- src/playsim/p_mobj.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 072afdd3c..8b0ac17f4 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -2374,7 +2374,7 @@ bool P_SeekerMissile (AActor *actor, DAngle thresh, DAngle turnMax, bool precise double aimheight = target->Height/2; if (target->player) { - aimheight = target->player->DefaultViewHeight(); + aimheight = target->player->viewz - target->Z(); } pitch = DVector2(dist, target->Z() + aimheight - actor->Center()).Angle(); } From 00a5bce5d78a74da4b461e6e091c406e29073c5f Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Sat, 21 Jun 2025 16:50:03 +0800 Subject: [PATCH 215/384] Rebrand the launcher to make it more distinctly GZDoom - Use a Light theme to clearly differentiate GZDoom from VKDoom - Moved the version label to not visually clash with the banner graphics - Added unique graphics for the banner and the BOOTLOGO - Changed the loading bar color to match GZDoom's logo --- .../startscreen/startscreen_generic.cpp | 2 +- src/common/widgets/widgetresourcedata.cpp | 2 +- src/launcher/launcherbanner.cpp | 11 ----------- src/launcher/launcherbanner.h | 2 -- src/launcher/launcherwindow.cpp | 1 - src/launcher/playgamepage.cpp | 7 +++++++ src/launcher/playgamepage.h | 1 + wadsrc/static/graphics/bootlogo.png | Bin 0 -> 95363 bytes wadsrc/static/widgets/banner.png | Bin 144351 -> 396260 bytes 9 files changed, 10 insertions(+), 16 deletions(-) create mode 100644 wadsrc/static/graphics/bootlogo.png diff --git a/src/common/startscreen/startscreen_generic.cpp b/src/common/startscreen/startscreen_generic.cpp index b76cbd3e2..df8c43c94 100644 --- a/src/common/startscreen/startscreen_generic.cpp +++ b/src/common/startscreen/startscreen_generic.cpp @@ -106,7 +106,7 @@ bool FGenericStartScreen::DoProgress(int advance) if (CurPos < MaxPos) { - RgbQuad bcolor = { 2, 25, 87, 255 }; // todo: make configurable + RgbQuad bcolor = { 177, 77, 16, 255 }; // [Nash June 2025] don't merge this color into VKDoom! // todo: make configurable int numnotches = 200 * 2; notch_pos = ((CurPos + 1) * numnotches) / MaxPos; if (notch_pos != NotchPos) diff --git a/src/common/widgets/widgetresourcedata.cpp b/src/common/widgets/widgetresourcedata.cpp index 849b6d553..a4e2c027a 100644 --- a/src/common/widgets/widgetresourcedata.cpp +++ b/src/common/widgets/widgetresourcedata.cpp @@ -23,7 +23,7 @@ void InitWidgetResources(const char* filename) if (!WidgetResources) I_FatalError("Unable to open %s", filename); - WidgetTheme::SetTheme(std::make_unique()); + WidgetTheme::SetTheme(std::make_unique()); // GZDoom uses a different theme than VKDoom } void CloseWidgetResources() diff --git a/src/launcher/launcherbanner.cpp b/src/launcher/launcherbanner.cpp index 823b0021d..8e834c5d9 100644 --- a/src/launcher/launcherbanner.cpp +++ b/src/launcher/launcherbanner.cpp @@ -9,19 +9,9 @@ LauncherBanner::LauncherBanner(Widget* parent) : Widget(parent) { Logo = new ImageBox(this); - VersionLabel = new TextLabel(this); - VersionLabel->SetTextAlignment(TextLabelAlignment::Right); - Logo->SetImage(Image::LoadResource("widgets/banner.png")); } -void LauncherBanner::UpdateLanguage() -{ - FString versionText = GStrings.GetString("PICKER_VERSION"); - versionText.Substitute("%s", GetVersionString()); - VersionLabel->SetText(versionText.GetChars()); -} - double LauncherBanner::GetPreferredHeight() const { return Logo->GetPreferredHeight(); @@ -30,5 +20,4 @@ double LauncherBanner::GetPreferredHeight() const void LauncherBanner::OnGeometryChanged() { Logo->SetFrameGeometry(0.0, 0.0, GetWidth(), Logo->GetPreferredHeight()); - VersionLabel->SetFrameGeometry(20.0, GetHeight() - 10.0 - VersionLabel->GetPreferredHeight(), GetWidth() - 40.0, VersionLabel->GetPreferredHeight()); } diff --git a/src/launcher/launcherbanner.h b/src/launcher/launcherbanner.h index 9a27a5694..0bfee5c99 100644 --- a/src/launcher/launcherbanner.h +++ b/src/launcher/launcherbanner.h @@ -9,7 +9,6 @@ class LauncherBanner : public Widget { public: LauncherBanner(Widget* parent); - void UpdateLanguage(); double GetPreferredHeight() const; @@ -17,5 +16,4 @@ private: void OnGeometryChanged() override; ImageBox* Logo = nullptr; - TextLabel* VersionLabel = nullptr; }; diff --git a/src/launcher/launcherwindow.cpp b/src/launcher/launcherwindow.cpp index c4b84df82..b60db7cf8 100644 --- a/src/launcher/launcherwindow.cpp +++ b/src/launcher/launcherwindow.cpp @@ -68,7 +68,6 @@ void LauncherWindow::UpdateLanguage() { Pages->SetTabText(PlayGame, GStrings.GetString("PICKER_TAB_PLAY")); Pages->SetTabText(Settings, GStrings.GetString("OPTMNU_TITLE")); - Banner->UpdateLanguage(); PlayGame->UpdateLanguage(); Settings->UpdateLanguage(); Buttonbar->UpdateLanguage(); diff --git a/src/launcher/playgamepage.cpp b/src/launcher/playgamepage.cpp index 84ac64ad1..53676b94e 100644 --- a/src/launcher/playgamepage.cpp +++ b/src/launcher/playgamepage.cpp @@ -11,6 +11,8 @@ PlayGamePage::PlayGamePage(LauncherWindow* launcher, WadStuff* wads, int numwads, int defaultiwad) : Widget(nullptr), Launcher(launcher) { WelcomeLabel = new TextLabel(this); + VersionLabel = new TextLabel(this); + VersionLabel->SetTextAlignment(TextLabelAlignment::Right); SelectLabel = new TextLabel(this); ParametersLabel = new TextLabel(this); GamesList = new ListView(this); @@ -62,6 +64,9 @@ void PlayGamePage::UpdateLanguage() FString welcomeText = GStrings.GetString("PICKER_WELCOME"); welcomeText.Substitute("%s", GAMENAME); WelcomeLabel->SetText(welcomeText.GetChars()); + FString versionText = GStrings.GetString("PICKER_VERSION"); + versionText.Substitute("%s", GetVersionString()); + VersionLabel->SetText(versionText.GetChars()); } void PlayGamePage::OnGamesListActivated() @@ -81,6 +86,8 @@ void PlayGamePage::OnGeometryChanged() WelcomeLabel->SetFrameGeometry(0.0, y, GetWidth(), WelcomeLabel->GetPreferredHeight()); y += WelcomeLabel->GetPreferredHeight(); + VersionLabel->SetFrameGeometry(20.0, y - VersionLabel->GetPreferredHeight(), GetWidth() - 19.0, VersionLabel->GetPreferredHeight()); + y += 10.0; SelectLabel->SetFrameGeometry(0.0, y, GetWidth(), SelectLabel->GetPreferredHeight()); diff --git a/src/launcher/playgamepage.h b/src/launcher/playgamepage.h index 79e92e67b..dd3bd170a 100644 --- a/src/launcher/playgamepage.h +++ b/src/launcher/playgamepage.h @@ -27,6 +27,7 @@ private: LauncherWindow* Launcher = nullptr; TextLabel* WelcomeLabel = nullptr; + TextLabel* VersionLabel = nullptr; TextLabel* SelectLabel = nullptr; TextLabel* ParametersLabel = nullptr; ListView* GamesList = nullptr; diff --git a/wadsrc/static/graphics/bootlogo.png b/wadsrc/static/graphics/bootlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..63dd52c790f165abdf15726e8222c8ef1881462a GIT binary patch literal 95363 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4mJh`hM1xiX$%YuoCO|{#S9F5M?jcysy3fA z0|S?Trn7TEKt_H^esM;Afr6*AvqC{pep+TuDg#5st+~PJA;B-jY`@?8;^f`YFvUAR zNR&e%$f>(QWTKjuBBy{?hvLGUqJakmU0qqEPb}zQ?(1vl5e?Mg>EgP<{zP$6U-ROQ zu2a7sRxi%Jw&(k^v;VeUw|lQz`~GlIyvbvLx3(rf_nY4%M1;3f6T0AWQd-kD9X5C zB7=f(YKIF$hBd>Q^I=`C3=swlB5B@7EE#U-G8lMT#JFBV&*2Bohp{DjnN#sh3w(sdU6`)JjJy| z%o3RrXF2E{s+=il)M�PV*|l=S%D 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

EhbLF(ocL6-XqEEl{z?a|tM^5SgC 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 literal 0 HcmV?d00001 diff --git a/wadsrc/static/widgets/banner.png b/wadsrc/static/widgets/banner.png index 96ac9a0931edac97dc97167d726f890ee55c00c6..22bc39c785b9208a5a18e5cf067a79b1e90fb565 100644 GIT binary patch literal 396260 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

f8KdkQG{-tmI`(4vE7Jgi@eP33KMM&(!8r#5h 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


e&c0k^FEI+Eru_X z`Y$E3FZld(P3(bpGLa2pvJ5GC2KOJ8bp|I+J8U97;g*Kml15f-yU6FWYwv~B&RcEI z=-3^~#M&_X<15GFz~gRP4?M{&UF&gaF9Yl5hT5~uZ%d_BUIfOj=-Tx%F(k&)A#uVSJit z${BkepU&6a|F8GU$-8C7^QS4?`Ek0lC^_~ck5-=JLl&Q%@0Z&u7;-+n`{ec`&r~UO zk;}Jtzdif)S5ej8W#)~Gr5NmwKR&S1H}`IM{rAuQ`z7p5UxYuqU$FiCy8rL==l-3( z{#5q8rG3wz%byYZxMl|59y=G){(XL_?@ztaeqVc|OmbZTyGN}7fBf&6@fvYWCYAeT zf7v!2h`EuLba73vq8^W7a;NTd_PXM`i=J|=H=Wyh*Yj}jnMJ(LVZ2SzSr$PFCm$(r zy?LO(D0Pzi$T3-^>2KPDvN-0vNQf+UH8|T6o7Z-I+ z$Dq~VPNq&mN|W)KGmBdqWRlkJWNcP$m@-wvXLCnwpOV*vH^s$o7xD9HJ5D%T9_*Je zfnimOaIu26qt~MiXMHDVPWmo3vD%GAV6ouE6z>(wCaAJ44C+a|oapawlCixjS@^Yd zt4Z`yp#?J*3g5E+&XVSM?ykb`%~c(o=i0bAg6= z$}`L93OlzgUU9#8#p+op8-C5t)|31ovB>{-(6W|WOM`q&jRaPf7hg`&TQyDhz?vEC z%nL=HWJ)P6y*_OtgCYN+;XBnhMY>YXi_$xc$KyO2R!=it|uU-dUDYbYLz{95y{nEht zxbRjfmaSbI9x2P174ZJJ@iwjY{<@guOTI41+p5o>@FC^!M*d))_H(+=nQOzgPJ1z_ zLtcLAy$74GoV!zCaVtc<>ggS(RjgG9dL{31{aBX9WV`0URD+1?OPYCB+DpdNPv847 zFW}E|`>5W0W1J=95<@(0l)<;lF+Db-y<< z2!BxBA`vzrf6=uZi~_gt{<)=3V!9!o91v z673eUUs#|zVYjc@%w2~*_Dx$Iu;F2cs71qsth(4{g;|k38*_7Z-_G73DQ0ZD^O&&< zhoSG15095!u|864Dcz-Iy)ea!`PbHcd`fn9n|LNJ(AfS<&0))<6289~uS|Wy?kx63~4R7dn0u2XE`SGPqO=>FYvIr9D9 z>)-u1`rJ%(TqHZ`vul#X#>nT>TSEGeGworMTRd51hE~v;o^28reJl1}>tQ}B%XRhy z-zyW*qZ3T4UX||eEZ3fM=-uwRxy&>7ZC&?cqFdsV*vlI>?>zixihSB*@7lono_p7~ zd+>iu`LuHD)9QP%FN-@^C+_XJJY#+Q6!-aGG>`57^mOz6Uym;5$Nou<|9@=#tsU3( z<7+CnSN{CiO?Pl2W z=5Ks(bHl4gclWv5Zkpxie63a3aHA_{Kf8A4-w#JBd32RnC5T)CT+bMNU%-@{AwjhlNdesY}Nz;jno zCxJC>LDYk_0r&fCx@R8NEPp?DPF~!f58eN7*MFJ+kN@A+`gduY%T3Ft3*_a$&&$sb z_E9hj4V_R!p}IbrE3?kcoOyej>~X{A zF{Wk4yZ7w*Q(gc5dlr}N^G#8@|G&)tXI%ev{lBZ?`t$U~yvviHEzA7(vi{fnpXbW= z{hhu4r+L=Hvj#l%fBOG_Twn8a^6uTc_kRliw?F@0%(>^E&!+wTy1xD||KEl2LKEfx zJYc{7_g(p|ZKo@v)`so>mR+xM?1%ZkWBKpOq9-2k@x9FFygp0liFo}(@#&|ZTA#Vs zEX6n9AXhMIL&Bb>z6Wd{S<+tEyH_zpI#j>0^S->aI7#Tcjc<7Vd+Uv?3P)Je-ZjpU zDd1SKjfGXw!kYc7;=-AH`a(=CEf1c#-f2>5Z$A7&fyLdsO=^zuhTXyp=Dl;el#`nr z7s{UJxG3_9tC_=P#{u`bR$C4piDe8>S$nHnxk*$okcg{JPcz9voAv57KBGnA5&hZ$} zPuzFeI?dks)x|z5hFZ2=Mzgn`K6LcYf)&CUGR8_(cUjgPVR{p=`-S^i^>qIaYpU4i z9p8NW&#voneU@c1%U{mAny9j(XKGXC_Sf5Y&z*R_w)EQEm|xm=R|eQVpL`%~hv5>b zSFGVIj|?LB_oxS${bwTJYcuJ*`zq`>aXPw zPNePsezRQvMdrspv-TzME8LlGlk$S~-tRw`uj|xY+uJ#hy?grKN~M_Sml(hXj zv;BUI-gD`_a|{A|FCFHP5o9$Ko1^pV^KyB8xf+HAJAAVyHtpp5z@ENv!I|Bx%NE|> zSG9$Muh-|M(1K-4j~V-?t+l)xSI@ZU4WCLyVpr^?nZ7evml=v*XmE%8KdN^SvR_uCX4R@)hznW4yg;--7C>6Wf} ziHvK`^_-E>bkX;+JLs~R)oW(jJ7z^;bJpC3UCya$FB&Hvc;$O7f$!VJDP0Ez7!1lM zGoE1C9+jxIR&eur%{5mxznm+hkgTfuX6m}PPCt^Ctl21-$z5T*DDZ}hS&y;V{oHlZ zC)Whuf1cT5)vdj5d*$uK37RZ3y{siw-)`S|f7-F29?hd#YVUs^>^q$>*Y5NQ#S=WS z_B@wooYnDnZ9fyBA6cEXMPR|QY1~H?rhHBboAe?fS+k9!ST`*zY{9#C1?@9`TPRIm z=}hOEwbIapA^6-0OOF-4!nxPQ__(B(-SL;|==9m4 z^-3Yc_pAJbK>NBiJcZh=8;y$2EuOh?v*3&?H4paA;#&Uda@w(!y5M^e z>!Yh2Piv3&%zRr~e0q9RZs@Z~FHB0?CFeIv{*JwPvE{-X?amOx9qXtd{VVuW@D&ptd(C@!MrOXJ|zdvHGg1#a_Rox-*y^&e~zfj zf2?NMyFtH>N$|Xo;@-QtKb}e4JuduD!Yv_u^{eex@_o~f$mrbrc=hAfADRvfKMaH_ zzbn@LN?v6hFpY7kxfjpR-yHvoei-%oFl44?$en-n{7;CF3zN*vWzHA%Z9Lzk8J7!B zy8ZR43Ge2qDmrRDQ;+htnXJk4TGXnxLf-ddDpQ|jPUaEWNLL4iEk?@ydAknWoTY#M zP3NMRPt$!S*?hT?^W_!eygG&ZCO$6vo_V{jG+tco8mmy(@38B=kPyQ|pLr_Nem4d# zIN{*dd5Aa7DQM%f?_E`lEWz#1-L#iWhD&J`=ib;iCvHN=0d=3N&#ULWmVfq%rF8Qe z)ny7t1UO&0?b6%M9%=SM?zd!GPr?R;q&MG#Yb_T{<^| z6IwB`b&k*S8=cU-sv_$&^bmR`bU94=oHbLwU99;preKV4kIKYfro@BhKWx70;C zOfuXeD@C}be7`9(_XNj@r@PwteD!_%uQt4udtFeWSkijjX``CjvZzB^vr3CDb|i3d z2uC=ugz1|XDIZVI?`FCo$=CB&!udf!^(NWJeA(^y_CIP^ohF{D%EUCwrA|Fx>3I7e zI|;_b6ATstj_)>qZ&yCrF}Y9T5z8t$NB6j+lPzOoUT&7V{Yxe0Vdde^?4KGt&MhuE z{7J;cWb2%&TjD*YGj5uA_VgdSmE3P7AHW!+I6dZ|!cmxbullfvr+t%FMbhM`WyL%Cgp;p-zyI&e&6AIx<(R)KyUhRl zZOmhxQ<1Oa;%)v#e7`Nh84(}%IIsNMgPg0Mp5OoXXY>BgFN04oF_ds-E)Z(yF_~Z3 zmAZR<`tPz!OD(+S@gBBuv7UXuv4T*ZWpaJ7?RzCokia5U1Z9 zMW=l6RvnBC6Xo}Qj=*tRTlzFqC7_kXMZzdQfu%=y|sACK4nJOBT8!{1Zu z|2!;|F`xZ4{s(vc!}#CV|J~mI*?RxqS65fZ|K4i9>2_{#R^4aw|G#SX%{aU3{=c{P ze?5)=ci82g$=mJw|NgFjo&Wc2{LiWJKOc3UuYV|BS7;rX@N^0-IHTlf zFtzB&<&S$8)#=P!QRTRoaYDn)NQF~<&Ry+HO@*3k4rU$eI2g9TMTVgG&Sy@v z%l}oUQ~Uqp{%_s?>G65{-78H#-QDM}`R|i{P4>rEz4q0H&x%Tmr+at0+*|T< zrK!{G{o%*dOs)sNSo2%!;7I`&^V4^?msfQOMBZG$8Cz?n-lM}Ir2h7Mg#HuH_doB- zl}=STp~G>@A)|9L$KK1*jXhfeb%Z=-2%Wb3{V_9jg+S7@#4El|K`;C^FwO8(c{i0I zNHcpcr>L+tmui}ly-b4aixj@plNlG*7H&WFJTS@4Bt@w6lz@;ahhroQXR^A}EfLLC zRUubQmAraVlC=|iSR=1jO+9{m*0Qe^#f|-Z+ZB8qgw#w6_7=5pI%`i~7VEa->sgn{ zNnuMi$o{K06*$7ndhH6Q5%=T?g2e(4=kT+}Bq%?&T(4r_&+hfMuQ03#-e@~B}_OJVTQhsk-y#6v_ zWoBt!GcGCpdaZBw_aOhjYnyr-e^l?jD_;4%v*vYF{Fgm{Z(f!ERr39B{=V8XJ5G&V;6k4YYT7oxla>fGZk+{y}NpI#p~mNU#iX(tFXB{8R@&! zvIu)P#c1E+=r%Yj<|_MQ-w+v}1dkwL%f!5?=mbGmp~|>1PBZH;WWLoVjzi zpM$`uxMPOFx1*lyk+l5nBRFGG6+eSrnyd1Z#O=3uCny{=nU&gi$ZiAA!9|OLOF`l&6+ti&*;XIY0=!r!!GjOcpAmCaempU#^@Q1qOYy*OpkdQYdfRE!*|K- znX?Q7uV%DbvaDQd^4Mf&^|`K1^9@h)$pl4T%aKgok{B-;wMRS0^;oRKLW?_*Ztu3U z28DR)YDbz*^INvTXjWg8$MVkH{H5t_uG@G{E6)gsdLG2-Te9I5!-qvjk7O^Z(-BU3 zy{vVn;#GDL7p{$~!kY>>jKsDk`SQpt5m7y5$o4rZ>zI4noa-r%1h27$249`iJ!_YB zGZRl;!Ov?wzoKXSocQJ6x5wpE%5oiwCog{>{wO zKF1i`c#?QnW@yE{{`g&fk^9{rZ>K$7zAxg!RyDTd$G^RPT@x$&Va0zf%&GtW<=g$K zUH=ViU)hH~e8=W2a9HY}%+uFmehxPu9QXU0v&;I=HJ54X6+b-N{@g3$5nSA*VN&7X zap}t~{>L1xEJ5cP&KK-Da5B4wv!XE9;#^qaooTgUZ%d1gZk)G0-v8_485NaMzv7l> zGI@vj2lvcy&Y9%6_R9^ih?z2BYp<+p^He(#+db`KN9XGh*(2<)s%;e=j6X#yJBCId zaXsk7@a^|L`R|i^IAa409k2LB{JC(qs>eru0{cRzdoLA4G_ELEO?F-Xx1+9q>k5r7 zj~(Y%I4@Ztc3rLX)iS>T(F+R>ckkA>KI3=T_2H(I+xcdnW;%6wiuLULjmK_R)Ra~U zvBcHaSGc}k`$Vhvtb%{cX{qaze_T{wsBL34b8cJ59>;i*sB#OF$um2z)@um_*dAJu z;nrR}r6+T4JX=ZC7WI1^aoToFL5IWm{4?BkA6<3u+(PSpp{KWc?u)Zv{`PG1tHtNC z1QT07+g_92XML3Qs^MfQml)kPH=}!R1Kr>5U3M$rYJvZ&Wd;{NSlGSI-EpK?gHu3d z&EHQV>Wszh^B?V5u;*GjL+6G`y!)7I#FG0L2)}q?-Ed{ukK~w8TSe15nwt)4so(zW zs26{#OP6=@)(pnr^U*y0hnq!aU4JbuYO1nwok3ka;~ni+0wU(dsjDU8r%7prgjT4p zE{nPQ^V_30fAV(Jy`9gVwf*tF!*6`#`EEF0jK5r8_qRFKVtiGSbvc}3ry zJ?<2flyGzEihaB8{#qg*@&EMfx2vA-FWva7{@ZqQbMxKn=a3H-=4iM>xqA_;*@_!pPdt%oB#Im^y#zr ze?97YeckVc%t{v=c_p}27I+E@u^m#~z_Lp-T0qq>;9TlX$)uTOu|bzk)~cvozj5vz z|EmW7%cbW3yJjxn;lBC$CcB9h>!hQsg15J*S+<&7kbQRl%3tGKH7D;@-`Q7QcCr4~ z+xP#i{~xUXwExHaKi&I3zm?A~zg;KMG1GO%PM^vb>_Tae=BNtBZL`?K^)*kmyN^D40-7Fb6PRaT z|Lxyz`7_Tq^V|LSaQpthzv}+;{_V8?{(RlKbxt1p|D3k}ExrGvba;69=9_=o?f*R7 zoPPe@yLbP7$p06v|I+{O;`R0M^XJcxkB{G9^)<_E_TK;h*y}&}|64qL`t;LJi?&+* zJpcdm{X1{J^~>A;dm8_5s=%SIU%z&X>(_ltuTL|ZZD0HAUi>fb`oi6J?J7%-z3+IH zrZ#EXbMJ40(>&7@inLV(`LwGy~vv?MHd3+78C?AzMC*%!mHffOqXUHmC5}w zna^2w(&k&s^Oj9(_ndrSYIl@CS)(I&*KWq6nM~&SC+#NPJ-J4=EA!FMHEzF?1QI7o z1PNc#(M#?Qn|N&5MZM!E7I75xahNW#b~Y(x$te&I_4_IFprt%I=U&<#M)kGlPVw-W z>P9e6_@2#io9Tu-UxJUsmW(G__YUzL6%P$5@QS&f7M8W7cwE}jQgx0|(O#yq13{vPEN-k6(9}2Wo#FVKGq|)?)8O+_o08O9haNn8y0~jOt8?l?d8Yf4tji|cof5yu{qsfTQKHuehabvP|^ z;F*RA->usc+jsw6@Y}=pZZTKdyDWu9?FC{&c|wm`BxTZ`Mp(yRi)gk~_@HR{KI{8b zPZsAu-s5Q(d4x}M&)Hsg$^Z7%qP(|lYr$qhUq^RJ-sX((~#`L>k%UuH0Ni{qQ9$btq)2n=!VU& z`}u~a`QWu3yAF19|2m~y_36;Q9Y;g=F8w!G{?FT2M!6~9BJ7`WOk8$(_4S|K{tM-Q zvCp^p^=k5RamBXt`SaHQ*s`xrm{~n%k+840jI@xq{TIa8ofwQ{bUOE0|;|dPmrnaQ! zJ)s+{Z*JM#*0o+~Q=@O#qS>6DOSCT=Y?yOqR{UO#e({#gsrPN&^zJTWzj5i%;hYFN zRlh?qY3Y(LU%mR(ZvW%s)hhqHFEf+hoBzJNIe5OkRM~As@m^VnER&V*+fSaklXGh4 z`{Q5Bta%Ccw?>VjBT=B7D}Zeo%LTb+_kSeQ2@N_i#n&hgUE zv`@R|ckPW~->JSlA=&S@+7C6Rb67nyjIMvMEBmCz#xfC>^%DA~YcGDWT3Y+O?B$sw zKC8k)K2PcNuwXH`vY;lyw{M}y#Kot6y*vKB>le$MSvCocnidxxa`XLKTTnEc$-Ol% zSJvWyO@Rovw)8g#Y})BKylf$bLE$#B=yC`wocWPQjxQtSfFM7|jKha> zUk2@+$M4vaQ`Pl3*s?zL{CYF}Ra{<^T$tEyevq9c_q8keG=CP8ygG47{MqLTG*0^3({ z`}p2%@o6|>Bl}G1#;Mp3EplNpp7VX@+fV&2e?mEQ&C{D*=Y&l4Og|_nX-u8o=iJlB z$>(h|+knA>zu&APkD-7q@$inretfCV7)sPW?>zHt^Ub{d#!M`%yAFJEtcWU+G0@AY zQ(C_MgQcQgirw4I3bONcLp>PS7BaRzR1}hpsA5_g7(V$+c8ssYw@rClR=4d7D*X4g z%OFf!fI}$YTFuXnd-E%JI4;hRocQ{;(FE3vt2V6)TJ8Te6}0qbU1XI>Yhsu(^~4?T zNtcgT)GUtAd*U|HMX^cu-ki&k491CKKaTy3na{=%A#9o*%`efV5#rS2J9)>1mmA$< zXSU2>w949Ox@KKKW3cJXnI2X=Y94KwQHgpg<@@FtIL)!xvU2y!^~+YSIm9enQ6_Wg z|0;otQ8DKgI>f{`l=1pxg!sBBBu_f!F)6s#=|GuQ;kSl@o0rp$Y`Ls=vr6^3@G zjB`3WJvXgbv2XDT96`<$EAJUBdamMboO{y6>p^7UOO zTI9Cm+)fiyURZDW=Y{952amqwoOoO-zfAu1rg?L6=H-1nuBV&)_xAm+{ofD1@~;v5 zfAQ=y_B}sV_n%*G{J8Q*>-yS1*Z)7NzchWppF*=QQDU1f$Je}n$$xGC_WS(mFF*Kl zGBtVRl!irkdR$GPQU2oFw!4~Z-=r-0v%og6c}WN7`#D$dt-bTlGu}j@#qf916!!&R z1J>!!GThDc^XJ{!^)_ego*sO;>%;W(@n`MqWbJI;*8l%J=l!|!=gt21Da!27-T8^N ztTgs@@r0fxL6HZt8lt5esw5xWc$2=tf{)qI^@h=kMY-a;cW>8wow;Y$*?mj`oHc)| zuGcy>H?!ZWe`uwow?k0+en8cOS*aJ)&!pAb{G9eRevA0>|Ihyav;Wg;|F2zc&bi}- zU54Kd3pmaBv?R)@=Xc_UMN?M=cqL9dlC&%>Z<&&eQ-V@?`GU+@+j6xh?Du-{`|rGt zq@MP7D&K$Agw4BS(Rcgpx9a=9*LBR9GiT5Lf9%`0n_o8Rd;B5)|Ly;KLFpMOXA z*|*<+XPf88?XSCA8q2Zp^{#Di+Y)!|0RfZEL7#H2>56r~e!Y73?Ave4W*5Kv9ea6; zrmFGo-Me#dZ+rip;nzO-d8NPi9J?W7*7yF`z1h{)B7)hBs#XR^c51y6NSrYFTHFe&(`%fam@WiNw@sJk zZRLMdJy&w3K=VqOcV|mhFb6Qoulyb}U6DV9VO6Ds$JO+_Welo&4o?qJ=Trz=ASjer zK1-yv=OKe(;NO?R+nK_QI>WwaE!*<$?dH>q&V?0!^I6XSO!3s?qe{%l*Zj++)6NvF z?^8Qjx4b+!Z)3egx$-kUKg~9Q5}EIcHUF;m-}?I@WA|h=-r#ME&Zf;SE>n0@K3jD8 zne^P7Z+_jb+i}O&@WO)c7u9k$#BEvq>)+W$!pB-9A965%a8}McdcyHYqmnaAky>}D z%Szs71}j<*oGf8l;d{33Rq;HfrCA#kCo!LO?|Z7!eoijEML6#6L%*&46DIy@^1NVC z)8^{5`(WSlYeF`l?3k;fW6Go@vbt*UFE3y=$U|mQK|SuFD6#C-%$i zcIkDVe2$5?yyPmcG4r!65z5TYbD3JVU$HT0j8V!@Z{xP%n!&qc_5|hy3QiH9R!w~& zEEKGLMlSzn>27as``p<-r1H^0QKWxQ z_FkEi^Lh$G>!PGI7x0{&b*mz#O)}5$NM(-6t2%q1;NFs-Jga}!m`$1Ic14TDR4Cz5 zVXFMP3jJ?6i`L(JbABfGt?6z$0*WgicJ|b@uPtixEESMxtzG$@k8OtK?)TqZZ#o+N zc=r19{OpyLgZ_c4-HxrTC$T+j#_N|4FCJBX zbRwnlRC)PYhO{fJd~Hoak>aPhPuL`=yjcC(v?uCZWUi7}-U90>hOHJ;j`X~(yDs8# zk4H-C2!pRgo6_>r>!!||t=BVq)Bd-g^-eyyFd;#8_j;qV+b)+WJ)0m^c=mFYhVX^1 z?3b*QwVxN-D{hndt~T$&1aXN(uR9Lh6BM3#?JabUU^#fD|9W(t4W}rNq>u%}iO)+i zn3c>0JBqBV4}UxRwyLv&akV~X6&x#wJZIrHhlo@2S2rEB87l=pZ{s@%eMW+D66xOYaK z%&ny}ltP^x{~H~6WR`wa?%MMNrZVm68p+wpEsuZcapbaXWHhjJ$s?CI^yIqKDOwu;T20SMwQ-sdD~Xg zd94e#ZkqMI2_IYP&W5M4xBu=*H(2xbSi+gaw9UV^+|ho;&iLT{YR~7dy3`7$K6?>R zRQ61qVgG^U4MyEUGc=o+6dm5($X}>1r-0*ts7<=U>hSHd96g-h<$u>64)&3c(XukMVXxOj1E-?XES9Y;$(+zE-WSrWQu&6OxeT;uzjzr~lj@6RS%Bw0D>o>bxDiOKNFZs2_#Jjss z`q~1?%?hgWF?^B6SJ?8Cx}JR0oM-ex%)DW%mG>R1l4Z-I@5m`Waq7}I;!vh;@t`>R z>#q4r%+L3mXcVY6PBrE_lHG5`ARhbjX3;(kjStNYt1ftcGq1Sok+mZ@_;Sy!{1pyI zV(XL_RKMEIaqSi3OUIlZX66&`j6^;1xi9WLHfMs1|F;*~9up5Rm#y-i$hN_i>+l5D z!p6{kYf+Blj~@HAcW>|zVLz?BZJjEI;Y5>z>=Ry1ywC06X_`FM<%Z8xi7f{@jl&l= z2HVBWt13`oVB+(6<{B$ulyK+wvygL(S_(J&d&oYsEsF`hWO}Ri$J(=3d$Tk5W}g)O zANR9aMaXpm|I9y4nS8P$RV%K&`ott)BETcW zHm57=TjP&B&!3+Sxz96x%4%v{S17BpE%V~X6F8dW~=4h<_46D_iE3%|7?20?uhVnla@-zO<1)3 zqD{JlaJld0{QI7>8~Rl49aev>@^D|BwdkLF7oMBdH{A3}wf^F~O~CRV@BYsZZ@yi0 z)3)ya$3@cgNfklF;-#gc+ zoJ(spG3-h-?cVOr!rG8o6P)9(GNG;gbW!=s!oE!Y`wmf482@&BVX#d2oI9nidRIj2 z!OM{mHN`LY|9v3*QFB-o*cZ=ks{F^RTF#0OEbzVD@yEJ z^66VO8xx)-O8A&AHId7{amRM4iEnUj((0=(d@rxaD!tGuaCdK&-?!iMPVE2l>2!B@ zJHxZjM?nKFA797+KX!fpzpvBv!^9bVsh_#-274U*Ar;UPxmKZ3+Cp*5 zoaQIi>m4%RF8}!T?^Kzf@cVyUT+Z;Rsefx&^-pkx+vQd7n@t_>ERhlUx@z&H<^wy^ zP8F%jd*`Nf<$W*R@J`P!?}e~Rfwf})R?ekWKd&6LU9;NSL#b2ngpbVKqO~go`Yy7@ z{r3L5!_l(r+~nEWi=Hl9Gu7a*j+@4vjbKC$$<3>V(M3wxBl|BE;O zzjNh*4)ZCrE(O>@7%QA^;&UHI)mMP}2Md%f9IA&jA zx|X=?mE_Jw|9R40EE+|JWEO>ngu6`JaN%5vt>S&{S5A$++c$8WGMbe%`MTg)u5{b7 z(vwBky%v36yy#t2=Dx2&8=Y0+8j3C`@jQFC)qHm6-L=Qr1RI?@OrCmmx-i+ldC@v64K?d~%iZL=&L%E-!{X&(y{r4B=RB?}GcAr)^u8^vo3??2gDd3+?)wy*?PanGo%gtYOg&bl@XG&GXXL7B?-+T7?os0@U&qpMn(ub{kCL=*hE<9` z4;wzTh9`D*LIz;ho&Mdlsa5^<(%w;Y2`$%|=a7n6OR0+rHt$kxd*B$YI^Ut|gh znVD4kHiXe=-sd$UtT8G_1x{%DEjO_d=4MzL+5Bpq+sqC2p6XkD8fM)CZ` z&b+#$v1F6QRCWb{qkh*GmH%z7y?&;uH*L})k!|5tW>?Ns&dZI`kw5$F?v&$Vehj%c zyO}?Wv7Jb9zHamH$rDC~r2>+#zgFF_H9cT^-qp#)zVD{;lGGWCCf01e$)Cp0bx5+^ z&CAU2Y~(?~xh28(;^XtrN4l(2mt|&NP)G+qb{6cC-^?YxEz8@e||^+wb2uTfe+y z;ur0^R|*#XOv>=ftLD5c^>~}abc54jUJ<*t7_f8SiZq+Gtov`?++s6kX>C;wbHQqE zEzVlKr`wKo-M#r~%RSxz*1)HgnW;}#-qjW_wLWr2N65Lu@Jh99Lz3LL56d5fiWV!h zhVs-Okgs!>*!lkl|DTkjRna$+qIdMkJXrR0Lvc9U??m=|&7<2qqi#P^QgTXuYm(V9 zVdr8-riVYm)ZG*^j%B#M-Vn;;VBXE;b6NgFvAo|)K8TkiXZz>J&7XTLa?&|Jp|q=0zCQSv+&p`w@HFGg6V&gYzx+p}pDnIm zfDY@r?hZaVz-PWs73hm|WDjh!?k z)_?ij{o`NHvxKfWO|8F}j!P^$TJxv$NrSu5aihhFBE}4Mha;OAM8tMJ_^xzO_Wrxi zVnN-;hjjJMKdn)myHV7h&*!3Q%gn8v-|Sl8mNFgsDO(2Yab&eXj9 z@!YgG^9#I$eN|VBriyJyy7Tj{mTql@`~J|mTXR21EIy&Z#S*abPgRZS@m0smCX{N+ zyY=N=z43~BjT%qs@4STG=LgdA(khH@%w~*NixO!6J^#7b`lxr)-o9G4U$XR?QrWDk zCk|`u))wrWs@*KA8)@9oa3is_eYSl@;#Ssk%LBE|0~hLwaj&_e7J6ob)#KySf4x~$ zt0A{lclV~ahbl_z3bq}e)ly#hImZ6)ir=q%^wgwZGn_pucjWJNKfN;EZ~TWA>pwqz zX@A|fq8;~Y-#>r$)bVG9{oG4R&kXk6+4KKFCr8n{__+M^TOKmsza2H#xBJ(3x_te& z;+^(c2QK8No|S5JT*&c2;#a1=Kw_MUx~cS=te$H1vwPRn>9Y%7)hqt=c>iCK9XS=! zpMTG(QeqJI{k^;R)BAlDKVILjs`{9)<6iE(*yq>FGdEV*R^L3k`*GLox?h~En$`~1 zWrW{xcsoUMTtex2*PzI&bB!%-!Fw6n)rtTL1go^9Myc3R-up(8hrZl14yxcl?>y#Md+|9SWSSN-q$pZEW)|2Oq--HyGn$3>Jc z)oowBfP1}}>9ZT9X?DJo4caAUFPnSpWXQ}FZ>@^i1-nDeRF~F=oT;{rx^Az?lD1Oy z`O+uba_iqcuai6Q{_nl}ck{N3i}UaQefND#Z0tYo|A+Sfcy)EPfYZ6iQ16RDUZIa& zU9O%nX^F7Ex$SO^oxbXGolvL!yLZn%|NQjRNi)4J3OMK+ijDE6J3`=IUfM{#f7CKbDs8#0+5U7AoH@u$OO zhsf;KDe87QKh|wnVf14Pe_C}j$FkoO=LyK%l$kMWg4e=R+jP%8jO$Vja}|=S<>!9R zFLvLiwqC+Lv{&&!rkGPgWd60sb$fn2dir~Q&5oNdeYd@Sy?grk`*(86e#bnDU|PL0?(i_%u>f!9-?$)N}7SZ`U9pzP}YnUbpMjz&Mj{2r^X{y=p3nudTaYq9oDda&Aaz@ zW#+L;^|x;M^WV<<`|9O-JG=V#o2SoT|M{EeAAtuNdwMn-=lSQDx|=60yumr^G2hMj zb@BJ#M~PS+zR4$iV&?47U2{JFy83kayye#qx4-h8o^j;Eac%KvaJC+oD|wve%mwtxY|hc-|>oSbkU|`OlM!*R3TcuZr&S-JdU5 z&Rbr6JB_DJE$QFEqwHI~WP1v4)NQ$0cGijMfpGAN)H9V@Vmodveb{#Q{+=@jB^s0O zX&S^CM>_W_)VP%4?H_?5n@MdiHbr^D@t~oVHTW?X)^+hH7#w~oYPm&=Gybjulx7r*R#Hv$y0)^$okGs>M7=lE#dxr&1h@pn{LtUpjqDH zEx&`(1W&WvzH7*Hjd|uxNy!sJoH7b~VonupY)NamX~Wb1zD>I&=9&NHch8?c{H7XJ9nSoF=$c6Mg&=auu<#C@D~)^5f+U!&=kX(!WI`J5;GJ|MJd`mSqN z3{r2KWVx}hN4;BjZ9PLfYgYG!mBpzQLfRfK2ENatx=r|4PwiNHbGkzc zpXupyJPFK#5B`4R?ikNGDz1#QdX?`v93odqsTnh|!qrS>8&qrj*!M(A z&z_Nb%C9kWj)2@p_GJb>);SD2*Qeip@UBVD&^$CF>Xc-o`%txo~&fXjIcvr%# zk3lnLUs0Mo!{MHzafi^V?x0s!cOROmtg(slovQF`X_rQpzy&r3T6?$5E!(g&K}pf| z-m5xM@!YKTzq=M+4fbfi_ON08sl`=~tXh7)wDVfv?o*IzdGUxkXL*BB(UOG$FG?5R zG2n1Nz0X1Rm(#7X*tS@4y*_xJ&vdVV z)eM8OlHz0P-_^b~6hz0(U&j8s&|cYcDfh>1{Il0+1SI>vn6&L?cjU=Ai&Yq8#oCfy z^jp`pw)gC`+P&Ozu5&tb-m0&$3f?w+84nvSo@2UrIP6<>%&aK!%N_TOLSkgpD=O5d z=U=XKe_d_sWWe#pd(M|+OPQGqf`XjHm3AgwscZ6|r!H?jy=>aN_v@zbI&~s@m*B)* zuS|rxs~$4fJ$zUra*5~P?(o?+Tk>NcUryxRE`R>-)8=Qq$7aqulfHaqt`o;2Qw4wL z_eTn+|28Y>_q(@$@zuHCKfZnD=5XnB()#Obbr{7(C;fhU`G%$9w%p77%6pe)?VI~i z*538|T6@t;oNQC3q{Oyebt#wHRGqNz?tA;n_s#6`>+3%D+ScD##JPRf{P>dH=hs`f zs;ry%|M$+ziB*{j)0jJF?U}U4@txm=u43aUx7Z#WN%6Bj7JlpJdwKp>49otdUa#dy zV&8iC?LAqmgX#s%8(i)=s2`Z<_i)+el9Qh@3@2?ld)VI0=6AQc{G2~O|G$op(|P{? za{b@=zu)n{DcgHF$jNKTq|YK6+hR*6OkBL7u*Wmv$`<*y%Iju#b7mczQ_P{*m9psf z=gPNE>!*nCv%)1+{_Vd`s4DIgEhl(CzFiK#P1WDGgu5h@ugn( z3wL7@Xk7O1>h0jFEV+3m@-+?P1vLi;^$a+KZgpr`&y=+hu}7n9Q=U2dAXI$a%e*X)Me> zVTJW>4VOtw!oIvh9S`?C*bq8Zg26>F@zrNJ*L(Bre;vDSw{vIo`)|U=I?4OCEK6oN zHA~%~!?{bJoP6eZZr8@&AKtiY|NH-E zYx@7c0`@HX%6H0n^(An9I{9mnXJM(>j{EJ?Ump!usWm+zE9+mCae>uCF~x_^)Y#Ke z!~ewG?M>TD4X=JlnWb>X!!dQ$9+@Y6+S?v9IA-_k*Lxn;)4>yPz3I+FmxporhCU3z zs^1oVxgm375qq)v5sRe-lfNssEy=XKyLE*i}8 zf7jYZPqm$Km5-ZqQH$BKuNCE;(`DDD8E(90_V{;|8Gnazm~7jwB`bFan{A(JBXp)} z_q++s6Pt`a&B{wJsd5nAvF=Ze_iM**=?@b%XRT#nF-m7~_@27wXttYBK*}CROGEX7 z*dymIA8q(2G`&}O!u_IM2aan0F6zI1q*qGo&1P5a;(PDDTkMgEFc z-gjl&A``aCl`WkjY;!AFc*d0#ZE?JeZam?}%^j_>UrhTiris_orJm0}-OIpZx#L~` zwQ|!5o~-}wU*CH*T7joY!;#_k+g~RHpUw2%Xjt_5%09#6K7sGvD_`xB->FiaD&rR2 zVGvz7)AHG*?bZ9vn(2PZ^kxj}ymFa$Uij0;9!ISWzA?2|9e8+Hmuntx9+SvvZ!6p9 z%ik2*=T7h5IqT_P9v)NmwiyjS52Ud@UVC!>EU!<@B6gAUE!@s1sCo!wcI?_Lk?JPa zy{c$e$AZrm@h4s?mHBF?ehFV6fA7=l59byviTf&>CV%OA{DWVo)2}}Y&-`T+Z9V_! zvXwLR{;d7V8~9sW{J&`1o7%sT|L(DW?zfMtd;Im^)!2P+FC64k?wlKikm2T4d+(o6YQ}`|o{U#5yJS4)d}LHm9C^ z7EljoS84h9?cKJX^W9C?AAb5BsLs)RBxY*U&e>T#hD$wMET>2Lu{|iSdv9&nwk@;C z!?$?5?`4zNzkar8F-D~l|6Q`+G?V{@3z@*U5mtfhuuE(TxL2X zz~t*VYr+$0f4@(^K7Gr(er6W`z4~7>wx{oSEjK^8k}3Xx$Qce!MF!mse}0F~3eR6x z_vhvGe7*H&YPLq%)c*PK=itkY)1_+drvy1AWbhfJWL&j==ff`1Fm)mGDf=Ds)^1y= zEx`9K`|rwkhUVNa!Ysm57&?=d9(q$;f4(_*t4PPHotG3v;`sHyrOLK2*ly%z6P?1F zCt2|M);7V}62i<9?3dRlZHo41&=ygO%Gs{V@|j7gFrz~z_`pLI{Zj4eHuF9;Gfz0| zFrg`P(_9B}3&jg^v$sD9uShBE<57AXz3=Ax?^~y-{B~I)yl+L>T4iMchTbdII=@S7 z>v`?IsQ637-<~e4)-*$tnX5eHyiMG@{FaXk=5l|I`+oiR>mvf)uDOy)w~y4Fvb(A_ z;g+6qR>q_==az1)Y;QfpvFCW~L53s8D@tsVAHI1bA=JK~ySV6rSoq?~8Da^qn1svB zcc`c&PCfPF+u)FT@3G^Us`SYnbp-aYbd%Wr?R3Uv=)jD=jRMb4Bl${I;i?0$3iTOkdv3_RaKR z;+el?cl@fZ3x7T_qin~nEwRlKCY&DqUvu~lMD9s@cQ%`Q?iZW6a>G@bzii?rI;#R@rBH0R}0W%Y`a?znaCfdLFOZw%y-G zaOdK_e`|Lgo}qe5EkEyTsEf@*_d}0XrTxpZjefhYB&2w1=SmH|k2c*k2XfD?5dO8; zDP+H0c|_*Nd*5EB6%$pZIukgVIwY!=t zxt4@zB>!bhP+MoFyjQrTZ~6fRznN`KhK+?edEp7|v(-NzKUcDK_uZ-6K2|L3=e_;< zW$A>nlj)O!+jqFPCCpj2hSkAo_Jq8`mlhHC@88>XC$v}Kxy9|b)lA{SyBFzv(0FMx z`T3%Qk7QQInR`Au{4DU=oo$82YZ%`f)Mv?m-*r1V+PXy~z~ZpS2|+d9BbE!dM65c+ zcQnz|!}74xg&ARURs8gpEmm|ExSe)CpZ$K9ecz-j?-p$Rd-(n1TW;;=pVB36Q%I4kOEi*m^|5P~gVzF~!TK}$fp;Pv*cU4aKT6uAin8561@}Irh%zl(z zdB5-8zU|B7YbCGu>Z?WVKYqXR#Lv9%p*B{LbB=LjMs#~Nq_rL1!?SeBCB5gTcRzDH zmVNc)UR9;`UB)I1?CFko7xE{w9XK`pX|HJQ6`PBTC*FAQXPRHtgN#sh?mD6JD~Ijn zPW^xJIQ~c3;{W~f|K7P83x5dkiYu^~zy1Gv`L%Ule5T#ix2^ec=)?4LWitX&dv<-_ zb^pxG=(Od`tyuzx@0`4Q=kD3pE81VRi7RmI75iziO}4OpNnG#a?5nbc0r@F=;!f(y z#c@1HbY%D>mdMAsPMOhfV}|Ay?p+^CuG#!4-Sg9by8N7f(f`~3|IXX}`}%+D|6AAj z#_!v+#%+7dl`9S_e(e*E&^Y?U;OtIo+m~s&f-|c*99{7VlV*ZI`<}ac-!Rr&g*pUq1h<(@*#0{wNIB_{?qO{J<-+ zSb~ekD?n?N+t&CnhUG_mx2-vGB*4^tZw;@S@b`l=JN`t>3gvs39-QGDcPKH$>gAQo z8P*Pp>$3P-cNGL|j9Iq*ccyT=me18>7na_0T9%@2XMW9w3W%+9gr+w|v`XHISL+|4iFZxasAD>>zOYVuF9vfoYDcVDbcS|zO| zv3qrQLF><(2N!q<{}QZie!8(qZq~yN&+9E3991djUAAxv7PuyDY2!3d^3v>^EPj@8 z&hL^ zmgIYH-bu~Uw2?6Tz&1m^(9dnzyMX2ke22Y%m-J^JR`B@#HK)sHe{Oo( zBeQp=&in^&RPAm*%84nG9#CRN#7Ma1`Vj8=Bw@QcMr_j2S9NN~*3Dex)MCbE8J=eb2 zU3PxOp}A4!Nei)fIR3+PDrDSDvDY-7V-+b<`%}+g5WqkGb#P2o|;;Uz^#kZyeToE@H~W+X9n4 z7Qeo@;M)O*G=mx42PbN5+n8%OxtaU%+WjsL`5*Q?d}m|Z#OEmI(tp;*MCtJgHtV-? zDG%Js@0&+FdiL?rl4s9;oX(?Jy#_T>Rc7J>#IGN@6YJ! z-JzqnNsHM~Roi+hTLX`E+w-z12iE+|+xz?8^zh9GHa-pTI+^T{nS0~iy$0cu4U=|T z&dOH!zODAAga55(k#TWT)l7f=ew}#xebJoblQqrC4?j!28Pk5DL=@*Xmcz>*Z+_Wtc6X z91(iusru_vEj+gQT*sQTqWflRw$1e9@wQH$xcBY)?bF{pX%NUi?e?&V!7cmq38Nbu zo<}>ZTEyq>GewGV!tujc4eaZcQmwmC*^f>YGf^F4K_ul)zn-`Woskwcpg~5qs z@r@hF`l5X7KGUYxT#S1DChgmoOzwnBC(nd+>W9r(a{t9z=Jc0qwpyOo|82&8@X~q4 zHHEGW9$s%$Ps@0Vt3C~U$2Qkv$yGmjZnLbQ;)T#awS`f=eu7kXB>1h5YXUE)7-8( zS0{Y--@4uM%I*sir_8>Yb$jvWV|Ul*t^2?44pV+{ijCE~ITz~x%}Tu8^{4dh=NlTx`SoLPA1lGyKY{5#IyO;ri^BvqY{^r#8f64DWz@PD{XH6FY0Ww zN;@0hBq=^ohJqF8);o4S;CZxa1#{Zf`jalDR{U#EueW}3At~(qgyp|H*zYH(A5qvl z)x7k;5iJ(B#G}h)(|9~ZvTa&se(usnLsG|TuI|#i3k@V5ROYzddb6C7N!j78k6zvf zHK(XEji<{+OO+a2S0pJdy}a(aoKw+^7N51}noe+W8)?lm6=5|r$xh?yI9wJXyhP!B zCA(&m*7hs!9U{FaaTI)7E2AOKWgxGls^k#p&lneP(xy@`pnqb)PKz%h&xPLx&7S13 zUb=zr(NFmWE0}u(f48VQFa?VIo4Db zYge%;CC&8LO@F&30>{6;(?8j->QZ!{eev3POMj;PT|K*Qt3~hPExtG2ZDtO9^z-E8 zx*M~1ZhZ5;Zi%y)=l8B%0u9j}lP^8wOlJ|?JSkNCknkb_VXwWvA4MD~a|kZ1HCugu zui5s{$*Uwz%{&%=*L3=gY!3FXbC1ruU$^jH{r~p*cm01ZZkW06`}KdP;{Pes#chdP zwmmq6d9%y~EsF&XAGa~{u{&8;}TFaKC- z{(nhC+tV$zae8%ZkJskgInVg`NBPsmW6_UXj%Y|LMrIc~O`a01W9Rkn_=jiq-14?R z`2XGA|L3i|-EaB--~a85|8XsRo^Ac#SD&uEv8XXleV76o7F$}?7BO{^5~Pa z-+%Yz-WE7icYn9P=Fp&%b^?ExKuP_UhHIMLU0OjsLy) zutD1aMVALJtM>jo6}~UX^OIuB`ns>#^*{LkKRms9b$;Dv>HnYh|5;!2@u;}VmG6Jg z|NnD-Ywq;of7Aah{r@cg|F?WWPQkjr*X#c_^V?N@`*U;tPviP96`#u$Rlgqi|6OTs z%JbNOCp|qKw5iMO=oQ1uw|jnFEcJ!OsrS$dcpjvej4RvtRJs~}mugXv)Nby-&K zMp?;(l}|$26pWNCIa~P7z5G?%(y~CHVCD6#8}4ww{VH1nH?@d^xDJqN>lrVCu>{>$Yp(Jo9|EC-dSr@5A^sckwbE{&QM>e~O~jt)2(Zp8Z|p z?Hf|cklV16n_IZ0_rOt&-@m@E)VBG(^z)8aAD)IR(`0CNs<|O`e(T(&i(dBqo>XX4 zeY(-!NA%daBMeODx+&?=2bcm{7AP!KaV$;Jb$zw((Sg(J%RQM`4!SG|%a;<0SnhL1 zx3rh9tEqbSFYB&d9$8!F8gL~nnmbSR%(ic<9C@zjzR1zPEh$!->w72E%3pDA^3$Zf zdD$!5yhM6E#r(Fn-8pU7^j@@dMVb%y!LZL=+!C#93CE6jUUJ^c@L<{Z@`qYd^4DY* zDhp`x6f8Sk`NDi&*0kSqtIQfaIC^+w-lZ61KYwdeQhPAgk-H*(;75v%VL1YqQH)$@$Ja8xAu*ameO+ z;E^C`aD85I(?$akJO0nFUzR?Qu-w79;LyW=DSAD>Hp<+$vP$)wxZ0`j)~*a+alglR zZe+<`ZJd8JCydGb{o~h3g`b$5{_^)n&uez{VQgCSX3h`2N8BFl-)iIX_6FZKOVo05 zNJv{J&7Jsg$HuR1`B~T3?D>4Rz~fd`zxTA_q(bHN35^asdaB}^cS-Q<(K9=9>$`ni z-PcR{`QNvH-tepSNAD3?W}e8$t6Dyaq;uv9C4TVP{dtF#*~?27UvC$xmuv_Xzs_0f z@cPNhnVt9d?fd(w_vm?p7^V7#^=fBa?|gQTzWe7}`hL^wTjKin=bSveu&06Z5pUf0 zEn+V04>)x0|NXT4YWC%%Z3kBF>b$VA^u~OTN~|@)x&cF!F4>e|z7PKUbH} zpJSFAc=j#RGbcv31$yq9t7R@mG(iiSoa~vdm zXSW!n$yIZ+&oEY)R4vQS%GY{~>uR9P<&L_d*`9NjMx5c$apSss^IBu(rRqN%vL-fP z3QrUYbO>;9a;9yb@8wZ-*2k)((_N7F!RJbMr{A^tH&st%-TUS&*Vfs}Gf#)pg*RZ$ zv|@(s+ix@L8EgIdR#3k)|L80D6~o&5B-{pW}0S4`8IXur_7m^tu^ zMhgFrYl$p;8>~-g-$;HSvPshG+L;IapRyzmUk;V^+0ZPRd!!D>{Q z{MqZC2OYaA5&u^D(lUwHU!*SgHof+k&bVV@NvC|Gk>t(N%xx9FQ=S%9MZar`Qjbv9 zPRm>TRG=Yc*%_Aad+r{rW#D@B^@450T)Ud8T{qu)&s-!Z?Xd6KYaPAs29^h3Tz;T@ zy+=3Z?!&xwY{6GWcIHZ(h0VS8)Tr&70`uvGuiShWwpW>1R(PKY*lNjc(6czZ@xWfc z=VxzB{~RglHh0RQTHD#C86{ho-p+EnTXpyP=W>@_C)X!VG3dUPQ=sajo2F^HrT+H& z(g`Q7zH;869L>|^XXS3vv+Qx)H_qHIELW2k&g!|LT-CMm&F_USD_pmDugUwkiD`xG zy+u=3sT~Z}O?=1Gs`}pVPtEddapB&E$uqKRziV>xR)xjqI15eLsi&sne&=Dt`DL%X zV-741G7A=)DIk0%){{RI``=Wh zT$?F=ep+DGg>&7_Tq+wj%w)~gzjRq)&gmV;Q?FPuvV|9hmR3v-x^5QSID0bhnYU5; zyqztv{9ki4Ifc`&W+kvq>}NM?(QI=6wn6^a6<4#M9j~HYW3Ntlm3N;}I*0Ww|Jl?F z)s_~=CM@4{?z(x^P91UI*;mfoJ$NGQkjnD9jTgP!CGRZkn6v!uj=eeakL+4$9X(fX z);rbK_s8yRJ(JM5Y1R=IUFVr;XXhNu+jshyLxRWUjcKQ63NA}D?mfEEN~-EK?>u|Y z!ll~Yd;QzzPh&2A_;-T;q@}C81bQD&DGZ+g7yq*UtN))fSHD>A|GE3_n%dpHhEJ7C51o1K zvt-_lxd+|q?sK_M)oNpsSXe&W|L61HP9NfVH~((7YN-3}8h-Qr#?N0j-`3y9Ui5@F zsQK=6i8y7Q2Uly>YuHv?G=I7G^UN1g-Rp%?_WXMNTz>wabNBy#-2cn|kNN+Z^^dOq zJ0kyc=kb0&=Lv83%`Y-6O5>(rf>3!k|Dy|L-vmy^z+y@qW9#|mZI61P@O2+9f)PEt8kv{Pqki-6q!Px?ZE zUN66VxpL-Knqaa`WZj3g`E^&%Y`J047d$U5m*t}Uzy7~%S%QK}RbM|nJ-Mm+(~E;9 zse+nIO+H>U;LE)|EAo1a=5zPwt0kVyv++EWwQF}x=LA0E)fG%;FGOvcXKb5qIy+r1 zeZhlehyQi|4u9yFGx_3qRLk1KsaRzjO^eRqGu7qpO}OucERp~I`bq~O+*D`&|I zwxz9LUY0Y{A}pane*Lk<*QNZv=04ra%c|w>DytA~e`V$@{}W3}3X*qM6&z5LKA4y1 z^QLbN7kgpDMhnZoE~1A*ygYif8BWdPomnBO>c!)-(yBFCK$%bQj7`sC6(u2q!l}<% z&+4$c6iw8!dgKtv+`-!;pm}Kxv!H>#S@;)tIjPxQ+wYL*{8W5mkJo~+-W5AqjAoCU-QrjAqtaxuDq|=yJ&J# zlKTvq8+-|tOFM47F7Pn2nw_M$Tw~!434w)I%;s7Lby?p%n{%;p`}esMR&+W0oRvJ| z!<9a-X5|j86P>$XEnac@;uA@x>oM=&9Za3R{roq3yQ7P*SLDXYncIuAX|8jsFmKFY zPg!7G@x`t{UZAAn^+(GUYef7{-(1@-Tzli-xeYgeoo(CJ|3b%2(#WiAxkh_UQLauY zN99`98tck0w|2?MysF+|Q5f@)>rr!W*^4KP4bpqkTKeBSc3v$x>-Z0^Efw9(8bVD6 zZhkdrW@t)$wNdoQ?Q&N)llNc#H_+GMf4kSEcj1&y`P<>5zqsaGPFVAst4^!K`;-cmL^5d%yH&_si4z%jLs=A9yIv_^$u;`%m9CDe@!rp9DV2)R==Ft`w_=~?&lRr>(*|a{NB;*sf>=^k0mp% zt(U2Y&0$fOiJJNP;A#GLIYA>fgqDfHM!Al*aziDeomXVaR=skXV{ z$EtSkeS7QblHHj_3wM0Gt$UaAks({U{`)uv4GF)GKbYh-)Dqd+X9Y**o1HoG;NmCa z-#QLLj}}~BeDi4K?*QNbx31QkO%XgKue(8KJ_niYrZ??x`@aueG?ohSS)w z;6}$q?MX+R_T8RW_UYr}{3nyN5850Mo;>sW*U&e1M$N{HBX9kmzuo@zMBUSum^bFH z-TK|~xLf)Dci->rFjq1%Qa0Asjud7Ootzb@GJC;|3k;kSSQeUYRru1Z;K0xHMdL5y zUK19N28NfO`ZXD*4yIRw%o)8>#MwhlBV9Kgo3mKyeeaIR=kwB@&)NGu%A5c9jGgyS ztYNw;^n}CWP229-bBo{HxR<>)ze}DmL5wMubwSG|^?j!|z7%CXbKnx2N5b8A>(;%$ zH_dlL(1)2@->1dg2;$7%mg6u(H;ZTXOkMHMw^mF)xiIo~V#+i7e}A4neQIjF-B&ni z#m!QNw9S^S75yv1h1^4ztFN1tVq^4!*=4R}injH-kBL`>g(j|^zC`BH%~fI6ytCMn zk7Z;$S6b_^dI$5(KS!S}m3C^*ySRaip<3Vr+cf=magK_NiJroX>?9`&-Rn;m=3w3^ zC~#ydU&p<9z5C_;eAX$l?#g)PFYe!E<$Gz%Ec0V|ZVBRtXRS3-6{}h8rF}eOPG3 zbLtRJSJasxC!PR@8KP}ROoFo?9A$fcxXQhzwp@IP%fsr!&X1OLT27y`b(zSLDOcBY zPHZ$(onO!6?HI4@t;xG$a@kSlgo78Pdpa|?cr0szC%AAXzcN?VvNiW|k!*?Hvzph$ zSX`U;ydB4Uwi9me@;;P3sxR0oYoYLJYh?K}&Or7Dla=dRt5#gE@t&?;ka6ofEzIF+gz zK3cn0@j2tf8Q+dar`~(%VxMurl1FA!p}s#u*VUj-&b-rk|E9&2+e9Z=MntU_%Wu=a zUJ`ZnLF27i%i23*0&RJhN4qW+e$tVBL59h#hlBZ>vI)Z?-dkPA47k5duxsI0$z)ri z71HTEA<>87>h4uy7lLhW+b-f`T+MTR$Bg7iUhS;CM-`8#@f?omViPp^Quw`8jcdXb zqh@K7uP;|wggg{pr01lN$fszb(|d&HXx4<)4J>cEnS53j#%B0^T^V(03D1tSNgPfo z%K}z&Sw6a4T&--R`09vKLV5D61G`UHH(s63K)PG}E!L>@OA0B=EdG+1W1s}BKYixSTubaPox_hnRF7?8VTxk#U zb{y5{{`Rr{;nR0%divKt>etnMcE9_hx?kx=hwGDsb*vLFe6-n6_$WH3nPr9V+YOSQ zrESuk2NrD0ov_TG-JADKO~YEhg*m4LQe{&Au6}QHEcb{0-&@CD^Z)#O}9|0j6RNtWgIP z#6LcgyTdIYwQ2u3ZbsXxDzRs0zkYpt_UTp0&%es+KW}@o_xtZkujh(#tE;x0t*O}@ zT_kCo}`hj;UB>uNT>{k;GC_J8;8 z|Lm`OJ^lZg>-+wm<+rc;_Tojw&8`{Mfu_2KW``r%6u&3Ooh>%cK0c-BoaNF)3A5R` zQpXb|g8d4@LyPTq9Aml=RCMmyJH9lfQ|#M!n&+=~GJPZ1X&4uCcWdr#liqppyQ_OX zzIc&mHv4RP?765d28JA#&)2=awQcX-y@`_~4o|uBE-tfc_r8V`XMXK7-+VNwuz2_F zyzL1|1^bOQw_Dcm1W!1`9dMxdhS*cS0*~emGv4pxnP;;b2=LdD@zt9e#dZ=l;^&_I!6g z@2J^!_s+68u1D@1H)QA7{Ue>gQQS;YC*FQap@m1lXy?BaD|z{qNj)C@7(OUaO7mfqOebEGME?G*~_8A z;t_!-xj&f>Y_w%OyryAkuJZDimunOR{Z7?1EWFru=J)sh)+5utsfFELGUM<*kNM39 zeROmcMm+`oVt0?8 zsgO99`SrG?1B=}HGG>EY(VWy>AU{)#<+=JU@rnMo@c zoo+e=Gz*v9sMHgaO#9(k8n3ven_{l_(~#T=5SPl*3Zb#A=a@~Un6#S`-*RwMX$z_v`C_mE5=a+`9e=2MW!Xm?U{fPGMaW=deIc z&TYA`@bkMe3_tmveJ&GAPENZpcc!aLL$m^moxT14PhWpN-aPs7*I(VAum3d=y7=&#yVNR;@B;7tUJp#jIHM{mh>xg$i(It`|WYVt+|h9EV`*C%=*IXnqm3u!o^;a2W=0E z_BB1>O1t&!?9~tNSp0XMP`i^`sJ+xHUVBlT`t4oq93n|!cPgB&RQ_60eRT!%JX_gI zmf1GZvDdh&l(=N>^{p5C@^Pb`>Isd@ccmU96t1-1swP>F(FXr6mgNeVehSsS!y~%6m zZSGtw?Ilv$C2V=|Zg<+IWu2>159n5~Ex5dMVb6=zo)fOUVRRR^Dvz1^dFi>?w#Rsx z51)ON-cobxxA)ed(+ehiVi&*t)ZIVq^Udk=|F>}Nd7geyaQ3&!{qz34DzCdJA^!gn z|Nh_S>wlZ?|0gPMss_A?8?A|KP@RX^1mX$2_GoJxX0+wBVD`g?FFt6C9L3_pB)sCDVcAo1NH%PE{I7nRSj#AD%FZe<6tL5u8g3gVL z1U-8AT;?%eRA|m`nPtTm#=66)j?d}ZC6CSNOp)t%Ix-iQhNeuJaLjHZ53A?dxxp8w z%&F8;nI$2bGs)SdNAXy|mj?nNml>N5G#u1fb};CgNy@TUr=L21+8v}}<|Fg;LcW#q z^-#^~{b$oWf@D*8ohqc#o#!>~Ei!lFSoif<$ow#~@|8Pc1Z8D5)XFq#6=@nMxn278 zyZX3|d0`^Ys90!RB3#eEYoTSw;D5 zS!(-erNiRwb~i6}`*|J_USjv%G+HC9>p%(L6pcc`@SB$cWA5F{t+hJEv0%ZRhd*`o zctd+H6}_=}>bK;EmG-fPRIS3B;met7J;@9&#;zfX&pQ<=iAdp_=c+}?Y8Z6@4Bm96UEumD6}m6UQLH_pqWwDaJC4MlJ+tfE z?#u{rf2g_LsoCL`s!_RVcUwzZ+Leco50;r%c&oAZ?sC5VZI9#TxU~#-?%b2TvEh|~ z)3!CMxKA-n^>EQJ6ZKA;T)AAV*?CT3{|bMpQueH6t&%KLeBFI>%n+vn(xDw-xm)d_8Nz46aTV25rk-UeC>6E~@x<>yz|-i=WF@ z=9@Q1{?e%9X0Z@n=qwXc{!+~0kf&4f2f@P~jo%Ku<+$^1om|O_5`k~m8s@#vO)C&< z^9XsIx+l&g{8{4qb)Qc^6tvr&qEM!&k)QKhYd^yq9UIHcdEZw4fAH~Ujb-swSu?l3 zy(c#6RXKaf+tvO1^W`;cdKFjA>!yC)Xt#_Fp}H z=XP6E`Fd4X=Z)Wg_Ut;gTvX@azPdyG)6K-#-jrQz`kBnuSakAsZW@D;dCgqYD}FC0 z$maiGFko}PxYVUzcV}gRyBGG0zebK6EMdx-tiE+81ws^mDi2Q6uOKtCa?nOcys=lmL+GD*x<=L{? zrZ?7oZF*2{aWg?y?vCv3w|obZ4E2*&7Hl_IJ#*F0*~&}{&3-O5d$KigwUWG2@pq}) z(f^a}j(oeu{@38zln1YloW8uipp^G+KmQEZCKXlV!$(q8%r*R(j&ZJBx8awc`DcUb zeZO8j{dV;B!=vpbXGCUrIhcGrcxm-rf9bf8=4mgPWm;U9RnN>8iQ7KmtLQy0mIA)- z9l!2{&sZVFtL@tL?pYeU%ig~4hxe6PK61C5sxVQpi^WO$*0RjVPitoHy-~C7o?fQw z0@L}iH>0N(+3ynm4m43>2=R4G`ZS+;sj zfznmhtqccdolncXePqQ61<}k?CIU}6-!r&(=8DhUJkybrWqQN}#>abAp4$EF5EPI< zx;#z#@~K~PdxVr375T1AYEkU>?NC~?K&yHFvTe_}Qqz*y!moel(r)0Nl)1v8DZuOI z!^bo7-^NZVX;By4c(2{CZC&={zQEi3Pot%G-dbRvafQ>Q(o?$UaPB^i#NSJ7U;n+h zcE94|!=9YQJ&6*hn^Nws-G2B}*g5IUcEhvJVsn-`#YD=Ex*Zq#njqxZN~H+6SWQt>$%>I?Qrlq#O`7{b;E(HfgZfJ`)=m2$v!*wf9mX? zM#s1x@0UKPFv;rQhpV4XtoXTldRfc`zRy2C{^{I#ckgfex(nv(`YTN1%i^E&-@6-I zZ@A>>nw3}oNA>mmQZv)FwYt9jcbWX5HOXxZELV*lFV>vdUpU_(G*&R6_X=O+yby)d zXS!RG`wST(&djp^_vq})$n9CX9)Epx_3qpCGjwmewdQr5HLtw8b^02a847pj9kB>~ z70~vjRAgaB$6*r>?st0co-BEiXD8rbaQfPJp~E^YN#Dfnetwbv_oF)XeWb;kxG7)t zH`@1v_`RiB+`edP3Y-o<90ztr8iyU=+3R5sgyxQpj)ZlqiJ zT7LU?AmGaPj7#DBzd48AcfV)v6Zq-n{C3t2eu7$yyqcq~oBDo83XAWbk!-#^jaRyM zkICDewJhi4W<~CLW+c+-xXUJv|L^qgd%piPVs~Vae!HkgCorPPV2)%+qeha$93B^= z%!<7JN#mHLh0+2wy$zBP#1*I`(adzn93_7l^OH=n2XzEDrUdpGURr$=9H zN=)P3ro8*|&SIVFh3EOq!9TyT2L|!%@V<68YxB{!Q8IlGVl>j8tP0d;ow#N4y_HcG zQqAQ%WG|lI7^E4x*w5tB&=)<1Fk0kB(9lqM(v!nll-qMoBhK#AbibAX3 zpZP8I)q&U0Iw(HvwuRlJ$xVAUmNV#7xZc#t@HtYzVc5X&FVbc2sywYl@7_tCT-j_~ zV{gNLtwQD^-(d$A7C(vm4?nXNdLLGv(;|_3XI)`P0ME;LFC41_%K2T2UGvUx1}_NO z@`2Glz2lNb!-OpzrfqFx%`WZ zr`FHEl^ws%_;5j8c!Oh( z&DF%P>@cMX(#PWJyDrX{XFgx-iT-8(>Nj@s``F7KK4Vm1A7y(LwT?+|m=&@t-=3Sil%I)V za?o?>RY#XM?G^}hxbfhd*1paTHGvkZ^XszW+1`3H^e0?zTiVz8b;HRka*i%P>h1@& zi|964JdSjGY1kmBV<%zR?3p!3(NTiYAc<3~Lf-H&!;+Qm5BNVPEn8(VWlh5^k^jQp zX3k;ko_kL$Te@V6a>Qz5M&5n5pJX?mE3rJ^99Z}9=<1VKWc7{Ms$K{)9{5;o|F_wJ zXTbrx1MR)%d8B4DbUc4sSpECQ;r!>5k|t6H!-VtZ?!uQBPj zSsiz%@p}uO$r~rvo9TsGoE8Nup80NM-oSNbhxdWtS9M>Tm)Cy2xbM%8XDqk<)SUd@ z%il5okh!}2^g~z28LvENST2$X4_#dprtTCNe!Afe>jc&UbAjWhl2b#O#aV(=4q5aZ z&a+Mx`|BBh<<7Ft(|7AD+&!pM)Fm9WB;mx<=SOb5m%i2hzb3#kRj}^U55b1F>)0=B z^_}X!|Nrbx-SF@a8dZjJy(_a-wtD?myLIMFBTrhv&EM7Xal7IoW*eBKE19mo`}?7L z>}{jT&sN?tEx@AFBaya#HoO+B=}Tz#|J{4Lg{ z1`~?f9v;(MUvTfo^~6=L*j-{7zUFM`oXLK7*_|Cs`;I*>uH|0gq;BAubMkc3&uuXc zwK7%BNAIXjIjOzDs`=l^j$NzRk1Umojk(7o(fnY6LAa-T#ifdL1B;#E7nNu4_-Qb$ z@Jk?*@rgY%O#KRetlhi1MEsW}N zHDT-Mu$291C9tk-c4k#T`?lXt)Ftxr%s;Pt9Tq6NglpyHl*4h0?O0FjK78=Q8{hTn ztlI^QebVhTy_ZP6Wj^O+GiAz)S?b;?x_K@tyvi#%CSK<1*ZZ*aC5w1)N`u#>WlUS! zj=W?sXUueP4ouirUVEJ>|5@RQuq*am56$G&msatwer2%ilw5hmr;A%&EHVBk>2h~Y zY*Y9RjonXY_1T(VcF?K*#4Wf}i}SE;dg;j- zI&;^uiItoU+59YN-i{fk4puiQT)oG1lwWoUlgq2T{wF>kwC^8QdwTWqaruhj*eRJ?&Q^~y9`~BBp47hkZR&Ns z-8TYn8XSN2WB0y))1F43-fnn*Cf|pq|2gq}|KG&7{I{>Qd?3a+VTPL6Oxd{xcQ)-l zqchv((a%XcGUN6K2rwS;-T(9N;g^CJAAR|v<(A~7l7DUP6_fB!SxW9MaowTqVWBTd zJUcmVUtA=xYId)pf$C{XQK78GFIOBbK3TUd>&gaMuY@e7H4YrlwuTplu2{8$o5Sf@ zRJZM7+uiQ6HihyGvy@hdbh7WzWQ^j{nRaVd^V?=Dg~ZRQ#;f?cl*1qOEnIgE$)rndZ2Ru560p(H<(0R z89ypIUKL7+(Unu)?fPn#!&K)!TU|E0#AgqflGh#)ZGC6@YF*x&G_?f@z7Ok`@7{Y@ zFKI%E*WnC-$(s((tn~3zdKsr$zr&K#MDWek-s5g%0lT(u5?nc3L{Rnotk9y&+i6;g zJeton`u^iTUDx+fLpfd50*lhie zlf{aCVt+l{U{ZbO;&Frio#9WEtD=g^yK5YlvwoatAEs|Q(;@VM;FRS;ha8>U%c}B} z8zvRMPi{SPt%7SYZ=8+GmCCB%15VMe;(z%|{WocH{1vcR?}7jG{WGdkJB)*_)-s&D ztaf*$bWeXlQbE&)C)!V{r`-5{Y);5Tq4uj4EbSuFt&4Y9Jvb^LAmw(JqxQwGcaxuq z+dX|~dB9CD(cU+C^CF4O&#k&wOikcO+&X9f$vMZ_tqO|o-Hu)9XR2;2%q{XNIP@gj zBa5F4XFNYCGx6QmS7rO9CbY^&2 zJe$>7CDAqMP~79A8|Q?^lqY#?t}YT@zxM2TrB}Z@xXds9O_BJmxRo*K-Kwak@~l5O z`uz*cZb%4jPG`U7&K(+a|BjICzYSFuzrPeb(!Q}*{l}MmyOS^Rg-ny5@r~CbQEci`&b^oQLy9h> zMb2Y1yHYADu!u!EmT}J6KIU#)je?JxwX5ec?OrGG z%s#uW>Bi>B@5Qr!w!eKB9`!5B##vb9v_kh1y`M|&^8CH?&C|GX;f&ZDYc5Scx~A&N zx4#MRx__>*Tlj9fd7Z)Sx&oe>sHHp&lhq}qxSZZrvw!<6@b#BP%Y(kNW!Jan_8gr2 zzWBxc_Ii$lOKX}BE%5t&aQ+?TS6xcCOt!>t&31L|^;$3-pHOXThp|NBw7Ql@ZY zps&ZtLn|T|y^)l!*-`)MjGfQ5qKuV|3w#p!diJ{2N|gyUE-+Yr*D5aUaEE)$$yR=W zuXC=(o^}2dnH8t{dRm5P=Tuv1=pr)Bq%-WQ*u~V`*)+ihhHuBwFYT9UMpmdhy$eH5E zO<~u+XU>yN-k`WzGe*3-a>|Fa@{5U;HujRo9amg3*LdmSq2jXY!OjyepMEo46ELB2 zv&Oaj_CH6L?XO)Gd*HfV40GL{Ik(@k&Hnhm(1ySFkM+YZ&6C{)ex*J6e^UFW_@3YO ztY>b0`-p(S4~zAEX(=w?cDY}30Y0Wov9v=m#p#jk$fZGOB%VCT8bINxJ_%zQ6kRwxVAdPv0f*3xr8% zEIv~xqs%|8{{Pi;3l(IN{X7;qB`Lg4nYrQgEVUQwqCU9#ExG>k*~B?QhI5vsRjm4+ zqhz^KYB?8=Uyb1p$AfZ-nV%Z&Z@=5uv?hcvvfM7#*`XjiJl$w^_TnOiAI{4Qbi!}* zc@)cBzbr1;Y#6aZZC;2=EFY`;-JLv(Hns7IX4)&fGA!V@6Lt86kfhkP M>#e6z5 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

g%??%&ml8k=^Z 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

eQ=G=Gfo-n$ew2sLdzeWjS9MxzJPzd1Jl)q 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

^RzYUNBB<=)%- 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 literal 144351 zcmeAS@N?(olHy`uVBq!ia0y~yU_8mdz;J?tiGhKEr>xhUfq{XsILO_JVcj{ImkbOH zEa{HEjtmSN`?>!lvNA9*a29w(7BevL9R^{>xV% zQuQiw3m8Da#=fE;F*!T6L?J0PJu}Z%>HY5gN(z}Nwo2iqz6QPp&Z!xh9#uuD!Bu`C z$yM3OmMKd1cD!6R6;>6w1-Ypui3%0DIeEoa6}C!XgRSz4tw6&1N(x{lCE2!0jvyNq zB76fBob!uP6-@O^b(0MYO%%*6^$bl7O)Ly`6pRcEjr9$T^$m@64UMb}EUgSp6`(-L zj+aY8!KNrB%__*n4QgFcN}8=wMoCG5mA-y?dAVM>v0i>ry1t>MrKP@sk-m|UZc$2_ zZgFK^Nn(X=Ua>O75U4ROsl~}fnFS@8`FRQ;V-u6|OG|8(l%Qr;laZmZtEsEGiIJh9tC@wFnW2l5qp`D-rIC?=fg?<>OMY@`Zfaf$ zOm7N8uNh9g;5Y(Bm`w#JP^?^1i!#enQ{0O3a}~fIx5~urBx9V$LiMKLc9IEhz4}l` z+315J87b1i;tx28KVeo-U3d6>&L# zGdCNbuA0}mFpVL*qTP1RL?zd*knK^MmKKz>TOUfSFKF3s`RB0gzefQxE(*E6EU>tI z@3-tVh6QdvcD*~Y`+Q>X)(s7-)|^qF|M8sqX0Fy()A#@RnRoroy3(z)SHIf2ZrQI} zZ|UYb z)n$i&?#iCEbaBP?m}jZs-WQE`XNC7)z2>m}bynB*ue;(_m(IPO89A%${&i>9_2F?Q zH_J++*WV7(mATGqy8bp-=Y^ZE?y`lS4vT2()wYc*zq%*)_O2+N&e*GA;Z|)7KAxYs zuEusZtUPerE%@l#b+6r4ir#eBH{dA$a)xhKR)}A$>FZPKQqp185)V(R6*x%WSZ8f@ zOU1U#W5?~@?BKW4WEY)UUplo{@04RjwE0inzXe&l*Uf4F8D*!r;EtQdv1unizE0*bF9VSIgJC+$IX#R}@SyT(T>7*TQxwxu%1P zO|`eg{6el<^j&cGe_WYYX3OXHvtN476>FIZtG;fF{;(kH`WD}Q-;6by&MCp$O0Vl3 z-j{En7rSw*Z|}_4Yghixj5~k%r^&uAySGK(TDy8$*7piozQYXjrPpmg8*1suG4W#YC!n?3(p`|<0EcXqujsGQlhZf^hX({*^7E>m)~{x;{x^-aeKMqOoUFY zT>X4{X3fl(l8-|~)4v*S%bqj!)Q+ssg_mWb5?^M$?M@6larnaat!fR6w3e^gcj@Gw zEgOaS*7Q4xNgoxq@z6hdxr51bdGM)^+=t3mt+wNfUX|GDXI5s+9^vk;qOdYL|L*VI zWjk+|KIdP5_4;2E8@sU6w?F>eU0%MvzCO?Xe}UVYe-}*uO3%qXP`Uismru^2zCZ46 zJG^yW``yx&=}%m%H#7zmwY#x2SE;z^Tu(fdsC=Y@*~E4Cl!vd5%4cprxoqA`&VP?S zcJ2y#&Mj(l_Pfl@`49KUbXL7-+QU1~XlraTi@=l2uJ^*MCwyC{Sui}1laqhItXmw= z7+m(VRVT|^r$O}5tg4&|y$oTSzZgH>MB%EITA|Sk`q3&vuN;dP$ z4$)}VS@&D~5>D(FC{&foN&Rka=dj6b>V<{28eSpFPZIvQI%RksN@}_x% z&umJ|XN3q)Sh~~QtLL4COXkwHe{=V7OHU|P|NVxgtkl}%+A{48_pHozhv;^rEgXXJePUm^y=E&B~N_hobMbmnYcpguw#Lku6){!63If(*_k@0OAAsL zPG#HvOSh$CtyS*T_9@Qo!WEUbSFwp4Zxaf3Z_<(||K#!}-7tbn?6R#+U}RaP?hnal zDQn)ke0z9-xB7)of`kay;efsXp4F?T9X`U{ta>a=|Eb8+4LSe2f`fyXAF1z1X^U9f zAyRgZyLLhIKJIVV*PhYcp)9b<#hdN+>CZcwjCx~AmM}Lo>|Jpz^}5dCJGL>68aEPE zRy5paySI+}=Hac!?@ajd>h`Z6+oIS$O^DrWw*KzZUkVp49R0XY_IafxZ~O1;+U>hz z!uLp4h)X@2_GIC@V;_wkzO21&XTSDwOKazXHn%-8(h4Htf$UfFW?fmF^Ww9V`r zd9N~FxV+7cKWU=ezQILO^T44rmIpGQqt+I0@pt&KNG^Ymvt(uQgnOGGvzcy5)PEYV zL&JGf?q%j@3M@h?WxJCP+|T`7ut!dEd&7S251XoQoc;ay?&Z?zb$b5mADmygu6=jf z_lhG)zWa?iyO-zXmd+7B_gd4|md|s6AqZm$m?D@!W6NDpUH9lat7vyy`1GQ z7?S$=`Im4ftX{my@SHct1&gdXK?x=1oGVpbR(dFLnq8lfnQ|lIcGlj8xa+%azi;3D zxWV6u>9J?StVOE}e!2Ta6z^#~`60M!g7&IJdnHE6Dz`Z8`&W+XaXfA0z1%a;JJ|Bt zv!yXpnT|NM`6=^FP!$qri$85KO)O_#^PY2u1g(~wa1KqIa51&yc%s!9kusEF3&Ee`M5v=9e`Jl^8*nREd z2h)9@Yglg!mn!H=S-$o}vE&T_uDjYUE`hIm&Rt+%HgQ+(?sfT&V(d;m8_m2wTfBLH za@WbWSI)P$Jd}Q2e)W!Cc75!r6TM9r7`B|Kux2?jJ5aVF)_z~~xBPWiE1CTDc7HB% zu(N!}Zer{oTwnRJuRZwgHto#@f3wPW>Rjdwkv+ejA-cD!ZDFE}UlqUZo-V6o>v9pkS=qYqYgL}4$XOSI6`!?k6fIjWejtTO;gXb%0^8q{ znVd_4)c<$o+ls9AZwXI0UYA!C_5XBTN?4Ag`j;QM@QA2%;8&0Dp$bXCQPmcrCOXY`z0Nje(fyf~ zX}7;gEX!y#3rkVfUuMsq`)lgY2@UFx7<3e-P7C!rEWFU&`DpXg44(W&3-&(xGbdd+ z@51&+jt52A%u_$<WjX+Yh)g-QP^zKs!=gTY^nA)ksDHBY?CTBZ;V{jvn9m+vl)lztJZ>*Ixz>*@9Fjk zoe_4Pq#U(+)$6=-655)6oFPXFpUEnUJT=ms?Z}cQqV`fjeTht3!>U7#wl6CWNfw2M z?w8-SkTLti0n?lMrYzhi4^QPe5q9ITf^ws`vmxsb)-}y)pYFYp_?liObf&1kE;k~f z_#d&WTGF|!m@~PLZYwybc zW^a~0QCWWeKJ(hstU3pbZ(q}y$04{?dd|E#rjPHRDtPCuYsq06TfNWcxc>-#zT!)xF!JPfle4e=KieSF|nn=fgIK^sc4 zFFi@%pRnJqE`27U6@g4~UzWI>YfDZoY3};7!S$l+lC%2-Qm$MmFU>Xov*K+GCx;NH z)|=VVi!^>*;a}CatAdHie$w_!Hjxg;Eex(-cAKQ``eAYMq2qJ9_K``hntG&U zXUV07F|rufAZ$PQE{7b|LeL6!^~G7%-fDC zMDAH5F_|~_AhXaT?W4jLZc=;NYoAG2bW62{ZdsajGd#ZLPp13*t_%=h|Nqi@2&Od@6}iB=_;`-J54FoSr5VkfLkJ#b`Zs z7VjIa^fww0C#bE^=G9bu_Hos0XVI;HW(04D_p0z;$g$r2z#U!&@Ap$QoaUTAKVyox zZmsborn6fX6)j}rcbqfZ)juuF?Fz+pa8;k7BTDYAHD8G3ovxbzkQj zu~sLG-roA|X|UW#Gj5fI?3)_SgfEM^m)TBEN$3zd)!V;8uYAiLdFv|k>#%tUD zvQ{oXE%$cL`Q7K`zjNF*y*@E>y^#>}+pW^K^UA*$+=#nyc$3fV)ZP24t?%rA+n3LN zlIhdiD$9O`FE78g6g;{9LO}I`JFWtM~2SU0qzhKYQ1*-8J|4PXG9L_il21#hZ7x7uJ10 zd%o@6Tm4r{>+0|L-TnBfYUTEDCo%b>Dk{Z()tg$4zbERRJDB-9RL3$$H{5@b>yant zFHBqSqB{TNZr?1gUVF9ZZ7hA#fA39Gy7~Q5-{mi*p#fwS; z+uq8gugLev+wt^5T(zI!mq(RXN*2ys$)|ImJv??>c7Dl(c(V%isWTa&uF{$4%?~oLnac*lON;xa0U|uEJZpiujK5T&k;E$m!l;?7v6T$@5yn zu7&TXXD<1_>)pd?oI7MP*DMk%Q>|F5_9Y!cWs{H@{wY_~nt5+S&2<*PnJ|yZ+b4@ZT)cwa+u(uKR60+2=z4nY*i( zB!B<2@zt-pMw8r2v@&z_l3BA3UEX!}uI+(+vhQ}af6Ke~s&G#G_Uz4mwRe+`ecUrQ z{@J={z2~n!FMnB;YEWQ#<44c27q@t$JJvQN{aLYR5%>MS9)Zi`CtgzflbsZ`V8WG! z$5PW0cwDzGU$o*#J%@<-f;vAV)@d#sQ@NrPzVi4lSGW{d9dz+aSkbM6>dVi}?qol` zz5ICl?)kQNc}CcUQ}wl0C2Y+3vCZ^Jum8h1K%=R@(bqKm7FR%spS* zFPGN-ioI>1@y@eBMwKQ1Mf>au!Mlu$D#9-D+&o)Z=6jXHCs(iBa%IVz&fK5}I|}3; zTBMuoJUw&Hwz`++KQ)w0oX)?=h%r32EvY%_tlgdmE~07D3~kOA!c6<~<;s>#;BIJ} z@@%_@s~OvgwGN9lC3iV3Z3uAcT(@^}gLu>WXn(ydjkVXeuHC)vovV;|*jtX>Yr>cL zJjj=PbHV5GHTF9j-c5RI_4m#;#(h^rY|Bi8JzJuUM6COlcusLlIKZ<=N#)ccpND7X z?A_vMx%ku~?->VnXNSjI>t+~*i6}Zr2@xJw?m&>cw{q(*+wz{aSRPZG1-r{VQ z{_mSCpUnJxY)J?Ew97}%xhVEN4q0XpwZr%Ko6L~&C;JTacYkC%XSM9-#9Emz*M66p zC$5)me0Bda8`S@3N0zYU#NByOB}&Z==%tjMy#bB@Bz70CvnBCD@F7P!Bm zaWBV(Ex~WS*lru82zgr_JaDc2`0m=<7rhPrf-*mT;jdhMC1Iyw^D^^_WVXvCcR&8y zVE--l{g>&}&L}_2Jhh}xPvF`aVWz$vyR3J=d(rgm(x;aeCq(#n{`B}``eXj@W&E#L zmT&*wyJp7sSlct<`4tVl%OYPr+qCZaX>EaD9p!xd!SxaI>?gh`{d|Mp`|Ou)&Z#$3 z-Zb@Bmmk^ocJ|WS@vjf()!V;&f31Ih`R6YiX3w_OV0+xo#`3P;d9#61)n5tUI-B@{ z&--5bo!`BD`t!%K&k7Qkzx8-=F#PJ=z+=3!hgAMdzOim{VDptasBvT109jQ@9b*+@0or5 z{A2#x*|ziNAAkGuv-el4MfcXS#mU$|^w{;O>ap0)vo?Z4$G*7x7x~rBxHKc+R7BvS z-V_s~(<_`iWN=Llc-Sd*u9{kKW(1BKHk zcQ8+!-d*`E&7H$3dJ5yjMKN2Y;#~MGo-&UbZjW%(QI&Ymy ztKZ|Ial5e3OY6vr#jExxbqi-76ceCaD(p=EEK_{GXHmL@OLluz1gJJVWu zeP%F6@}&uaCluDIy=ih{ntxYK>OUg|d@z`rO;fSWaqY zPVH6OldhrYoYQu&)Y-&|YgK=$?)+Z0X)nqr`Se?vF>9XveQxaRk{>%MuzmAtt!S$lo<`0-A@@%#JX z!{N2n1>3w9cvfcVc^mtm{rmgX+3SC!%zOBWO&f);uYJA#^Om{S`5*t>eLsF%*_~B+cHyT>@AwH=_TRl* zswgqHGwEVNwv72~o13DCb)LC-Zg@X4-dCdUb0q80z7MOq1EW9B+~Ot5_ToTGSf7vAo~7KH(Hm65U%u+O z!e|6$hJcS#6Q(cd%LyA zX&0fw(D+vU3cUm=k%wC_!3-S?~07G2R<%L(_g_CYnDXy#?{m1Jry}69_u1dq#n-=mJ?y``a`y55(?tv4f4jlM za=P+nRp6&f@7MRpI~W*ii3@F$(s_03_uW0&(^D5La4+n+$H&K(`&2jbhQ`c^l8-j& zTz=DKtg+?XnR_h^o3~2`X)J2WPVA97{QAnJNArEJYVSC^$nM{%#>c)rN2G=G&ZkT} zQCL{g>2OEOdezR@kns{92 z--XIf)APr5L%Bq!o?3HscDKbgHg(a^SNkI8u)T9ZXeQr!K92&~<2mLQd4Y zu6qX#cgq?!S$;p9m@w(cib%#aDKqZ*87@7&>SSq64VRaqv+9p8uHUN~BZc1c+~)KC z;$2{=zIrCx%G|U@D+4Dzi{RIDx>l~MUCrQr|8*(bZx1<_bbTosxo`Zx*R5Xvc#F?z z-A2bbMZ%#+8jLJf8z!FgY(6B`UhH_T;9joFCbwq{E0%f&_S$uwy}-c}?Y2B&|8=94 zj}?|^hKf1~%#do;^4kzPQ~yPQmsCl|g==ERyTdfPUGHA@*m3;qk-VqE#vjw(X`XJ? z7GbPE6=L??Xxg2Qk29Av{H_1ym#S*yb=UvVoCQixn>HGq4bf|EkPYoh;A$w%`O>b- z6ydt=-wpA^hHY;z@EuM_TQWal=Eo^|OCm(CH?=S_te&Kv!sl>URS(ixRh3Y^V;dp%j-;V*Nziog1dl35NZisKoLT8RTt|L|J#U>nA*ivOVeZ$-8<5lfTb0z<7aUh`QxW=nrg46ud7|a{AjM?B-y5r^d<5xJ{qlcFGclD{#{|7E8Ucqylcye#S9bW zE^T3F?ns(lTj}ZSFW7Feulq-U}aT!3z-!leSk*mX0fFyyR%)llj$jb%>2 zO{FI%8dm78I@BPw@$(|NwI{rP>UQ1ZP`KA>ZyqZDc60Eb;8M%X6N#@j>|}l$6kw&H z8F5p>!ijC-TgGWC7}ASa+jcqVE#G!3V8*Nzf4?^?dZZ5P{oj)}cacuig|K@;lm1TH zUez+y#D5cWC4*=Z|G9;o87EhLan(QLz2Q>9jrZ;AKA(=Yk{8{lGUcb1{jRF}cYObExxT_HCB00SKFgbZ;mrQB!a^YMvKTX$%*)6dlXx#X zFiN@js%`#!;k$@`@$XMnFWP7IbZ+zcpMK!Yu9&(XiFa++U42}dEB|ZV&1HW#+MB=r zWfbc^Bd>AC^qq6X^Y_F}D|_+mZ|(P=!Uv+R-n}!otNpo2%S%{{N|J z?X-O>kHMoZ~o~&$IB9^j8(Z zH_rP%Uz+Waa@EN;U4-qLq>hY!+3~{b zE6*gpn0Z-dkC5M+MTNVRv&8=yn@-VWO>+M=%ap-`b4}1Gj{Aq#ZYn&t{^7~sDQt^( zlm;wMSf&%q^{t@e^eX$fkaN?dRvur*B_L#`(RE)kKrp`l_adPnj@c@|G|t}MTKW0e z+VJnPn@+88ouXhJ;$i%=AashD()!pR{)b)E&u)|vE)c4o6s@r8)1)?|9TJSwdn8O8 zd1a&D{^KCsUMXeSR(M@$M+^%%JNb6Me&NCM+%02-&iz#81n9_8Di9sjKX(U24kT z1m!;R`SQR}JwUrEC^X){@ajz`gSAf6oNJZNd|1iyME_9ZJm36fC%5h1S)svtdP{~M z>)$I(7U%tDY%nsheOZ2Kmry(3${FgjGH3L9_2r%Z#l*10qLp7|0sHSJzbCp<8~!ix z&JZ$RxZ}*<%dvqwE(>=a*u}Q7se2XEYlr1CEbrSLVBho5>#48ao&Lu+!hX;H|0LHm z_TSm~n$LIn_y6zz|M7iab()}3vPH)`Q{_{hPktl`N8h~Owou!b}{@7@el(b^6wL#YQ>vtEwbcx-6?)JZ3Z~Avv*4OSZ%l}<= zBmeE+Ulkd*OJ&bKnB{z3X8)~YpV@fc|N13g7i)jz>fPPXuH;|7T(h?<|N1nS_4`(T zFMs=PYRb}^zcQ~kKA9XZlF+o(ef`;O9PWCy#-Z6OTJ2U>${8}0F$;OLiS4gnJY$#7 zy{X$DG=-_?-OOiD=3PEVgTZ%!;d#kfzYHcu&YJRj%}3r7x55@nl{}fcLL+(G;=sTi z=h|{h6^=D7V_qQXSFy6-x&DSJx}};2PHt(rUK90x*^yJv3-`{^pZ?io|AQB?cgt({ zS6rrA5g5x68Ptn=*ri6FS@jc1{zVhs%FdpG${-DNNk%2yS+B(;>cn(Mb4ON(lS1d0 zl7!_yR^)zkS)RY_Rcq5#3!a9|uUqs|t~f6|zW?Y--PC@I$yu7hM`vu?(NcNYWz~|_ z*`{5G?vz;Aosn6*QGZAAZ4VJm#o`Uzz1i|d;u^MISeX7T|M1fG?fmAs(mzkGS)(8> zPtx?GQL`|aktDJ68F!#6gs8o+G~DJC8mSi(p%5Z zJMoz->^-lA9Dn|OTN%E$FaIztID7k@QE7p7|L$|o#X{!WC*7U??HzA}v^Ue-zAs&b|qIcBcG*y|F-(uDT{}Hjo&Um|8?WOZTydKtl9tn?U%pb z+vm%E+npu5`eeT5zMSQ+&CD0aGsLc{R0&-9>fnN}Rx|a&797d;y=9!gIi7LV^31t< zaq1?|f!^mUi>4^}iu7YpZz%d&Jy` zTP#|s+y@d$W_|W&UU^A7GQ#Kc<7ZK^Ho@-F%fFULGG5_6%qb8Uzk8YGn||Gf*G>;i zeq3OTSuW;QB0sn9T$s>T8K*mE{4&1q)V*Hf{@?)r>}|>47<|fqEctlsPxyroWx-7H z=Ne=xx;81S{qkxtOIO#0uOGGYR}}>cPt0@|p2V<3D`}?Y62|{u-kgrDjbw|BGffTA z4xB2j>ft*1*~gE$D{f2{IuO+Rt;#j z9d@}}!vFi-1>qTSrs+|PX%)c@?^5GGY+GqkEZP#)!qH@!{>4yR;l&$U`z5Es-#1>= zpThmDvRU#+0n-^bul1(Nt!IC>I9(IlU@|jbIv!v9Z{b`1&nLfdd`&Lgf$U{H0K6p-Y)Hq7g)30a^@ta2?lx3r|-%>7j3FzoVVh{jfJw)-ktsWa_8?q zA@%cCO)t%u`S@}B)vD`;^;vfDui6uH+ujx(``7+)3t#)%UGvw)n&uvUefnYT{=08q zPtQzS_G_K3-TH=~B@tF?8lSJ}-M%8D_1^o-?ln78V%~lC__D}LNb%6N1nr-5&V^c> z=FapqD0=zqS>7Du8$U|=Ef#W_?)4G0R-PU|=L+ZFXROC%clne{i8RlsuDE?ZHzxh^ z)-Rc+J*v(!eHlsDGwvPqKXvoQ#{3vVMm_W0buVsy(^YsOYoV92_6LvVs~6XQIG8Lq zTp=1?=$tZNz1dflZR?Jyiyqpm?3O4=aC&yvV{LPGqtsuQ@XV6x@W~G@a+DtY#P9d6 z?Z+k8IXeWr3{t=DqLV73B?T-}`;I?*u)s7&rSsH# zz8liIWsC3Yrx+NXTX^T%y8c92(K%X2r#bDMShDs4_e-tA$9(uA6~CO@{bhlQ&4pL| zO@?=Mj&GS1yL64Y{2JDVB*U3Y=QhuqX(<`$!H_uTgP)J&_2*Z_-kz9jKAoHIuCxB+ z*Cn!R-%of^;`O4qq9OKb@!ObV6ZEZa78r6l#;iG6{k-O)@Q1EX87t=LTy}gBa=cF` z;DOCGgPz_zuB$D|GxU}^O}+8KV{XRjyj*$Lj`h<#0-XcSfB)e==Th*V=vBvpR_?i7 z+Gw^lefE|wTYeaJ2Kwruu4HBQd6894smUpqG+s{33TTy#)!271vC(Yl z3C&AEpIN^-7KHfZad@Y!lxV87uYJh>qUlgUc<$w|MzQhn<^Ja{|31#XC^@oB|GfYI zPyYYj*#Eix|K$i)q+~jJtG+jVzBq@&JnG84qC*FC=XH9Y$c|0XT(kS* z%#G9U&T84>JeTiCLXX;LxU&;M&B86~}Z;>y;0=BhCsVIeQwrU~+9FKk-7b6TtB#8~N- z(k_=6jndg$iX-kZ?f98Ab#=^k&t>rkKbY)3llH?~YsOQB1BS0eHGHCN0v7Ms@A1&( z=*g`c*I52}WiY47f0m2+B6gj|NtLeOFB+?JJlWg7%g}MNUf#@v<~0IbF>9=aip#d} z^@<*}I9z$g?_%Vytnlsx!`mNVs5?jO+{h@REtZOf>UPw(hY39DZVePdwDY3V1 z1=grcdgT7p<%o+>FJ#A06!dJ_f`cu6Xx-4d=CWuWTRkxPNvP|8hq9?o4hW zi$%N|4!^8syo=fUTKm>kPluIXnT%?6dN%OA;?s~|RqD%|!RVak;iQz)c2wz;&w(v} z4@8`ua#pp&rdI9Er04FZKdkU}l${o~#L#kuOPfQ7%;AhH?_aI?#Pyv?pvYR{-YS=J zwTvrUcQL;CE^F+eP#f9$f%UV=w&DoQ>cEAUT;?0{-g&rNpvl&L>C0u?BXwuzEjL;d zRH5CvC-{KK$uqifK~j&WD=Oc(>Hf_%oq5Whh z+3vsGV7q}!gxQkiEGs4cN*WeEEe@T`J1NQhaG{j`UVh&g|NM8WyUlm6`CYfC{>$s2 z^ZvGstNh;Vowxn&YRL`vz8Gz5Ppxz#*jTkg;SoKEU)?D{w`TLzu zCIl_HHfzq6Eg$rEpL7Uye6z}KZRh_szbi|ZU-An5|L~d0!u~d?dAriuuCR&E)vQkE z@a>hmW?w((Gf}qmjoQ08$5zf)&eTY}aqV^f;m8M^FMDcN zNs71{FAX=#S{#=0FeXvwm1yNk(V`%o+w#xXTAb6pB_w^%!t~vYLju0V8U=1eDQ1am zbGP0*-yd)<_u{s_K4PIQYS9n7WR-8OY)ZTM>Y%W|G!8|XCohiK%{#e;&#p$w1=<&~`QK3uR$gztsS zhQpcCT<(pj$K@F%641!)weUh&X3>CV70}t<<;R2$|pFACd%-iJ{*3%G%osms^h2J`0)F? zPngF}my6qPXL9}9FOz+H>Si4OX!WGYb!-0dV%t~`X`g$$vivh23WheX<&wyc5!-)p z^P1Nc^Rm-TKQJ2a$dVK}TP|{`t>C1^`WlngsC6vWs!|&d<{ve`F5c~A-D|@sYZbF) z{k|o^&(^G8WD~bc)=A56>D0&HtgiD$KkX9F|ER?rmUkUWP zxjDaj|65Vy#S{gWXMSPdGPBvfq>$-jz6QRs`R? z)EyRmwgn2|q2c^`$GY_YU1Z3L=x4iBDsaeP$DWr*rd#c~FTrDNd2n9iQL*iw8!h+b zF!@Ya9A&0B$ulIYV}r;=FZr%K<`<8a{rdfJ=H`xr()yB2V#d!-x3_*2@)rw}dRAGW z%gWf763Kh@s>!*_6}1&Ieczc6%Ut^~+g0Jh)=G;n>?YUE{?4x3aeGzb?_d7!`46wF ziv8Zse1|D)!;jtf|NQ#;bI;Y-efe@bCK^sXli~d>dCvmf{Vxtm3;%4~sVcB&=bs{$ zM`c<~kvnck={^3X*t+z?n!6TPmwPiitkt%(xUjXh*k+HVQpxcYnlA^n5}*m?6=;3jmO~ru4_B$_QbVi z_VaycS+y&E{qvV^gl!)Pe~ax)LKy+x$b4<7wQom44Tkx<5YBvD;vY*h>$0 zt|`%nBKLF{c_r9HnFrW3b$B~pDY*K_%dV@4)jy^+OUn6d@b=uWBm2#me?}hfSoymcDXm}3ijT*- z7W)W2usI$@qv(T70K6F*s_U$Q^=Jj=iN zf5z&(yR)*-@*K~u*NC~lWzp+x_Y@cwt-boDrLq3d#@D~K?@6ZZb(N6X=;kz8W~-p* zVWBv;6%*FI(4Ia01V`0|z-JeBe;0T+!-_Gfa#z=n)F(bB?i;-xEZTK)d2Hm(^`~D& zew}cvVQzf1z_#_jkFm#JWC=aScyhDR^^~rTB#*g!)WzqOT)UmX6uzpOc`@o>Zz`y!pt+#-;7d%j$v~>}D|ByKirE$9B=`&y^~C8Jc|=>`x6M^3T0+ z?lvr4 zxw7@*q+d^!KQd`tzW?@>uyM^r(R&hKtR)riov4UuKRo|dPgVKbrojK7*4fovt=oG0 z|D)ND{rP|2t^NNu`*C@%k9`lvoC)jB-uZO&VUeNlx-WGCEpr^^J9hB&*jwcbu9W$| zqf^s0i%(|8g|((Per}xQbh^6vA1Rc5}}Wi(}0 z*0XNLd!kP!C^kq2h0APLkL~oD*voNe8z=wC!=_>zZ_Zex7`WgAfwpY=&)%mp7( zOr*6fT_kQ7IfqTE4zy&iIB$2pJIU5%)@=jJ^3nrsSyzr{X0q#VpE+?tpb-C;Jk1$j z>vFc3Tc=OpEB=3Qxs38`D-+qaxv#vm*j_Ko+7NQarCd^du`2gVKFbcio}15+@^n)fbD_K5P5)?!vp(4}CAoZJ(KFy?Vj* zjjPX{P2Fkm`CZY=?Y7t6Zmj(IZ))MekF6{h)~5bBR=KF_MLYAMkdJ%WFQor7`D*gh z*NL~wKJ@9&Xd{`>45`$Rxw})>#l_A3TIK%D?bw+q&k|mg@2~sz`nrJamfepJo86Cn zHuKBeJ5ImL?iN|Z$=zF*|8ClPsSUrYX1#t~nA`sA^ZV7i%Tt8Do(`_Px$SY`b(!n^ z_ZNpv+5D~idS!L4{H|sD`EJktcr}`N+eY@KuglcJ`y~G9G5GKu{#Ws)&)v>b;7=Cw z&0iI2LTA)_)BaBQ@bI$Ez8wOeRI6@m5t5f^?>J#;ency+i`6w$>9D2i9_H&yH!)6~ zwZ~bxK1+8)V$ZCOpfra7kEhbx?#)q&z1$tS>ayA$XKl-~9#e|1y=v*@-oAU^-M6n_ zrS~6xynFY2{RAwtnsN@BO`7QS<9ZO#JeI35PQdtazlM z*Ij&hC%;dFb>*pFZL@C%%G`W=%<<-usVjEM-qTtdYIa~=@7y`ZdDt|~65!`qPlWS} zw`HexNxP49i`Yh>nZTOXe9KoPB)v@$Gib zpNcFUrydEr7_)Kxul2hgGvC{^+VA*F^VhH2jTiq|xlcxpy~?V7-m}cN+wT52DQ%u0 zRC@TBTbi}L#nQ=1^RKm8{|L^~%UJQREzy|kquT2eO8W~$w=FJ-NMCSxn_v98%SLmS zGfEdoeRn?ixumxG4S%fd+Q+*}?0;r$-@Sjq!S<>hJ!dzCo@u#f*K_!zjm)*}aU1UH zeSPJ>i8bmm)~eI%9>Cy{bDf`XK+YkF#;i9l@&!D&gaQ^7i+IYnRsOrfvM%V4 z|Hi3nXEmvb=A2XZ{NSc_DnMn*i@s&euj2e(`iJ>&OK$M^FT0r~?fD<=&$iR~N`zl? z3o}i*rgSJ~Et`?$Lhc!YO8wI|_k7kT*K{M#!+R19c#kt2 zu+w^WWtz0Flv~rC1m8Wu9qYDi{My`5#?5z&W7~(287+NRp0ekP+}<#=X{Q2{;;l!_ zCllXH*r(%tKQn$?QCHT^sch#z{{HRr-`L*U{pY#RRXcS=#V57r*kAH=ys$H8VbqPu z{$`6#vhMwJs6|10(`kt(_WwRqS?cDx?_-aws+Ff zv-ddfzCD&_WAp8E>dbl5>dsH8pFN*vwo%y0RlAb9m3X;+w% zZL@^pcQMVn{DkvUHbY+IpI85?a?elhdd50`$zGoW@o~%M+h5^7yy;7_@tNXK+uHE@ z;J4?y{bl}5-ktek{rVSgijC)$C0noiYA{dQWUoDkzwP3~TdizQ_wt{&)6t(V8Papj z{JXV5>Yg))_q}|V9sJ>RRi&(Xz7gxcb&HQ>2FLTE8DN3k|rhNDK+P`F1}O|Ng!H-@kv)?| zr|U_nYSXEtAHC`3>#sCKT08meIqbD=O5)ib?xv2b(_cTi%l3+SBDcY;yd3eB9ko`C z#A%viYMe9^%~`w902y;GzLURQ0bTs~QIk;C2g>uUZg7ksn=6R)URw$?>{5G)bc znd1MATO`QiLq*rEqGKz0*h&-`T`t7m@yWC~)3DM@k&99KsCUrZWA9qGOuZ!bK7Gmo zU;gU)(?@>yGj^Tms$XGjJ|`gCja`r>XcN!P5*E$f9HHBP%#Gc6JL!eObj2lm;yf4o z2~B6y`Td7+yN#{P`!`E(+uoCXUte|K_v^=t&z;`B|GwvY==Ql`*X7=MRINXMf4|%; z<-}d@+fHZewX9_@c_XU!z+jJvn(+hGnMq8X@e4M1w#coo`eWuSShl~sZI?a6{Hz_B zRf{rqnqBtsjqUl^VEM`@eEQmdCCjc}k_)dl%AO~$e>V31|GZoGzFmF1@o&kzEQEd2y+$5%Za2ibV%#(h|`>TUVo#Ohinv&l2JyGz{Bdz=!*ko-65_h+e) z?{-sOZt@ckaxxCfU*cers_Rx-%0F*eA#?P_wwn>x+`6J}tXY+2ae&?1Qo632X-4JY z^WyiD85GQ7CM-Pm!cZief!l+xwM$Cs{_2^q;9R+aWw+qw5@w*G!S{ayWv zbKl?d=dXUa_5QusS^sLwEzEEKvzljk@yB-k*JV|+rgJ+l-p{~$W}&c7PwTUvC3gMO zg%yqT-)>4dq2TVevCt#uV7#aI$B4VvoLA56+u4z!w_0mKXXQ0nljW(OXY+nhe&lp~ zj+n{09!0K;%u}rbohyQl7xgXPD&)11^VuKeoTW2vE$&p*`LV(EQb0n`0X3hOKaAF1 z=8-S+WiHyrDJHS!f-#3i%BwdE_Skp6%-<6d+s4r(x8?GtMGu+;mUMPxYJV0f6JK#q zRMlro@%H`iR=W8dH`?04ui-o^BRXrcpmpc2uC=zC`>*_Zq`yMgZ{|Xelh0imep(o= z=;byt6c+iDX>nYAdO*KQ2&?+5yqFVv9J#K_EVxp&CA3g0z5K z9e?yOaqHW*to`ioZ=K|}6Mp~KK<3@+{JNhOwcksA?vUM=_~6jm>=Iw~(|LQ>JpY(l z`G4~Fw{_-r{yvJ!DrX)O{54_Ev+a$k7cBkx>v?uuEj=LXAa~>Tk>J15@{__(PkU%^ zppC8Pd6Lc9EwS(A*;dVuD|~BPl(zKrg9Fz!l{EZ|t@~_`SKPb3ZMWI$%3JqJK3CqX zEtl;3Ug3OM{zUq>%dZ8eF1S~@=A3vN@8QoyOy!TxzAdxMREx+eTmSjw`x>js(d%m8 zhco~AbRm^hi?d+A{x#u$*>%S+Ed1tv`c-+_eIKSNf@;FD+qopG1sYZIvey~MCGo`U z{q3s|rg_cZ*KYg5s}&J;3%L{3rx&jNvpAVO+Lm4M!Un@_toQ;?;xC z_k{NB<8a^4%aDBT%`DVydPcN}^qoejl1~%n{9h!ZH^KkL%-fbc4-hMYF12yxv`Vd%K&#=03^WnvORgG#+$%aPQ}g{x35_1SU;j zlHWQ>XW2Fban|aT*mDsH=ZjAC*(iO`T335Br~R;or--BAw1Wz%@-qbvb0=1Iv z+?7?d=u`f8@q6Egz5Ozg?n>h6FIIis^5zU2&G8QR`{nY*nhaka(9Uyk37l*L>tSQ+-iI5n(Wf}#1Kq2QE6Ns*P( zX{A?6AN1tiJaskm*c%-Yg%G9(YV*>XX52IQK0WjP{@cesZmO-b{BnKz()wN7%G*D_ zOFSs^eEQ>UWxoV@j08geK0McX_vMny)|Z%~|NT>05#GPt|M~L5eKDI8Rs=7$3ch&x z))&8Rr<`++u<9>8D74syxzpj0T~Dg-fshcfyGg}UCM5Dr{u!tHOKJARhlkl$tbU-B z!WM4jZaQhIk+sN_UlZ>bG_r4*nc!b~+E!IsE3&e2_KP(uuJ}zcE4`r3=W^+Pea(LB zKTq!Nxt8DeSYQ6v=fe8Gc{M*W>whJF|9buU*P@)hv-j=e`*;6*e=M&&s;>6tzsLN` z|9`vty1vfp|F_FOQ)kaxo>?_Hk#I5IEYNQ&~LG zm}P%tblIL+zKeDwwJn%_FMy}(KH7B~Ad z-glNqE?W1UZupp%@ojVjfkbr5|0lGEM0~+})Qm`S#lBA6vR{(Z;+F z%bQ+g&YCgtp!p$*%@UrnuJIMhQr2?IzMYf5P+$H1>)i`;9JjwR=s&z`%k#?zPdB_c z{KsRjvFch$$GF*N;!T|L6c_s6Ib{5I!}~tjnCJStAe=hmi`M~q}h6fjfJzJczvLk)nWiz{n z2jovD%nL0)lX&*mMU$-!7ddmMw>{!KbaShC(AT2F0XH4(*k>~|+$x^DbCJ&XMSTDM zac&d3D|(l)M&ok3|4yT+>vJFMVAE8by?b5El88M#-DE#w4B)I?a`QU z|LVeJ=1&Tolw`ED^LK4}dyrki*SmO?O9S_M7uOggy}4&@zxLRv_l`?bKSM!l%EcOM znRhkMHfAjS;o)MY8?iX-Ge-p9YwP#HixXx1*;*2;G@Noh7&77EV`{13Xh61d2uWibHX0U$!^6QsZ&dR!wQt_e8T<7;M zn|tR9OWwYOxmuLR8kl!y?@az5mcg@!)spJ`0 zyF9aw`m~~b`1?Z zdBt;lw&y4{Q&}8zx2L`L|xon*kkr7y~ zvBPoA$K{V-?LJ&%E;C`Zy})UWb;@~G`3G;s#>p(1Xld`bW!u`~+=kov2X(?1B+Gnt zlqoknov6IoQ}2Pq3#RCcd)|bXv_^hqjotI^@`X-W{TU(xOBeiN34QhAIZwbk*ONc{ zqYK0)64A-S^v<>#J-3`R}*=zg^z$|FgyY-)2cpaoCjk(JhqMA--bzL%n*x>v>0})-e9q z!(-%ky7IQ3@k9@%72kK)WgL(!T&H0?zf6x;p(?XM<$QHc$LL&d|Y z|IH4C2UoJ$7HXLC;{PTBLQc47U|c zyO_JeP_$Oja__?G>#Yy&1O)f*_;59+ki}Wjb;4npK$fqq%NwTVKHhZVm99>o{x`jO zo@drG&-hlRcfv{`VAk~b^OxKwsIP39X)S&4u||`%^I`_xLxpnf0eY6ZCw2UcInwyz z;+v2C4B~y$3=Wts3Sn+=o1_2ai(;^=$^#Wut6r7~ryu$qOOdvp6WD1!!{qqFcbu~m z6@DJPIJ0I&s5OiERRICc6;J2Ayl{#w_uGp@tI8fNk)FFL{1ppN;>A-7r=4$DEiun7 zCyjr5@8+M2rdZ23&H3Krktb3X>was>@s$Vq{#;-dnD;)$$yeVqt9j3jp!e6jLnj>0 z%KPhRn>b0L_k8EtN1Ih&Fo+&3FVNl0Ze+jQ_V$eX?uKS+_tv4wdBA{XAxRj+&c3B{`~mE6v{Y`tRKE zsSV-_cP$cjR{hSQn{7X@yxadq@y)r#AsVF>&Z}OR+U&3ScG9ra*rs8%{(8P80n+V< zE6Qc=-x}PhVKBy7q;7)%#6Prd4$+=`_7&D=W#{Hjo9>@GoVP|k zG4k_kJmbu1dxd>*WbD)XU(Evj8@b%$Ebmt?ex8`|yYI-dMN;V>SPqEY-KEn|=KH%K z?B`J*i3yJ9PfRfUdw=@s?JP`JUMgBSihgVQvF^OX)dX{$4J{K_KfEX~^@=&4=k@xE zc@Abpnp=1MVA&@bvY1gTB@Su3E3%^Q{nE_c-ZUdIbGgKM zi$7z9%Aua~PuA_4I!DiO;*VC3@E3|K91E;H+4^4eoqer2LsI`mVQhWP{zDDb`}J3Q zUugDz@`J&OAz|GUU4bd_S%-MOY-Lh+mb~?C#iNvNezSIg(ph!7K{H<{KDT6Cd8JtU zPp8zC&f?#9izAMff9R`GX0w^-)Nu57>YsD$k0*Q&S#)gC!Xq8aLe_5#&-h)+W!2#R zEq`wp>#o(O{l9n_792HnnHD3n!04_VpSz57EFaIt*Y6(Q;1As_r#HXPlDTxLJ{}+G5gqiM8i<-@W{hPuR%l;@#7C4}X1l zw(7ry!ljZ|$Iq(o6SdoPX6aN#rw*x0eoG3LJbi1Me5G+?!%D3?*S&3X7kh`Ryni76 zg5k*T(oKJ6+_+$CBUfFdyZ-;7>DQl~NqpYwXQX-Usa1aUiceb@mRyc1%Q=!Em~6M8 zL0Nn9jRgsN7R$)8eGh-9w83Tb{0*B<{0P?-xtF4~JmdBD6y%L@n@kT_JsB?dv}|+Q(07wCH{RYTy0x#1D3@kWc^f_L^1+R~%bg zbKP(1LY4(V3mDiLx{Y|tMT&R!x-D6>z~ZaLqRz`v-+T`(c$hW6n^}qXVYB+fWClmM z|4iaJ2P1=jF)2?GK9us|qo5~ys~&-) zb1V`!)mW5!V%KBwP{Yep^$hu+d{|eu9QM7qM9p{birRRLh zuJ5~_uGF||=I!4;Gmpu~O*22owrcK{tYpRJZL4m*?{l<1bx0%5)tslIQi`pFLrL&` z$M(%f@BKNN_<~_=OvEuu`yN@^Q?|J>w#LM_&qpowP zVamymJY~{qxl^XcrOIzMs%YQFo>il){A@{DSyZ4bzh7;8^Lh2ZFX!+7{Yw1%l*!dU zH($~%>)+76Eo#w&&Cui^3%h_Ox^9wQLpox z7EQdkw&s(Pa#cCY4r%n>5IRO95?8CNZG{TDCVboA>K9rwI1&Wq~n%>zCf z$*tf0*QCtn(Ls|b&C)u$Z82?h7=}VJwz)2C0hile`t#sB=j66{hu3ua-fjd3$ z?YRh>q%&*@{coo-9+H@pYsTAtM(9+L(r1QC#S8kcTjX7SWFER+EkBLxNA4u1B!%q2 zfVczqgtRQ=axCvNN6hRHIN+-Exz_jPdbfj%QvPkQ)KIZLrkP+lfp?k0MD}WV2^K9^ z@x)2JeRn!V_k4EZJ^t}Xm~&f!pQGQ4_6S#|rv{$i>`z9w>ib=)+2pNxnIMo zeX;djr$t$+S}oJI2U!g~JqFFt&H`|wBF{(a}Z znwcsb-}rL>Tlsr3$3Or2Qf(}>$a`9X`}uEj`VmcxDhGR1H16Hky1~I>=~`QJ!f?xH zyT(7)6oYvqO{Y!lW?+td6__)unb@KeQ$1U7y%F z?f%iiWu9*;{;tg_n8bK>>djOau7vB+Qzyo$d+zF3dx)`YN!A3%=_U>rdZx#IlvY2d z7SFc$9Pi;RiN`N?m>iB(Jz&mR_bO=JYIku?{*9q8yEdG;JU702yQV^0-twoo3*~Df50T&-I&9mSf`Lt)kattF%D( zhf^}+okQgSLN#_9bWC+_}NaXt@69cE|xb=SH#UyoOUOgIbKXymgh51Of9jK>cuuYIb^>eZ`L?yqmC@?NyAJt@q%rcK5n)A{~WOZ}gJnLaa|vo0?_ z^YNU))~7*h?`tReq^pJdd_wfr%-o1}m4zFP5zIWGpiO=O5m+xQO7t?pVbs z{^C1#-Mswyw}O2!x6aR(Klb&8S+Y#D(xi{OW|qGaJs<0L{+>sU!25f59)~n{<@ntw z_`1#GQ-FT*A(_K&_kWf8%XaT8@IB6;zU%1wjcmzg&U|YmtXL+_nVb?}cxhV0;W$3C z*r)mhzb4pz=FAjpR1y7r@sx5UpV9@3kTo-=-|7f>ca-(z0lN>3G1C?td;G<92WMX9 zLW2pvZZxx83tj2+!8dp2wD>l)+M9bHm$k^8X zas$H#uZDAbf{%qJnsD1$)Q9g~{jYspYtMygTwDeF@{+nk)OZ#NFE5#xfTZLZOVQjaOyAyp~+Zrx^xpgi2{=^&G=EtW^`1tkbkrSGwzXfK$ zx#4)Z?&9*QM^n=GZ#Z$9HSF27v{M}2hXa3B*nF^?cl@b|@smTB{@$*wH%Xr}Z`Shr zdd6J+av8}}VrRcz`tjPIuXpZm|GV3{{Kd)a3!gq;e){Ea%kqnnCt2Q1>SJkR>S&vw zcX-+LOb&_ihGp~0829bn&fof;|DTepO2lW@walOYTn_0y^YLo=B1fmFojFTeY$i-E z)H!=M{P^qb0a7RTJl`sJ!e&-k)O9^6PQ|K^pPKy+-8}y9)4y1DH$Cs~-zLs!iP~}N z=PtfCUkl-7I+Dzk59d1~DQt??rht*yI z&dLhsTA%ZiFYfx%si`{ue(h=9)I+-uv%Y=E*(PyhfwJGuoJZ5kZpti6VPjUZaC7?T z&@CJ$-niCL+J-Zn!@yJX(8>Al3h(}D^{@I|ZdsO*Be5&FedjjQ$%~uBZz(Q&eEI(i z2cvnde6e3Iay;K4^FD`(ZQW5ZU*lBm91~a>$IY#g%&}EP3$i_wFjG z_4@Od-3^M3zrORUS;U>(+xi(hY`5VdPVqaDr0|DldFx9a!wDx=Ej=KZzEQ0-5_JAp;OO2pyL zs}|RH%bM8TJMUaoJyqPXp{+6BV?qR%|MrImF7Jqp+>-bx`$ABJQmgGf1Fp8Nb63<= zue7Fo{y#4A6c=>vbat~)e_XHNGL7LS)5&Xc$g81uUoiz>S+v7PD@G!Ke; z$eplK%RQ>tR=Mj%t9+JFl-~7&Oz$3Vo!k&>s4|-&JM7|Q&V6&vUpBff+q_=x%Kf{& zIqMqpO6KK#oUJhNed>JRebb?>X4^1IX4?0V>N>VnQ?yZfCg_M&%Q?z^3v|N7YE62+e61gqO>6Vzq3 zmJ>VIuJUZJn*VE6#R1cMss1U^1=^F@>a7-?ERFoIvuNXaZsj?gZ@UzfG!;&kCf=5~ z##^YV;&3rebE4*|doGO)ew~Yi|2}20U6pvps{H-uruj!_drcPFS@V{Od+~>Z%NGPi zPW#g0^zhi(v(}eI|6Z7pIQiaQoit0?r+G=oH&p1nFFLx-wD$h{ex7EQ!!7x8-}?{T z|DDfQ|8IGI-B;;66Fk-6i>gzLl0_X)l+~j8{_4f$rL` zRz-=&R{B_~YBi;u3<_!EOP9?)aPEbVe9D6GcFSLC&v-J|+x+cVDs||Z1;=`}@62M% zF&U58qg6>chA4;QmG0s-Nva?{AF#sgpa3oUYmIBqkQG*%TcA8IenWguiY_| z)Oh&bs;%Hj(t?t`N7YTXMBVrppt!lMyWsVsyNtgdualm$@zLh|_ur1M`uXba;f3q} zf6K4iyp`{#+074T#j06HCo9-p7CdVhbVMUrfw3q16#ufr0UyrH{2{b9FzdWZ_M$}n zp4#vH`u4xY-`}gLuKE4!U;6cj@%wlF-q0iUV_`n~^|R;CUw?S^!;h=~ejesLvv+s4 z{^g=N$JyM@9S>Km*p#t4!z?w&%KZ1D_7@Gw-`*W*4#?>AxpV&5(Q{VWQ$McRr{mn2 z*3~jIY0IZyn?!fGY97w3i(S3alFxd1QgP{Xz_SNG%Y=g;d@Evq~Hx!d1gTPpdj*MCQjl)%HHkE+*iK^6|@n8`ECJeN*&g3g!OiYWp!OE6g-8><;U%7m_@sKeRq)9=^L^V)EmS zduE!dXe9Ga-(a-)yAtEsoT*|ZZf}o1WmA-9Ve2?#dG=DE)UDu$cXlqkzCv)b)UMU{ z=iI5e!S!6ye`T}Vqyy_3);92^rd^%MB7L#r$elSZS6mnqU%07%_v*Z#SGI2FPM**! ztI{4fCn+bs$m&sh5MTXdPP>M5;H>rcAN0z6Q{r}f%6e(-EJx4zpO!zLHTTBMyGHY) z!_!r72v2lX)mXQC>8$Oye#gl&K7PENQ!CDEg?P5qXTKyvA*HUhv)C(H0(CNYG-G#Y zf8^2h<#NnSo9tn>sx^b?dsIt2Y>$jTxqrT|FhZa_gB}(%H1~Kf1}#}!Zl7A+ysT`{%9S&VMGpT!VTl+sDQ>Sfp-|J^y+j_q@_vu{Y-LIG4 zJnQ2i!LaS=o^#9Z)v14)pB&4po3ee~xtLE^b6)MfZ`&*Xcj3Z^4W={PwzOC4?2`TV z?;Nk}+wXP>1#!#%$|Z;XcILM^IQ`I~Z(OW-3EJx(#Q$;3mEQ2gYKm`Xv6rp#rbS#_ zJko)OE?m@lk@EVsY_ZC*xfNkjKX?<;>%4F7k&HYqnZEp$Mw8XC)yz}Nj)z}VKFiAI zpef+id68R2zj7VNkEvHKDXo1VF+FF?nsr(HJ1$lJD0f`&Wbu~GywYZ@Ik6|pJP&2M zduEz0lH*nnF`L|ETHt23IyZyUFsAHXwL`F8j7f3qY45r6Hf6gN zJ)7aXYfkSu+ix6NM{T}UX;1ztfB){jDbZ65CT%~Y_*-yBr>MGg!y`lMK8|GP8%T_Lb{;ulL)$7w2&fAqGQ}(!a|6Ts<&DFQAPk-F7 zCph%{@nln;8JX*@UA|qd#-v=C6R&@~&RIt*=JiaMZ+raIFP$$fUpZr5m9>~8KjVe3 zqWYT?7V7?t^YLyq%3h}B6Eu4b@54mV`wn(l&1zBP`?V4GC=81cths*)%`rmh^ zu5n%|_S}T~Grs`ejZayN;@&oCIzBu!S7O%g@8ZvvS?uC-%V&zLP?DROkMN(plG- zb7oAlWL0~y$15mrqHjHec8s8#iJ|f238!-pbQSBSwqL#Hc%0{Eq@>M*CmDfj1MT%w zKHPO^FF&-)ILtS;DXviN(gC9v&lp^pqXX@YJyf2>yQeJQb84E$Q5F9F__e#^V%WW! z)=p_!%l%m@SM2c5nYCv6ah%7tm~mfyG2{I$#t)H=3+By9np#@Q!?(lYo~CR<>!K4o zTzEQIL%#g$NiVcK;ri`Ryp_&uYpxE_Vy)J^H%hBn4X1{wGiJbatO8CE*FS@bQa&O#>y?5{bZ0Z01^SOQXs%PnJw~GIM zdf8)M9%>wZeS7U?z3cCfe|*d@?qQTXLw2G79>4sDQ2GUH+7ZA6D8emXC5oO zSdhRlWuda%#E|W#D@~Q><*76+uC#EmJS1S(TzBN`t>x@nc7NM6=i#)JE=e;cM{f2W zVdJdTVS%FGg%r)7Z~V3C`OgPeDk7I}-p?M|a@pmm2Fo;0?V4`!j25<|rcPX3t8@)d z?OqeB_u2CNB;7^@S+;pkTH0;anMkskI{F&@(SB#FYrJgPs%PhwrQT$OH=Zs!UAWHQ z^RUT<35mji*oKGi=-Ti7Ko)al^!X-|su?B#$q5Y~(-c zr|p|1*v6N+V#=IDuD0h6H`hI=zqtN-!2_|*lmCs9raH3RTPzyRH|Ow0eO-k-WlhKZ zB|rDyUv%pB@7(9y{zjkNdlxnHXp3+an>h;h++7@Tvf{;8)nCOgco%+nA1hEB`?gW? z<-@$TJT#tw%$G-V9kGYfiF!K!4 z-1q!{Z}?bT`pNlWY45@1UT<38Y>iQF5~&T~`&@9ZY4eqXm9;lG3y$v$NoJn3Yag5M zuV)R9*Wc4?daQi(SAopA$3}Oq*BU?Yt*@)Em^*#??zGtBFDqY{-Fc=Y0P=SduL!PX3w1 zGv)pF%aZq^o38H;+>?LhK)!W8gTvc^&u|{nYU-&vWygrUsQU1 z(w~pMT(RoTclFdW#cN~(#IBqO`;?Ns;ecc8*NFv|4<&xB+kM;H_p5oq!x!QEW;n5T z*52EbQ?_rz{2NM>1ZP<}nXGF$5_G+zY}akUEh(mKdoq>IJ*q48TF9sJd84h8<|6l` zJ)KWvbiAE1^}1ec>FjjW|IxYjCCfU`)UPHc+;?`oTe;fc)C_&b4z<@eOVVE*?qb}> z-1zePZLWgA)JD?^A#2v4A_fQkr>gAR1Q$&@uyLv=;|^19vBwswZa36**0xO+)_=d* z!Pdgc^uzU-(=W~nwMbsg44a~&KkxL07use^!#{W|=;*7R=~2D8EUmXnD(l+D809Io zybIk}u1UQ4R&-glW=h=W_V;^&nEe^{`+lkUuHzPD?0^2DREyDx;i2|k!*#{)8^~14JK?~R&v{K`BQ^C@_cU%W(4i2-mPBq=jil(JI`JJyt$0a@b}J_ zClVXJz50B*ExBsB_e!^%MExhtix!Jrcq`jJC+R}DD~H71p6P!ZKOB2K?bVLk+uNt} ze_nUHwr1Cv>ObqMetpeUej9zgeb@YX=NjJptl0lrd*jZR#{}0Z6tS&Ixt5gexzY2| zn^In(lQN8&+qA5>`W{V;Ee+aq=;poqFBJ`+O$jf}IW+Cm64SYpznr_E+?2q3=gg9t zxoh|slG1!i?k>9}U9jUVY=N_>;M-54^dW{?ABGMZC0# zvte?&l2j?P@)E=B^FK=NJb!l9R%L@l$V{c|>iR=vALVo^!-Ebo<~zAu|HqTlGIfdd z9+{QGg1huj9X9!MSQDRb3&@_*+(~K}Nq;ac^4{e`~jzgTmz- z?ZOXT^JcyAel36Y#I)k22ZHxxd%iuzXUn0dctBJ3-a%KNC3!afi*IXNZxLKE!|XMC z{Q|X@n^Nab+O+Ysc1u2w!|b~PleJWwZ{K@!Co9@^zTN!$wtpYl|FQqC|9|`c>-+zI z|9@G({@=47AMS3c`Tp?o>m7TV7GJqnQ*^{4|FX-8HNuW>Vo%?@`?Ml0tiaS-MbPnD zm!@2?bD+>+!_*^kX?m{C=D7#h6kjb+b~~}8dHU(~(v!urUDzW3O#1G^b~Hq9rAGVY zX(EB*FBv#)7y7yHo_qPRMeeSxZ~1@!KFZqg>*2rG+w&!ird`gn{qei71U;gxM`0sb?{{DIR^o!N5h_R109zK}~Za+9>KikJPsCF=PNeqrbAh3&)VJ<$t@Z;30e6jhq1f+=g)p2E4%k$bD?g;wB?@D*bivz zI=4FdgZ_hy#vdk>);@~fFZl1;+N(--@ij;D9jyX7!V+1vc21=ua2+s{UJZ>-+KDyKQ}+P2|+cI@r4V`0v{4 z`!@9!|G$OX>n#4a^qP^`>bTNo;pSosUa3=?CQWX1t-r>2!t{*J!yWrSAL4!)D5q!I zu>Og#itPd6KHIaqGizSf-mp*q_JZq2>5|2>?-qVNd3xElzZ=y38!gjVOIWH5BMcZ5 z_;^Zsir?;9>(7%Jvo80=M6QU+O`rD2^nX_iYdDs7tZ;)W!`rHfQj&XGuFu)MOZxgO z#}^y){e+$~UhpgpG`b^LAmDOq-QH*4Ol_E_%ob={QBq=qMH%NF=q)*u!Tg+2^6~G4 z$$g8$cLfPDaoRpDs%+jF(PS7b5a%Q8d$X7Qw%Z5$A2!z8uKwE;#-(}Td#R^|yFl6$ z9{b&i7PG7m$~CLS#BW`zQKZ&_I7wT?d1 zmEuV9_TbpM$US5N|I=mWmfO7gYz19wUcR57pV@igL;lqb&z6^PEv>)ygF7{D`wHzY z4*O|;Zd_wp^|8;rn*Z?M`G2qO|G)qL>iqwYHpu>6IdPjTU%Q<0`RPk-W!`_6y*%f1 z`SQ#5d-nYK_WS(*r{(wm*6;uQw*KGO^=0CD{)v7p;wB+||!ADY1Q_@QXjoZ_JA3 z-Z;JN-i*h_JjUOQrxm{{sBd^su`6cY`IM}~3-4Dwv-|w`@88SIm#>WEbKX90761PN zrhiYZ(;hu-HD~I1=#XA`lC4i>#-36i|FqX^LD%`@W_TvY3w$m3a7j$aZ~EJxC2q0y zKYyBEmDP&hZ+Gwiuj~8OTUWeiP-K(aGk@W-+J9fHzN}T6{3OgyA~WOn@4t!WGra$N zlds#c$I56WlX-d5+Qa=fcK!PO^o6>fz>5Q(k||T#T$0`^FL=Sf?~w~{vv$FgqJY~? zYo~wayv^M0^V)O0Tp#-j;Th|fAI*-+t=<_kYh60qEXHmg!H`}1<}GXA?YukMBUSHm z-xsk*A>Y=i)z7M~ww?R;SW4*G=__BeshvH(dtqtVWgFZ1KFb1A?#?}T+bF)jJ?+Kx z>8HOIMZJ6PBfh;f!G-y7;@qDN{g=%*=v(j#PbjTBq<4>j|3}*0MW0r#6pdUc&XYIu zF7Hl>TuY^`F2*Nc`PeKDVm{I(H9c9sw0W(>EUR003of3O5Y2X+Ingb?S@;u!iCeL_ z>FQ@+Yi>U4oOIzzcw(QK)X~RROm04~Nja!_JZ&K}-&?8bi`N?UU$}YC-k`~$zQSq! zwsvbyLBFRn>J`p?x|{dnRc(EG_`M$&=ahMG&+EB;gmwAP<^{QDKMI~S^nNjt%Y26+*<%@ST<~{#c8G8Jy5Nqp(o32*RTW)bFZBCX^a`yfIyXSAK-&PlQ?)mkZmCMaP zOjP!JT;V3M?B}_+({?BR-~9VQ?|{zrHH&*k>D;o*nZ z`=9=N`T2D23V~;m$4xncb><#^SG2$Vgo^Mke#Q2C4;Wl6e~E6c)O23F&gbu`ezrr~ zq@3Az6$ffXr-;6`nV$IA{ov0F7JSlmPg(ene+p=tnG&WZmMPP8aHscX0%oaw#Qa~7O`axym2y6z@t^(bousnRY9?ZvkR>zYBjyI zU11vXxZ=^9_T|?z@4oB*D_Ej%B~YrT=k)dW_wLBHJ^%dk-N%LY-~Q>Gv4}qxT>RpA z;odoW{F1)6s@|`R6goWZ;i>gE-hXoWDy0A6!BVs9CKBEUMJCsZKc90nK9PA=>~8CR z<%%ocnI+ZT6!l*_9G7KooBsaywxts+SMFamg-4c|A=pJ-RN?#*SFu%c)`u5l@66Sk zVWFh=Wb3+%xu@&=Z)qo4OkI_=V5e}4=FPL4PkY#%TKjFc&}EZq@uJt|dk^SqC}^)U zJttiI(%kPr>yp+5ZU4^2f0^-#W5u5trF-QYBx)E;82Y}d9rNT(*K&>jCESv9{*hVJ z)I}R#?ugqdXtgNq*%7{~=$(8TZi@q#y-?ZBv}Jnvf^E+0Gwz+-8C?A#{(bk=nDC8G zk>Y2=_OQPC@c!r;pHrLXJZRkex+rnY8L1l?dYo_5ljr)zZ8f@U=(1QdXr7Lr=Cq8v z-%elKa`p8giOH6())u#>zE^v()L~6|@XTz*p8ZY&{3pxz#`Il2)zKqjzJOmweb3iJ z3qp2$V7^{1>-9!0FkoZb6q#2BXMA60fBrY|f#cWi&*CzQ8R>bec0KemnkmGz?~FxE zG20r;{<|v@V*O^WbYbT+)-vyC$lP?8``V_%`=@@=ie7Y-+4aZbwR2K`7s?;24DXXa ze@wAr;e$(TUyp>C8LxHNc>QVR_WzgL*L`o4P7(cl`epm|@Jl=7EoTUkXJ?xp71)U2#e`J1HJ^KgdoZ|h6Pc=gwA*(C)(nX-uf3DK< zDuuy-%n(ROUQ}hN++h0b$HC{%CraMmZvXey zJcU`$!)>;&`~B1E`qkUJAO172**iym*Wua6t&>{VE(5EpMCXq(}0y?*yt z_N3{3Dj~^=y3fkaNTtV`-DvcLV;;EN`P|0HFbnS_%Su3In6!v#MUjIs|Ag6C> zfs@*bI`2!fP3K(u{YC6+T9U}hJ74ETGCOmKq-U%NEZotQ!N<31hSnn4z1QbDFfP_U zwQ<@mkrQWf1v`(;S#I;;No|-$&8IZ39ZT2M|M0vP@{yr!RnwtM6O>;t%~`7HAYQRp z=G@AMrUo|@HXgQ1)wQrzS|T#x!iR&pTJpTU#^G@dSr2B+Z2PYNg7|teEjjUfUFG)~ zPj8rLTk9$Bc$WL&Xp7gH^PLxXBxUOweKc09A8E6lHQ74#qD_%X@}7^5ol{axX1ndr z-G28{?#tO{`S!nYnYhR6VD9h5Tu+Uqe=Dvyn_s(9F8smurB`t#{y>zpqoYUNRXUVacq@b*x~BBznp7w zlIJ-&eYDEGT)Nx(UHiJ7WiQ$~(XKXtPl=%OOvV1ts?X!7?t%t%q zm%Dv0Sw8E%zp7j5Y&%PA#Vs322Mbj@&w3ZJC?><*Step<5`;28wZ0O2!MK0*brTJz zhkeBhg!vpY5@d{)b!n>XO?H3Joit_FV}^<)M_w#fjj5`r!riAA1z$Maqy11f#YSBp2zyL6;2r(`VywTaBkZqziq5CU#Ewi z5kB01f2rZqtqaTNJoz-`0iS!6&*Sc-R;IH@Ki<55JdI&-cC+{5WV6pMBL3%?8sh&< zo1VqUW-Qh*or7z6gwWg_J68RBdT@4J=RF4YrPG#pmnBT-DWLy-<-swcGZ6iy|m=C;qqJm zzih1TxW@IOF)#TOv(`NS<)>|WFaJE;E?@Wk;o-}tKfgTm@>O}AL*v@RqQI9slQ|5Ip6^ zovS>vuk7Noe_1)Ss_G72v@B-*|FQpp2#YIY&V|k;J5ry0 zeYWV=oJYd*mZ@*?DyzKhS^Rc=*#TYiBMhzHN^6=j&m9QzKf^D@R@t`on2%c0Jv$D= zLpNX7b)1=PBk5!%G5d?C6Yq^9`?f4wZfYy`L}%{DOUnx7R&j1HdcJjC5m&OOg*%J~WA|sXhua^C z5C7||JY{Nwxy9tSdw#A{j%xKk|L0;Fv)u}@zby`$Nz>J~m1+AvU-tXgy8ZXVmaWsv z{&?zcQJwb9ZTH?dwO{r*cl5!=d4B8X&$p}n^XKR1&zDcX{90nix$1Rl)UnU!;y+0r z_h0_}z2E%0?Q^E@&ieE2>(|BnU;U?NSg#V$+;hjp>B;vSp0bW&3zW@_Z@H}Ki8!;4 zC-pk}p0br9=B646DLSkf|HU5e`r&J}CyYbQD5^{$X77I#1W zQ15@c?e~}6&tKd#{OexJy5sA=HKA3vZuZq?SMc35H0Whn&0Af>>vS$+m*4e5>%+fi zmw(USZ*}14a{kB9{aGIFxj*??%b`6h?1i$Ep7i-s- zY}+yARH^5w zhmR-DbjgU4xY}TRW6eSt;fGPzXV!%CED3+x8hNB(g@z7;ZjHi>vd9_r4SW7QP`1>! zj@xAX%ahCHmQi0)@$=Im-;YJ-KX1{__7q{YFg>|~x5Dys&)$R!pTA7%IH%y>yMTM| zjJZbyG+3f9aQnV+o-S;2z*gqe?FK`WAJ6%DbpDlP-kI1l_v~)5$=MrOm(6{?`6e&Z z;f5NCz9V6diSsAUsBv^tT42(UA1=Bitiht^^CtDPiLc&#a_FAdW7%%-Qd@uB5x$bt znDA$@{-?!0@gK_C42wnNi226_zi*#I@YF)gtX>6Z1b|-P`s2 z(VJ&2FnzJffW7y`oKD5{GaA-C|fB-zR&yC>pTu$$2#fK+_vq9-!<$NV2iDu z94+!(XHiYP?k$eu?FYO3Dr;{(d9rP)tfQ^Z0{I-P8H(cfWqWCpeJf(B_V5 z6FC&+KXligwy6Gm{|zg@$#ed|jA^^n)>qY4)>c*h{POG1pFek(pMMg)e!hMEuZO$+ z?f2-M|NO4$``OR-bya`+3Z_4w{rAU@!}4n~T69uhuaC5S;Bsfe<{o2@_3YNEi;H9e zvNzj(Ioi??0@zb6ec~9Nos-V=c&FS0j;wEmD5}gF{=U zUv6m1(4T%NqphMd<>Ks zsQKTU@%dM>@$OsQ0j84X8#djRuUggkdXACu>mG>rh^M#E?#L{*Ys(@ z%EBwl<5|nja!$V_xMo)MW(gjPrXBB?>k{(LreAS#S4+N;^Kp4bEVK5kmFx6lrfvH% z<(R=4)g5c?T4(vro4aOGOVv`YVvdHT(_NT;vAl0dQ0`mVde}a7O|sM4 zG&Tgx`R{hn#cbx9c>52%0!g2jIj-H1Em(ZL;&c4&*|IyH_*#fxadlNV_WoRk=W@3X zojDpuvSkb=v;C@$iR)U@eZ|FF@85wgu03z+zBj(T|AycBz8nvedh!#AA6=X(Yv*cz@tl>YUbk@fh5gq!&MH~w z=Y9I;Z|*;3@`+;(cfQ!)+}?h-)TqMH>&>KsXR#G3XJUHprv(LxPu|3q>@|f)E7&Pu zM$7k^DrGTo6VIg_f4@xU@sHQ%r}IBii{v`M`ZqkU`nba<|M+8jem@a9vcanJyGB8s z)1fz=f7qIjFU^oS8}Xm}*v^AH5+8JQX(pU~;%C$%KCd@#uTrkXg4mi3FU$U3&{ugn z{p~)fbK=K#JbSr4cn_!2vanXOy{9()St9>_UlVWN6{h1Q2TfXdos(DZ(A<0cp~i=2 zHJc7)S?NBXT&(?xVM~X7jNXxjKNWhQf9{hsnV;h&dide#L91@r$Av3a^C>Pxrc zy54izA5SQrSaJQ5SMTd%Kf0ei>G(G%)QTa&E7gH{hR*ewob1OW+QVOEhCgTO&J!6b}u<06n}2v0y!6-2~+>xo*@5RwMiMwrR6wm~gh9dpC!vN=A_3O=Y|A0_#1$Pkso`HI;3%-<|ArxpCFo z!vAl7|DMlt_U_*go7XXZ`0KV*bB$cvr;ko+9vKEC`ic-~Q``i)}xCF;^gdG;LLc`A$Z+`-Hl<;RvkTC=4tr?t>UtKE1MSb|E^|#_R#7L|2pAcn~jWz~ATLQ3J-m6)#ZWsx5yWHW78J!!=x zlg+LkYnE(Nce0Kz7yMJ`uY7#{UO_d#T;XFEjvC(FsO55&=gO*8CLw|`4Vo2a7bJYB zRPSwW{xZoU<-U00x~vlO+@+~+**m|8aV(NOqVgzats(E?Ij_DvR{ks7^2lMO&U2Fm z(T^K^1pe!vo|}=PYMPz9Y3+>Y)tt>W$2<=iEj(2qH0w)4k;z7 zl5Gugb#i4=R6ZcSda}E}VP=BPcg}?LWc7d()-k7dXf|b?n!_ej+V!9%?m|FB=`@a> zTloxGaq@ktHax~*GUynYBxURrxaOuJ&X_qNsN z58kS=lH6Gu(-W1n`t{xo48BsUL!L|vQP1CEKeuh?LNSBaOLz(=PJi&B=+GpN)WeP& z+c@V+Mr@Z|{WqZeckTbPcR$uTd)3&{Xco_tZ#qM%eSw8zw38e zbHVlU_G6B0w#H8jj$97=#>SEq{QKu7SCy8c=W3hYZPYFOUw6OccF45w!tyxDptaoz zHxE8(64!s+n6=aN(wQsIbAMl+`gli7+V?^=1M}^|IonF7HiGxomlbL$9g* z>)CsfoE+NK8lJ0E=X@LTfO<%rI+caW(jpHdStP- zZqw=gyP4XH#oli}pZZJ8c(JKX^39)*Ref^Y+%%7^zFQKhw|r%u%;}FEHH#KwCy_ZR|8phO1eeg;N?B7?lnvvgrk{%jvAi zU9fM0$)ZcMw@&m3V6dEUtkO3Fbm&Rbagj=L8A zO=rWgbc@&fc{Ent;N5v>lic$OOFw6)tvUVX;>C$s5~9x*I%X>t?p2fXl!)PfApfQ~ z@4?h&KUrt>$6XuQN{pmFJapyD$hHfazVGr*11`Zrc{=dWK8C zE0}YYwb4Jk8S!Y+iqJY5Bq4%jS#!O`bKaV`J8N*Hh8=q*f=U zJy!M-^XZ;*Q{$}t`e?-)uio=n%n7ShVYCy;Vg2z$Nna{4y6WaSm*?m7ne+O1I=V_q zqklg&`n4tdS4dng`wOMMy0tUs#h=^%>&vIRmoL}XR{nigQdcEgKjRvA;p6rCI*zAP z`~6Gr{{Hu??uPvDU(4ISsUN=kdwYBQ9n-6~Ta)6R$-72335qX)(0(Z_Z7mB{#w_GI9_O!10QNJnrPbAyT5;P3@F3kzz8SFWke}Z|+gEie8dTxxV=21nSxn}KLvN!l-*RV=8dHbt*EfCn~!8Nxv zdHT`iPktVS*@T!Q;I@tcY^jOLZIc7+PweJMNf zX@FAb_2^hzF3Ect)2=DxbO&iFMc-{ZXR_vX_%q>>mD3g~%%0`jH}Pa?#R{DpdiRUd zHaNbYAHQvX_1+qbx%Hnve$BV*y>IKg|5we=zv%|wt=@jO|7dyY^upEsBd^2VL6Lu25b_S{?jS9vo$pGKqk+!ed~X-8)U3bWd45s z%D=YRmL;WAO|GSe?a@(X$!!due$P|fz&Kg0HQZ3ixb~Gdi2-4-1q*x=iy9`|6F7b5Z_h8;A_=+DCOsA&aM?pr>_XS6KcvF zmvzGIn(*mqocGx;xbJt{_+}5^G91-F}QJ{FMVy=`z=9RL_R&+ zeJt>Qyw=O)gd{7i&@vy3Ki7KQ&9mP9e)%`_N_xDb^I2Z4h#6MqB`4gte{SyH61r>4 z&Wk$lj~-6!sQv#nb+V1ur`*;n{Y){ZxSS%>=3B9J_0RXO|NHRE?CCELKYyLC_xSYFPj}l- zw{QFX?cKze#~&~Jxk{@3b7hqvBd_aBr=z!Yxvis`{wszbzP7v2E~x5-9RI&9Hs9`6 z21ZB}<-!%4+Uw=zq+%8O66)@NC3`76k_8kWD%aV5Om>8R}Vnav&Y%e~I ztsk@cl{FG(J%71Vs`gv9Sl1rY(`RaCTBzh~;r(n=nzv;EUq}>>N}=DYt#7Ywsjgo4 z?9Noz2h6cQZ%NFW+{(~=>r#?T7Gq#%%#G-0E0?QFiN0G}w=jRd6yvqaj8{^ec5Dxc zb&L;ss!`gKP`1I$waTgHOq9%%!|PbRUL6cxwp}qVxPs%(^y!;Y)KwH-^~^gT{ax8Au#T;bd%B+dH1LX@Ay(=~LD?y)Abe>`uh4~vO? zt*hH*RPDL{<>ANxeld@Os-gS;-~RTmVnblg^DyrCyr3UNuXSW5FKc|TdNOCl-LIk5 zH>WXo{yF&7W%}k8>(qvbi#cC(PF`3h*_W&8S2$14D}0`3DgUR*Jm#*8@*x{3>rojTP6Odcr@y*u1e?KWJjiuVz`#KhqeB)-8DI-VMz*%8@@WdVl;j{`Ifz z?Rxjy_RHJZ{{MIQdB6VW)0uz%rvCL=*Rq|(=t}$auMf@ECD&i%YS?(L-`HjL(!KY# z*3C88*LA8^$g;GmVxq6IadSlEKE*FT3%&|iehoXwK1=YjUQ+Xv$(a`~b{dA??9Gx6 z3~%X5T+E&I{Ib~}?g?`?_E_E06WuzgWMh2R#qOm-dh-rOTDjS^mY=_(V<{D+QZ%8P zzqsk7b8}>Z!@5{@-`{r*yN3rq8<;v=xZ-k-}J%YZi^oVRInt62MMac!*OTNwB zT>64r|FxC>zqSIU$Q_1vFB}O_-ZiIY=COBs7!Fo$5PjF>xq@40)@^Hv^=I?We_=}7 z`>ZDR<RFfW{zdX1r%(Uk?#~rw z_I-us;h_J{&cW>QeH-R{vzj1!XrtiLC-(}R72ilYW(YNO`TsaIWzU2rpSHW3QaI1Q zJU@TYf;7Jc{j$< zEo=S7S_&86n*Ht%XJT;K`&eV!^N&PBCo1uaE$LqU{l^LMSDWA8@Hw^V;cWFO-Tv%e z@7V=ogg3LT5q4Rh$;oa0c*j)!s9ME;PbHg8>h7A}Sg`WgQ>#zuOByThH8t9Pu|9I! zy7I!NSF={0-1>Rr#3i{dTP3CPyRxplHV6;*lK8Z2>s^k(ulw^QSJ>DtI@%D$zxHdD zp{%38-t&Ro#vfk3(omk_=VX*z6Sg}wK-tz(I$5}{;kc#Hg0!g2jJr&KLoZj_=bgTA z)%o|;h4&7ZI)#0F>=7Y$HT908^t=a`U6cMaeRo*Us&|bsUot;qwb=p|Q>nz?FWU=u zS12g9ToPZL!I!x-Wa&)Red!4t9-Cvgdmer$b~|KUYNKhO~d2`=5I^Eor}_T9q$`|CgdC@@dhc>mw^`|oe1{!jb=@$&1J&E_?? zJk53*<;?U@d}g;{(~G??X3Acf=YE+x^>(S$^}h=bzZ7IhPSpLAbiev?&BJeBs=je` z9E+-%5VB0o!{Yp6#i-wJCt8KiTjM*g|5~}7X!%{Yp0`sj>2sbA)=QP(w~{GX{%76- zE}jLk+!l%wU7|Oygnl{m|LnC@zIAJa3kvKG)~!q87D#lP%yXFG;LY`Eeg)$8B2o`z z4(y20JDljx7`N>{{}f}#3#a-{iWW1yx?tnH%;vz7%THBiY8GnF+0zxv(EOso{-pHA z!nO~wJN|B(y0>GU_(ee(MW)02-xQvGIJN2Stm*tRC*}8DJ1%uW>uNyp$_P6Z*#nz6 zV~TPTZr*w`TP`vB#-}pAug;>M=imLh<;}Ctt1m1o-7Nj8E29ZMIIl z)MvR##no!_!f#70ryLdJ6xn1SRO~JNWPj3p7UjEw4tjlSA6wip|5~?Tdcedr`#S@_ za!-1EOQGjk+kvxsC!QX4I`K)+)|&sTAK#y(?Z2M*)n08EQ&U-#AF9CvVJs$sKZ zL=pF&aC5hlZfl$ybS~%YlU{%K!ued&2B{TCChy`{>%BcP&2%F})Tc@LPr4msD%3vx zDhjQg8=PU_?;@nWo=IEgfK~KGpR%d(myc!tco1@Gi)w8$qy5EnF3Yv{yWNYkZnf}c zP5C*gFV9Q)&Pi*Z8TGZ2KQD^bwJltbdRFrLuJyaOuHN{c6wYm(<<~-v7A9{i~ZRvJEupVF_@A5ZLR%E=X(o}{qb_Q@!yzuE0nn}MD=gl z6-9x^kvA{!thHIYxasM)N0(-uet34n`(L%kEm~er$W-+B_R>83)!Ft0Z~a82*Dn^8 z32>GN>&dm>mTkE2mgSeb~8w{qi*04(C9@iH)_$F>^DAXh`>#Kz#o7N@{#)6`(%}BoCE$LV*7a|kp)sN7i;E;# zjl33exhu}K>s#39#B}u2OU)xk|C~e=FZ1d*U%6mI+yozDlRIs`}sMV`5 z^8Jm3B}EJ>h5M#OOLy@rvsI>MG=+0$Pg@-8Z0Xr~J@Uj$fv~)nEp@KbGD7xhOScFu zEn4bxfw#eJ>n+tPjuMfTmYa-xE;H?Y-C>>+c-6At`W;#KGewh9GTW_RADmaNA)!Bg z`t<9kU(EIT)|ZnV`NMnuz8fb`PiiaU6OX%p|L5gOAD2%%^zK>)Ef6q{5}(@WY|ANY zdhhU?B3q7}f=@ehYHvGFSv9BgUEr$4%dX_#U1{=IwUmWjaNdX62?1w6Za-^f`oG{< z!7bxef**E1UG~KEQT``Zv*v>V23HziEc^7^?%yKu$1TD^GrIX-AC!vo*GXPJWy#dJ zs;8SitlF{B)zUq2CWrRa?{f}L7W=i)Y29PNCoa2UJkQB~*1VM@J~g4y{8+<=zZc~C ze(yfMyY1)w3*XxNRF?DzeOtY@*ZOd{zJXPzaNNVDWxA;C!c9rU#H*wZriHY0dlps1-cbxi*GZ^$#UN-#)x@H37BjazJbd<&2NtS z)O{;NOh28|keq(ZcU#-e*=H&=kC}Z`YE77vG9ir9m1C~Z{fSpyOylA*Zd$6s#57IgueaZjo;mmusIxl9Qm>-k(4 z?V7#lOx~j>uBJ<+t^K>WxBZe4neUr;;8tP-Q{~@7qMnIQ^Gk2|JiO^FxMI;4nWsCR zEtX#`Q2exwlShHQBH)Zo_k<76gC6K8t$$rnXZ`DMRKBB9lJ~n7@n@oTsRA`;uGUxV zRBC%Qwf3ujxI}T^*A;QE7F6G}Jk71gb;8Zn=xw#qs(|Od$s99V+v=lwC;$Ae|GQ8* zjx~C%wxGYeyp_Feu$<9Nfh}t|U+j0ATzVid`eBz{NNJf@?xxz<8TvbGnxgk{}x#C_2G~}#UoSB!bv$@m!Xvd#>&BI9 zlKq|et=&rm16JyuRZBf7c2~e5aH7%^4e32QEhBz?Vo6jJP~ZoPdA4g)z`yQWu?v|)+9TV&2X*%>|(8mSGJdO{egoi2RzUwAY1myln8 zY39QHVlDekSI6fpsb!vWv)lVd!C8%vc=hQwWcqd5kN1@21$_Da@Y9BG33H7l6-3_! zx#hnMEL`-m^~$YmQ9GuD8(Y2O{p;R`nHnT6cD`s3wsG5c?J8}9zqjVeD5fY>{{Cfp zVs)41H;MVD`p$l2Q4jw4$?ZT?+>$7b1rnj*TsCXBv#PAn5zVTUTCgx+(!vLw`m-+; zUy3w%8f6lk+FHOk;Z2*5`^mE*r!VvV`8C;Iey)2&U)|~}d24!Iin_B*@^q)0B^SR`d+B()O<;Y=iY43cOg^!( zch+>vshr=<63o&=wcKmB2P_d#yO~`h#xC~yp~bWwr@X=k%9bUjUcJv-EdE>7R&L2& zvmi>cx^~;-4guZU?q*p{c>+GgTl2MBN>AlStyJc=4R}l@=B@ z#tu{CvYz>Ud9i2Vr)t$p?AKx+a_)_OKl9H8BW>>wN5p2ON`$30*v^`?LT~Lu#rMi= zXD`V{FJ85xQET6i>k(J_*DD2@JUn(`g;{snjWw*%PZee#`f=&ighuAoQ~GDCzesB> zZB*Ux#I;C5YR}0#4`-}uaB;3?7fMyKou%pLwB%JxQ1jy-Q@5lZl$-PBV83ipkixVW z4I#l5t~E{P=i2Lw#NOUxa@A*3=l?$!x2|D1q!+(-L#>pC%$%Plo~_R;W=I-cxqkS9 z(AC}@chZk`@Tq@#(spGBL&kH}_&WzrD_wP)6ddn2{d7u_(|U$oT<>2@&3wIUp4g@} zHQnP$I-K~)U8jXc_**Ka`^g3iO&v`hx;dV>X!u%!1!Uk8P^+dSt z?p)W_5fX8&sU)ZFkhziCKTED1e1{v-Z4_;&PR_ zaZcbu$-OMKO{e`lvdqjfmQ>zYY!sS$@1=dA9(QHoxkRIm2I&)$A@z42&Fq*Wp0#km4?fXq4fdXL`Pba0w&bAs6ZB`V(|=mD#A!pxi(eWuoh;QSU)kW_P`PBbTJz@0 zbC0VUugIH=u$molTzfcC>$h96msj+?n~QuN&QM!wcjuFzuV2l=n%n(mIlPi5Hq8$c zJ2|7_b*xyz_L}GyOCw&j_Z0L*oLLYm$oxdI=IFy4Pqf~@5Xrb8AF?;?@-jPJ@0_O( zjx{&knENp1T12LJwdm1ZhxPsk{0ln4*E4n7zlY(8mwNrW6e^AQ1cd7z1Yg{LW4p#_ z#w(l4G!GY@k)AunN{mf)5rf?gO}!O2|9*%_xYE<0V6)2Psmi?Ap!a941ReC8#u3a` zD)Bh@;?3pq|8KIlys(cd-Fq(Z>QiP(-96%Zd1o}kyL^sJh~T}f;T8EL_R*i|Q3kon zcQ2nUtKXv*AYz^Pe9kUYlTfD4l&QVb!ZtH&drUMpJsEi7P-B^DUB-jDKim!Ze;y`v z7|uw0nV7SDRW`rfvBy>4`nFwt{e#2Bcb}{Wyi{Vz1Tl^~ALHw_Lj_lC*<#gtrU-wTloD@gUt{Bt#kfvlzO7vv#k60ulx^*9m5I{ zj=tTA(2@yd6%F;A={K5wR-L*t}cdiRoPOYN>|q_A}SI%xOs)A6>}oNxIzS}c~z{+}EFyp&sZ^-K|A&Vxx^ zK8qF>2pOh3O>q_xD$H_h_cJhvX% zeHn{84y*0F-6*zYs*$C<*Ehx@Q?|sABat)G7cbj*$nVR9wvaM&>zilQbv8^tU7K>y z)pon^OPPg6{RZNup?wxky)%y{>B1^>&P=ilqUTI90JNncH$ z?}7(5Z{wAX(Y*RGYQ~~125+M`m@dCK2ud+kEPbkw&r*) z{WB`U(FL$* zxvzVo+vYu)#Zw}xZ6&PuV~M2XzU)USKR->8oYWtFICYPYXY$$z)%MCa@h*iGT`{%+C8s|6 zpIfO~Y!LI|nc5SUDaJ(w$HH>HEock5aO}_BjZMaiCy!N{-CCAE>$v!-Zm)AHrsYWr zXJ(x|keSC<+O+Uc?#UO6@A)3PIPZJA*Ysem81JKB8>H`KD?awypQt#;`%t0J*QNPW zl%u&G?B{xTzE{p$WT!Xxs)b!ypZjw9ET7)&ZEP&#RIDko;Cp*-_rLGQ7vBH=;U&)% zQ+8JM?9Hk{GXq3YU%72!u)cLllRfRov8ncRJIy5SNJ#9dR+L%9bv??#dkPQpkI7Bk zr-Bx{^BvwQeb;bxVBoX0t_G@;g_e6PKP=SBk^Qu1_2m;gTEq78u>O?)8W3`lZ*9>N zSKTSg53^1@crEC;_v9se11`0%cyuJb^yepS{}6`gN_B%{n?Fa)=$}z=ny)U&SXyJj z^X9eJ1nrw2Jc)ZwOx+!{d(ce^S&PLhS$>hCOVq- zikhaeOD4E~4w$+3Yua@lyR9=%K7MBOWTmn^Th`_Y4@<)*o3HhgS!<1{NNdesvB z^bPvy`Q4j4Qd_0f0~R_-?9MfCw}8ObY_L zT)2u;x9o~lZd;^xYTA0{j2N!7UUSp}n`IYYpZ4uHXqQ*mslKZ2YnJJcF8sQ2=Apra z$&Y*wC;F$|s!(6qFUV=j!*c!J>XaQj0@)tTU{mU3Zok)fs?yc!>Z6$KE^5eVptlN*Ae;Vxy3hlUDVYN7X+7t&@kBHSDCobFfz((Wk zIfFNQgI7t{6toHj&J8?kx2CXIT2|4;rsdO{^?sRINm70phUd9sm01+>+AFK-s-6YR z-PC_v`TwRJY70cKx*GN8Xez!tJ@KKC=*9I1wY)bi^Gnf~!SUQ8W`n5m;XSL8_fOEc zJAJN+-li80pI@Bav%-J%k7P5Kg|il_*$PzbNac)Kv+2j$Rh(}Pns(VK_VXHA`U$7s zT6Z#kO(XLZY5vsB%eLEAYp!2!^jr{shwpgmY=2^hR!#AIOwK;KF?9)>Dh~-_S3)ZaP6uHQ! z(D-iS7cRr)l{)tGU3n#9KTNoDD6{*HUe>H>(^${xO*c-z+)wrP{c53F5$BTMY zSrX4#Gg!1Yvz*zu`g4E8a+_Z*G4nPbvk&=Rt=|24%kM9LSKDrHQQbr^SGNA2%o)U|E1!ORie1N(G=%}Q`Riys`O4g@%+%-d2T5Wq~=`r{1m}Z zzbNMV0;6kBo~YzKn|AE&dUS!G|J!8a=b#%_uZzZQbXmZhWm#@xs@%3+vlsOp5ZCgF7dp8_oDr91U*+m^;m|4{P$szbiWirN{g zwx3U3a$R@A^aBSbDpn~<$XMM;=`=TO(pmB~+BfXQ!)exL-4|QKo`2OkGLJ*#^j7n2 z8`xibJ{kG=?3c@JVGnsqx7My)tfnXza{SdRJ~<(tBSPxDjx+lfYFx03-!_}cW2@sH ziI%LKx=pOkQ%!##lWSSC{He+cbqmWwezD6~cCnw+HPMh=vv~QI?X%>BBvn?%E6yo9 zp;o@g{qdf4i4qtji=A_Q|Z8VuM<)&u(%1EsnJnkQF zW==@@Q_QNdcd?A=>#wVO%H_h=em@rRV&^Bf_dnPe`Sc{_6!p(9VK;iQ@3H)K8H;uO zxwR|)*3PcoY8|tBd-T@hJ7RrTnfe5O{_{rMP49EwJigBsbIxB)ReAmYRn5!z_|=!Q zZReG*U;e5lb9v#64cOJTxT#}XJ82L1H=5OR$#ZH@F_&Nc2T&EnhA zlxK+Qu3T`b#o^S^?LjOLc5oimj8%}EwdVdI{)}z|C+-i)ELs|`LnC*vNJ=uDZKzPr zZ#QNM5_hT!d-AS9sTFU@vfVEE!auq`Po-5M7Ei%*{~#0C7FZO`&L9ou$biMN%Ox1 zhw1&aDa*ab%Q{VP?iGu}hXXt(ERb+p{?s6+!!G8~^_R_E7M?L3hd!{ZZL!@|{bqU0 zu7KM!MGYF+=bA41c;k)gfrFm6y$auM6q4V-#`<l1u&=ztsxe z#@p?``~R73>)1>E&OO+Yw7scdqYR^)M~KML&znVim0suQ3m8so^1aT2|E@!tF%PKnn`uLrB^lcD~BhS z{w77AbbqrbajM9Kk4&XG7jC?s)RFo^q-E>xC?*e0KmGTLCr{dDzZFYcreijH)tXf& zFYmscTz^~d`}4wTyB`4?)@h%+d{5=#QI(YB+x%Y-E1o=6JIVa-jxWrgd@`;XZSvP{Yr1;cDw)@F5&{l)Jv7ewV#o61$s?|*3@Vizu1ygHKw22`^4^dz)S6w$SM4@vP%YkCVeEFb28g$S=F>yxOyOR%sOf zURs4kPy6Kc4CkgB9=_wZGu-Xr#l45bSN&YDoVTJ& zTJVGDCX>F}iTS5KS!w1?Y|4C7W8l0kx`W}_=Sg#3?pNYUNx1)e)x#$>Qnx>sSd}fm zEb#oe#H5ZOqhBnx^MA6he$e;bqio-89+T^nV;kRjg>4VxY&KN#7WB@4SLo3Bd8ew( zTJ2JnX1&yAse?H~8=HJ3g1#XA;>#Z zM4*1hGp}z?rQM{%1%&b&&$u1BX1O*fzvHxU{7Ta0@p9y?`B@0wM=Y%`WnZj zcFxc3)Lpk`o-$bAy78t^v$Gsy!Cm1hVMcBKGh%gO-TsI7=&DNSFnW6))4$;5$?)v> zx4L+qr6PPvK?}^A4()UQ5x-QWYW=Ra85iY#K05zxSN_MfCMDEMLoxB4)l9xKk9@u@dF}jRhHGO_7>h&wPQMP zVS?yPjScrU%REpO;W^gG-RdHv`S}&|feVv%&)}Zk>ygxOb=xLScZTR>mRWAR)zd>a zZ=HA1K>mQ(qwK`ygx-tp;v2Tkn4odzBI~XjTu&z|Dl^a1 zb_x$XuU)TgW<2(##33O{pYhAY-Ko2B3QyT>Nq^~Fu{YoR^lz27Zcc47=kEyhDa@QW zkF$T8{=rptXT7IC3-__vIq90-WzCt@$!^DAoZRwkh+@2OFg1* z9gEV6*>Gz14N<3EYkVc*{GYjrhjl>G=2hmijpU~_ zrf_wZvocDY>d8=QH~SLZvvlu+hv|zh*}p$Nd+D6hQ@)&jdi837hp1VO$fb{wUzVhc zSSYlvxP1A5pxL#s)qf6Mv^b)W;~MMXerDIU2=*ywUf-~JdM!WC2a6a}|j*Lj)fB=>a7yp9=+vnM#p&i@mlEIaT1HzAESubRnw zRxjprj#OOSaVV>KX{&py{W88=8PQYgc=z5vbLPZFo}}ZyrcYY=;&S4bZCr;LZ-|Cl zSB4~>jnO!+Z)S0S*PT3RyYTSecgvjh>a9vA=3P3a6FWz7pM8(jhPP58e@x_6*$*?V zi}>KHtngh$NF>?(!NS>|sxPXY|Ht?Qmsk9ma!xj>^UnEV_Ol#fmwOf@`*h7V+5IW? z#Al1=v)AmoCUtJDh{K&vNnSq^>a_oeBs;TlE)afiY%uTik~_OwY|9#i(sN8FzY@9- zyQaTu^CM=-yJnwfD0WOb;POde;Wg8)80Zh_k-)9^~sKs_n&yy_Pp_) z+-Vjtb;c1Zi=dwchQ)zDgdGC(wPywixh0y{*>1V!&YLT;rPO>`ikP*LM~KBy*ZQX4 ztEcjb=X50N&2vi^ zi8X$ZU^pqL$McZR%R_=H8!rj0&lR|Pb@fKY>juVI5i91LX=XhkI#HR=`tX#af0Veq z7jU!xIJU~z_U*BnbNTBz5^jbz*&4p|Nvjb!v)re0nh$5;uATGwE<0wV)ZEBv6wp3(nUt5}aks6W#aF$YGjUht07%lLiBx zmm4poMy4OV@@=l4gKU!2rOo?^sqMVaF@c9ibm>ly z?Pv3LukFf>t8$ADH!@svF}EW$qc~w%X5r-x97f#>)@*HCFYrO*(^*$GRl_AcZF~<5 zRg6lS@)h=M*%+nN)R+^>)7)J4!9sUs^NI&2EFNg|)=d+2RsAHouw%pa6Ivbtv#d;J zYvmP$?0dK;xTU~IU0BFy(a)q3N4}B zsUAPCOq!%oX?3Am(8ZTkL2J$Gs_5yvYxmup7IE=zSv&91Wn#X;({3LscNFW2zy7iEVHf2$ z&ILD8);BC!`_HY2MYBo4QEZ2Bs0;&3#N)*0Tc_Q zRI|%A`$mya_b$=fHH*!|j=r4R@S?NqeB;!;0*-LjirtGC>m zXwUNKU#~+7bnU%E5DBLm&p9zH{pD=SnQ6@*I^=;HmXlN zuv@CI7~Nq=6rvmoD$43eWQ2I>rEF-rmE=7lTuM|$!BPv!*AYp{@CNzR6)^& z4`-ijRCPBnNHD+ddqcZ>g-=UkQLkT-ykUQ{mVNQOXX-aSc5`?yx07UhzeS+rOq%_d zwHH?HPs;WgjTPcO20ViKQw(MHu z)wT2e=PK_Dr=Q#_a$oRjvy%hoI)+{9H%&qlId~FcmN{)YxAeQ3z@jXV30f~^na9`s zeioj;F8=z>nyMf3DxCK@wkkLm{8Q3U(h9Y!H@La_j6uJy(}XFjUN|v7wMv;;-zgzl zx8C*U0(Rxux3gw15;J+|zruBbmD^(0uz3FBl^-+=Zn^0^)cZEY#NMyb;cNuk(p!_i zmbIS`ywNa8!|Z;9a>Zep#-mzW4{-#%c@o!uaN~;4%n3?zE?*7ja!l3r6fgOAB)iW? z@$tH>^9ORu?&Y?Ay_54gZS%)J<&6Pxy~US#Bh8=0`WMWa&v)|W44M9uFQ?edJF#i@ zI`tJUi}+f?US~agvsNs7V&AU%6{`$_??3-2P^@3dsk30kz57qj2+crrRSKRT`ciJSjJ=H{QX&#TqVcAuYdB%FLtescr zEtv8@+kd&4&_+G33#VUaEIPn7Y0X9@mW!L_2{gL-eY&|}o%5fc-_E_)%)07?SoetIMz5cPSKZ?EdhIjXkxT69e`8@*I!)Mt0i_}hxPH8a< z$#BZ`zR2dBoNKuCh~$UGCyujvdr8glVNP7uIfd=m^8mKB%XIG5W=!cdH}9P!ZPBrMF`K3A(aca& zzTz6W$;Cxoxf7TFHJW-kDEZH`ozIMQB{Uh1xMZm>vs6Co^?cvoFJC_Vd;ECi+hyle znO`O!*?4TfIK32Ig)_&0i*Vz2;;j^{o9xIb_Q@St5-z?_Zslk^ZqVS+d zMOEm@^Ri-@b+)B#H7++gd@dGf&7Qq`|BiM0SDjwX$$iIG?)Sxjn@da&>_~WiZCCo< z&ns>_@;tf|s<@D)StzsF^uVI2TOKq#*{tZy7ErVBywCdW0#U0L8XK}alb3YTIdsWz z*20fb8Bb3=V_hU+bZ4!BMlUa26@rBAK8f(wo`_8ht zWyNos^^aZVsJ}j)#T|2bkpuk2UP?9E?aSTWweQrOX-z?98! zue#6j!MTQ8=I(j5)y?E}O0=r9|F5TWOEX*^B})hd z9`57TZS)pSS30UUXBk(r-h$OWk^&27Z#UOZ`($)J%}8L?xi>*x@2sX>@(Jj1nXIrh zx9D7h*}D8#v6O>fLo8GZn;N&cnj}9opS@Z4Zf)DS3;dsV%3CcCVoF<5?83TnnuTB! zV^h(T)zZ7fk_3)k;!vG2eRB~*VrR>QP5WN^J5gJWO`m?f`!!uu(PzJpz{0;DgR4!%m{Rw4sd;p*-7hi8ptH{A z%T2R&^3NaE$9&R%zrMb9_vvFFl^bg=u)KKCyW+@Z-ax6Ew%0EhY!8a{IY)0fBJ8}u zMbo)6)XA)QhS=>RDt_zK0=IN)am`c<5>K3^%xLs^MZ_(!vn&2O7yn4(%Wm)r_ACFm zm~n&EqGMl@?j3%1%x0B`N#w(wb3_g?FN>Qe>F>34)`Kr4FYN0q=J_9f=n&=8%~v;mnzS#r4&}B|A`lM2hvJaJ>gDn~b`80VBA{j_-^4-JDt+^2OqW_{n1 zm(8=na&6X5xAIEyP8<1``l06Ugn0grKRR_v%&K8tms{tZny53a!7$(D zh251C>fSZ)B%T_}{&==H^D(pOVU~rpt0it&d^uek)i%-efPqy~(e0yc8|Qs`JHc!T zr(w6mGFBsJriD)`RC5mZEi8{sTVfG<*@KmFGSj^$4+AAROjFaIq%?3(f1vg`R)_bA z^S{^+&URL*^Xa>PR?VOI)akf!&CE}$w^WqHzFGg7H)Z`N$-6RN z>!T;}6|MJee94wz;ic8&tLvrzFE~h{&ByS~S;-S0Cg0#X5oq9{$LerTJ8!|avuAw; z7G3YyajfyiafO4&B=~tg{0!JOKTw)k`?|XKeHE`i4u3U%qqJv9lJr&z8=4&bRfoJU{>ZKL5WvKhLaG zt~J=J#W4G!4lu z+jqWxzD@Ra-n;ffUg5I|titOqi2js6srsONp3m~Sy2`y*JgdK%8Fsv$GHnOn$4$Cr z0fq+z&q{E!2`p|`U0QJ?dxpZMNqM_BEZ;Hr#}O5S-7-bz?i$D(`6H@SYiaA-`}b6QquePNdXTl7utxf9JC8s>ajK6Ar6Cf#L648*pvgk)JLAK(9& zSv^x`(VbG|Q$pvxuijsik~n)>g6MSTwJbgAtGK)J_Gw77H2KdA_1&9pH`(IT(JhKO zGRKsaryN^7qsf-3&p=Y_sQ1GKD_^im&Qv;~v+0a<*1?C8x1~~ZRqtzV;Cs;#y^!;n zYjU=Jox)C^0u%N$nWG0U{(0dw)qhzS_eySS?>AGcr#`jX%`)+Q-h<++%08u2J3qNy=6lhUyLp>$Ru&0)-wJt}cDq7q!8a$KzAg>6 z%hPX9e6iMl>BPlKF1Lcj5@i(H%$PTyVVr6ya`)zuZ<`A$CTv^8`8)Pq|K%AhE*d4* zeP`TI)8FP>7TUW#Ty@?0SR;A6%l^JmCO?D3or|aTx?bb+v`+o7smJ|ff>Gy-0}@># z^QyXL&J6L=s*X$HN^JCfacEo8g%1wLT3dFm+nyD@*ZG5xslz{H)#S`uqMoIy!szvn2_7H{X3% zcKU*B-`Z^cB|=9I9f`brC+X#0r6%6%Gd{A(Soyuof1m%pODxu_?dL+-e;=K>H*?+! z6AYiWdCkXDGD%xL*6kPh{Nd5ExL=v&p>@UKwzgllfB*V<_T9sm9bT|bz1H+${VKUB z!5gP6^Ika1K*jHtXOye_mWg$JEpJ}R&XBa!K6zuWdgqh5_t}0MeR&=9{#U-SMBbxC zi#G843q(JfFk_0D*%mf_*F^EyEHgK8xt5E}oyV*0KR=aec~b0I+lDD0Y!ghD6iS6( z$bSAO=8B}17E>IvK>Eg{u#`sr7S|(6l^uU>UEmXF`Dw2?!!BOXsr1I=MeEGw`K{JE z;b`Ff?a89q>yPs|3oZD(R;c2$5d9{Nu}A z0+P#1IBf2?U1glThh^HVl@pd9Gd_GHjme?+_w7fz@)C+lXD8{hG9T#hU&)xX?V{f^ z({&6-6sm8CFsBr@h#X8QD0F_jo~LWpm!K^x6QmtdB_q}>OK}amon>t%dV0}^FMNto zH;kmdZ#+}=>ByBkDIGE*-_J@WTWvp5UwcV7q$<*IR;PM$n%U*g6XeBR%iQcXQRy2) zEQC5(+2ov{CyK50~3;bt@7dS>pc++xBAciJV&;lU;ft9w+mlS|2zBn_nRdu z^P7KUIxU*GXVvq4>*M#FkhZz6w)foU?azOn&fZh|F*D+w+0Tb>F8jn5O>-4t;SAxr ze%3Mm_?iu_Ws1?h+AMwbp=)(&qBd^t3<-6f7J7a5vX8Y#*|uG^xPEy{>z}>e#_CIJ zO;2-KDjTb`9E#%q#hTIkI$i8~b=3~RoGnFYE zt|Rl2;&Zj4!ln<^Cl&5Kyq2Nh>yMWl_cI<#BzihOeZK44*##eDo|+~26!KJ`@wsz5 z_PlrYm9{sE>k2cqVpAKtS8@2id>OvpWKH|RrPi}Nm{buT5f562iwPUnAg zvD3fR)oA7s;j#BxN8&+q!@xQ6LA#TK&gw}^$Z$%1`S7PJ{D+s7I@hNS%VR$;TpDsF z*7`tg%wdNq7ZM%R-!D?w_U>ew%dhk2rmElOo##~Ec|kBZ!*xzp>m5nOSz$(D`GQ>A z3RpAZY%EWtDF#=a?Ox!?$hnU3YN*22hbB#v`d-O2`6jQr(K~UEki+Apf0GnC8U#%m zcL^A?u&!^v+jz}Vk!g=_eO!(>bKp}KrW#6$cMUB8-iIA$C^&E+)NNR|C?4lS>Arn{|brCZ?{}I>|yfnTgjh~GxqqGeP6$S$I9hT zULL>y|Hs3}mnAL3r#UHoV49cKmMPqr>v-zI^Z0*H&Ej*Lb;snS}iMH^eo_B)B0-LqmGG5(~_M$cfZ=@BzBtVYg}J@A0z+qe-$(O zj_=D|efh$d;LI886HV8(xE*XP-INe2wsCox_w8ef|7)i6PD)&qQgNt8t>IaN$N}fb zwtEZi6|^QM-}OvQKVq-l6*A>U#MVOsMYc!Sriw3hnKg6%!c4YD+=UE&?#5n=8n!&l zw`f^kzNA53bpi7h#R=&So7R+eNyN;WRv543QS&S1pWd9n8+^=;vqjltrA)J5h`zZ| zk;wlzn~A4qX7f%72KiRQ7SoRbU2L4~JT3tnpSf-eQ97;B`%B!-bdA}XJtxEr*Bj_* z*4&Oa6%byv&ivVIpX;iIY6}aiohAl4dMniD zdf8ccz8n9G6$`r=&d6?4d2yuvNb8?NjY*sM?Vy8V<*uI3%ptLRX(L2=kGU zTJ#_(daL<|Jc&z7rCZ{byxf>|Sxd5K%PBvR?{~tl--%AW8pWbz9^4G=Z*!>Elrn%xO_RFAtM^O$805^(7Oc@gS}G1*wPm#HEnt5!GP=|UMJo($E3POCH{*^X(;{t+3sdY=>B+XK`~Pj9JuCRLWs3YO-`jTt&#F95e0TfTmxVRIzVOdyd;k06w+#vtw|od+RBgny z(x>g=$`kABzINyTTOEEh{Pw4Z73WU9o}T*8WNUHc|4;v3ZJYD&G5`O9HD>1Fv+tfe zZd$!IW?Fw_*{f-FK7Gk`uO|HXc_l4fp3hC{W;A*RH?t4){P@PlI`@QD5?gZ~&KZ-t>%%8M! z(#(Zgit$Rh%wA=Y!VT7m90EHoOfY#}eLcGQWcE5e?v2`C__H6$yg8;Zz0&#Un|<%1 z`ok(F{8a8v(s|t|8KxkX-SKKx$gEop397d~B>L}s$SfPOXG65@GRv$EtII?j<*9G!p z-1sovzC-z0r%{>G>`%PfU*<@+#_AbFq-$$kWIn^two-*d{dIrE&DW`61v8h-dM0J1 z!|9kRad(aXv#ew5p17P*mb5fz44UUWO+r*gQoa6ETg^F}i9BI$p^F-p8M922`mcBP z$RvgImFn+(XP%x@=&ql6Vs%#aUj5ZO|9%LOd7hNBZq{zwCG&n?b}m0RDev;!^}jh~ zt=_GAJ56MtnM4kUwe8x-tM{&iNXcDGeO%o?{mGrL^Ixg1^uFVA&p33iOxw?+Z}(-( z->V8`&omlL)QvBfQADo4gH?Hjdly~CiipG>2 zhk#!Z?_1AAKH|<(UbIC0a38af=&^(S%K|oj6=_M(Dej)4u|n#s#EeBpT936%iV$o+ z!#KlRl4Z(-xGcdE)iW<8rf!?NO|+Wb`puFZ>wXD1AK75IY*p4nf%{iJ$liKdCE2Ml zp-RYNT{81Se(DL!dtQ7rNxD7JDlQbUY?mTKT~qs)D21J z&Z)0Yj6BG_VMPkFRc1t^%7RU)(hHrSj~V2|}^UB$E|hCn}%l>+r66 zCY;=IV|Mzvj48>TJ7V0DXY?m8T$;6hg5bZ3$8%o23aH}TcU65p`wZ3e^*sw`ORm_= zraR|Of0oqu)SdGtXhp^)vYmJA%IHv3H0D`2@1iYZ&a(24+EWf3a(pYY&r)J*&>8ap zOQ|nc`g$LF=rG!pY}n`)cl*_h!lN1wm3?Eh`n2agD%-c=nnC5#&%5S@Obxx9WBYvT z-Mq5jyKd&r(z+eDyXjHr*Aj-D^M-zpGZx7$H;<9!t7fsyU8LIk#m_lq#nJ;7ipFZX zYD z7UrJ!^e)fH3SAxA6Ckm1!SSA(Zch0nb6VEL?JI~$TO-9DsH~={$i`~cSmhi$ZQBIR z$aTw8ra$qo^s+G2yrCR3ua`&6X7%)h#BB|hUJ>_p&NO|YztA(-_FBWF-rKwPSN#2y zziyKKKKnoI&fFJ2Z?r8bl-e~3yEcV))5 zwH4Rj<$arNV`;m`R`2TDuV4SJ`}O1Fjhne- z$sv8+^{W}%qP9e)Kb}AHb=9$33!Xo--lVG&*|Nw(QZqX8=ej<%Y2g!ZCMwMG=l4In z+)reM*Y*Rj&mgIdaNKYw3@ zEq=S8?%5lY?%cWdRqX0SqmnON(>e2;Or*5h9JgCce0o&wa_C&M-?J_A&+KQqR+^#1 zExIL9rRtbY60dD|_3@e$UU%|LWS;$b+n>MrbMlOf19fKn zUNy3|H+psP{LkDc3lOIUfVvO7j2 zsn}+&!-hl0QOIBaWSW6)bM}8+i0yFXvn)?%yn0 zCAKKt=i1rNcjrtEE$#byt;o%$XwlC3^W^GnzD1?KU#;5nT42w_wh3YyC=TIj$GLxXNwAwj%$7U8ydg zzC`>g;T2&!{O81lg^b0DMGUPH6W?TCU$SaAx6*WmxmP=)&%I>n|Ek~!ijyiR@lbWNCeE|{aa&G!8XS% zwR2KMaAHc?>*xrbFQ%*l+j!0v%&1h-X84t9q5D}ga@Rt8AK}}&W~VnNB{jcZv@qP| z0<+MJPfM1h_^{4sT(awsU3{Zp0`u}Xv&;|Y!)BlAm|4u!;kMs4xzr%BEK%d7;(Hg_ z?1>Y8v@Q{-n7OK|K_)WjwoslY+s~u#vM$OTU8MHuu!w=vfklxKGP9W{@h{-0yEsEF zF4gdb%Ar@1H>B&2y*^m@>U{an)B5{$%&X-$v+Ufv#>Z-%&}p3>PQSR83&OmWrcU?y z_f7h8r*ZM}h}|zMr0n<%8gKCa+}y6@`Ksot=scUa$3WPz^Td zy>7%%7ACWM$=`}^w*>NYTx%O+F1vhjV|iXVcc+BM@!et`8ww6G&J<2Q9`~v$W?g8L zp_;FXd|{XLi3N<;m^hl#ZfNKjPT6emY0mzNw6b#=Mu&>;J0^>7IkepG@XWvJ~9TE@g~} zzjZL8O#98UOUZYdKCF7-xr=qal6A`D=W8ScgY8<|8XN5RIWC*}I`8RRcWRBq={Y5H zC*Hnu`OMEfGUtxV^DRIA*dgI}QC{);R&RBsYdsU>V`^2@%T93g>M$s}cnErMbh0PR z?krd+ywT(D)$Z9@R`OOeLL7>xm-B2Yn6~85SVPw3kC$&KQ`9puDbe0G^+b24;sbTD_D(0k+@-QbZpBZcC9kcj~O77Ln9k5rc7ntoXCZR^?C@Y%cG zaeQC4@%3)!O<%e@x2@_+bXk*B+uf;`!eaR>Nx+9&V&WRZsDiSqs~iqZbhSKJ6VaV} z-$Za-yK%(KqX83JQ|5jQxgl6&*)~ziWSN`orM%zz+b${C1iV`{)2dbR!j7tIvvZF$ zAIm7NzHxFKqwJx8T(5 zcNTJexwpT)-KcDsvoZSZjjfM=UYWbX{;O=-7n_8$yFPp9`>Ybz-SYUx!+Fn^2Y-JV zYhKDfZ~lkO^RvQV^lsLW_Q*&QDc_#-vT3J6PflaUD&-t&H?PFAgT z#Wv(vcspd`^9W$cZv4e(|ax(lpgJUXuEP^rM2Ue{MPU! zp~6JZ!@_IBg%3_(a0t>0)wuV0x6}`d3|+CgIcynw*KkE%KXZ|*@so#5Wvi6<)TYTC zn?61LX}$Cl7r)H*$HiwvoB^Y~20jyLZ*R zIx|_|&o)WolYqWnqGD#(P&dm)@fnwWoHs8F!H8W+`v@LBM zKBgtPJ`cIx_6q)q`dYU3=g$d?R_m<#5aqhw@^Ax#Qj6%jt!wtLK9QAg`|<5kOU8g6 zJ->fa8Eq_W9zhEkHh1sba&^l5zPOn_PLH+bl|-Lh9m(S4e!`a9eMh2^%k$q51(AxuDtD2iAtCH=h@N%THoGg9-H*b zM?nAU?8i4XK2`|+o#Ln=e^}H^=hwngZ^?*hd43&<-0`!R?lL6`dTjXS${WbF*xmo~ z<3BbojLwg*9V+Pfc%k@=&bDnnlbwU!+_Jns?eYXMQ4_8CrYF1CzdL<NLA0{S z%g~BPUu9~)RD{>uJ%ZXg=B?hsN;i#9{+pEB=|98y{Oql)AB*mJT}ZXp-95!1Or%Wk z)%;q4wu$--8WM%mc$})b{aY9WqnWh|p8x33l?r&a*4(_K|A4es23GjHlo|Mz_IyB#^g{AQS|=;wb<^|)vIa^m^t$~Ohu-@k4#a1CUxE9diGk|cFZdF*=cV7Y=(n$fy$0UF)8~t#IejdmG_#>cIxK?hwh%Zf7(j* zT7h}gq4vv}Z)$cpP6_pREbt)le)-jyyzc_0&!%ouv|R97PlfwFhvXySg)tYLy($hE zE#B|4@8qSIwGFkh3Vz=!)r*g;{loE<%ksxz_MK0nS2mqAT-`qH^Az>@w>Y~mi$|JX zsk|rPm^*FS+-G3xdw2 zAqIB;HFpPI`t$15y4jyEvZp-YI#KuO;Ek{!0W8x^uco{goAaBq+f)3}>krivlJ4|> z*4@~&bCKh^?Yoi!gCD(TnY`!f!Su!D`+gakMNHGpIc(`Zr%6}s=%jBimd`S~zr(@d znt5XAcAwrJoqORNwimVd!iw7(zMKq^Zk5{m?8zt26oUt>u?{Q^9^YyYX>+PPP>kTv zVF-D-Nm3-e=f~_#HPfE+KmPf5vU)P7!N%EZ-#=D!llLr&-63PJtRhKX_A;-t^_S(_ z?!TL@HuK+y#}5-{yw892I_T7DC(kJTlZPKpxN@~T{OQM%e|Hy#PkY!9|0nyzx7nWx zV&u+GPXAJCC7V}SSsZG!Ep6dJ&5sV;P5E|87r##WATJ{({j@;y#5~1Lw?s9TW8YeL z2yba>jaE#wI$$Bd@Zy+(=$jAn8yB|USRh)+GU z0Rb&D33-F0o43Al3HGxw>c$7nmFn{fH56c#cJ+{C_>jE4l)dJUVwY46v)S7>^7r$0 zH-8V0o5^T2CA3NXJDZ2fhTOwn5;m87+Ssv>!)312JD*ztY-tSqtP_^x_b3!^G%4t; z@x1)NYi;qR!d~I#QU?7RKEWHCo=;W#(Vn1pIArT`&zEk!yahRbo7y_${~K~m{j>2T z|HSg0H!C*p)SfAJcA0$FyXT+vL`^!fzW)!|V1MJ9`keUxDy`Ed9`G97sv%^iX@AnmH-wPMWLtJTHw|6E*ZwZ}OV|&RMgq{ql^*`=@=W zkm_3Lp3~5%vq0x^;jwiBQ_dbcqPH)RbE??U$X~1b&*e|N{qg9Q13KMX*XkKBNt<%I z=jDROuhtye=2hMv&idf*oRH5&#%Il!d=?HX`sU&}>1)cvg$LL9xGm$_YT5FlETb+o z$ysxsLbt~phU&O=!iNfUlv5N|o}agY!^2@x&c}kvrnyIdZj1aiWzyDHEC)}f$Tu0! ze-a~}9=Kfcc9u-)EwzW&zqS|6y7whcq4blM|HRaS6>bH+{F$5othynvvHbLhjj#Iy zobR_MXujLPwYqVWr}eW>{iiyv&sliG>}c-HI-9S#6Zh`CYpcDfecfx0)S4-M?@#Q$ zmA@|If?nm;o4Hblbc8M{ta|qP$y1TfZ_R9Fp6|Ljp)zcVP3AAo+mB3lAKdgH^2E+R zs@g_<%|Zv=W;R_{Og+ik!^P02zPv%};mI|*rDn&1zH0<%ISH@l=a|yuW3nLf&Rr4h zo-<4v)`{NguJ=6M!_&}j?z6;V7F*klxp!m&qtCs)Fjs$j{!KHkpK6zT7l*TM^zHp6 ze3v9*|5O1N^9@^%$A1z({lDClT6og5;gXAx$D)&5 z=Zqdo&v^Tx#luTgY3uobgR>H9BV_vAcq7#u&$8YU{nS16C(r*~2e)?#y6#B&+gK%} zkiADne9Mm+A7@^asqG#91_3PH3vFR$=om;m1*{iJGH*+#P?ww^(`SNIw zkwX#7<0QYGM-=yS-rn=(Tn0~s$rmf-{#MOqaqWZG`?#2nRyF@MDA|$4re<<^nxNJu z;S-A-yuHiH^X!FBd0y9F*^+zq*_zA^YZvF6@8ogk+;;qn{qz+dUK`)lSj7C=^{C$4jIhhLjFX)|mX#mXKAEi)AI1B0&O>L3`j;n9E|;s((r=&CX}G&j{g6$k@G%jt z2hR@Aj?(jZx;@-H$m&3h+9TsNhOvU_9~Opty;6NCwn$OrsAxyq;jTH&Un9#-1ob?a zAXuzAdyCDq(}j}RMgMoyux-9^ptn-z8Rrs%+~R3h=B@R%tNOe2nY4sq@&e_q21Sh- zY2i_`Ra=+192NDqTOgOn7zL&jc5n_nZ@h&urA*=x|Em!mONx=~Zs4O+T^4 zsq#*G+}U|7@Xp<)l9Byt(*`ED^|^bt zOMlNgU-{zDHlyvXc}gb)FAH2)T-jzQ7n!QOX6m&gj*2_$o*C_5+?wXLkn5_;tDAha zpJzP}n6&isUVpDssVVNE^PDPel;umcZSvOKy}9gQx_8QiLuDaG<{lq6hML5*PN_F5 zxW^&Rx^Y>rl;{OF!%RlQqw_cMy>qp^)lnib&G?nbEnn8N&abChay2`!YpG89b|o#R zqbB0ai>GJ4accEsqfxh*0f8E&g@80o>3ASw;`Y) zN1@*8>x^p$oEv0~)4JvNFD!Yp=G3|)ttu81{!KddK;eGw{mt#n=?fSXbxa?-S^WDf z!1rEjayHXNJ(dGC8H%$H+gyF>;u9bit#t5fY70-GnCn#mshv)uG9lfEmwCt8Gwv1e ziaYW0SLnmj?yJH?=J2wOc`OmZGvzsV*2cNsTB>p8sy`3$=DNfDWNxEy+7sVdys1Wlfz9g+S8u48s~dbULg|dr=by&R>-cUwR=PZa z#ZmC-PW`U*|8Hm4|E&M+-=2R@|9{l~|ERunuKM$riK{j} zy1{qc@NU?T#XncRwV!)j{r%m%%-i1z+dSDa_9gs%TUS;5-q}uWx0JEj8RiSqZW{1J zMD7rk>FN!i3;Ee0>pXzkpt_isf>iMoNN-kw8lkq09S zZfp*kyE^Rq^|NpPu3olpRn67bZ-)Kzm)(5CYGBQ@>WqVLd&%_pW7F3CS$#xA{K4T9 z$NH{ipIbFM^~Rpcul9cf!ukK}9W7g*kgQmHM7LU-%}`=TW4BFFNyJLy@3u;8J2D<^ zcye4=>w2k6`rU(1FDL(ewC?r#cw4(?$=~;>wD?YM+hL;5wrjhro~J-!ttI~q!=@KG zN^?6ZdGF5P7gS7$E`8KAE2r^LlOpS4qy78UwjJ6~bv?tVTyXokFHcX$70r-0+1%`L zPtP>vXW~rjGd$a-W?ovOcKB4M(zIC}^L||1&ZqZ&FWa^mT<K;knnJe4JCSRfPFz7f9+EJNMSU@%sJaM~T%hvzEjc)o&8}cfP%~&epc{ zy>G9)V7(3ME4+n_9T&I@zSH?FoMfnfUjqqiG zkNcu|PTuI?Z}z|9eE7k({Gxe&%j+_d*TzO~jH^}uESRucs42E#;_=rFU78n`f7D`s z9-Y3WhV8D7v-|ToJ#2YPa`;Y%UDvO*VfX&KL&Q?5IeU^u$VHONSdBe$jqHxqYF|kX%g?De9nrL7#O(D~QjXkn3(I-N99`hp2Htj#wPG^&C#)))rfYVL{&RcF2aOC4I90#qJgXs~K&pF80W`onHOL zWV@gKkN=q`^Vgr;87uUuL}X@%G0Q87#)j#Sb}4Vp)ZgQ==6jWIINy;Y{o<|}R*r~3CFk$$bwy~r%=jIf6F#K~`_zFBrfuzyL$2Z2QWZ4WA{ zejN7qPyYPs_UzTUhRbeF|NnLN`+e*9>;J!d|NroEfBSF!_rC|U?O7y!{MoOg`@HyS zZ{E|BObCGptJU2uA$;s7MmF}G=x<6sn#R8j0t)C-VM5`92$%rZy^@P6BSzVdh zoX@qJs_@n(TBmHxA8xNrcT}pYl|$V-AAV~IxZJB$c}F8+JC{p zR4GY+-_Hbt{tfC1hcebZXg$ZxY+=l`pJ(%}xuVa`J6kd}FltF}DG*UUbag|ViA?T- zg$z!NJbteR{k%VOmjLGv>q3YE>WHr{cOh-gOCYNgBD#{ z>3CoM|6PXoZL!l6+&65v(%1Pi#+p@9aPj$ySdZKbj`8Iu0`|V)G+lIwlQ(tO%oUA+ z@%Nw1-aWmgv~bZ`Ne5;x1px(JSCIy9!GMi?HmSW%62(6c%2YF5W>|mVOCrbn$#0&n zkN(_tQulp>XUso7xxBuS#J-?3T@Be!>{?DV;)6I`wOJB=t(X(vcY`>MO_ehs5 z&93t5wmV&Y;PQTqKvf z-j^?rJG@)BqV;3Zn-wh~sg75SE5gP58l&`!zN}itF`=`~=uMAyc0=pck4LyGxo_M4 zIsPlTD`2B(-qP>S57uroG)gMD%lyI*JyYMpRZ>udD*>Y>xKow z(rK01zC3ahPCK9M-g~Ujc-BQd_J(L_uU2L^@_q-@HOI^O;w$36b6(hG;* z|7^~^TEdeqR<%oZpZn3v9Xnnp&RC(xtZ02JEPZS64C`EN-Ho-gavGA4D;_v{QRd1) z#fG_`CT%I)yU*0eYU+`-bHlf*|f8`SXu?*Zps&%lAiq`m!O!xNQ0n`5CNYovtn(>(eLs zg=lGBNDF^0sCje6vxVAvRV>y$an9?VQkkaB-#_`!&#P?nR6Lj56_#98ZpXd#d3($B zWGUkr7j@P5oECcix{&clt7P=lH`AUxo%E)rOZ8#OrGEW)|91U+b~Z%)<%h!`m)HM( zczDwt9X+c9?!3wKwl8_?eJSKvpXau{)nC8r@2md&^z+-=z54I(+3a3hKI5fSaG{Ni zUCi~P#qz#ZI{lyh8)G(~sm^r_sF-?Bb?tK2YOCuNSAMOJkN^Aa>btYKrMG|ZT?{t2 zDNSF$`O{O&fcI;Pr9M^1y2U?tt@eB7yEp&Yn>@d7aW8xX+yWl%4*xJM<Xw%=~`^dH$c@_@4hWQQL2wjsD3>i@AMocS`TR{PRj_?fuLD?tE97uejMuqoG{) z_Tjth&YYU`k*RSoXplpgF!^Wuj z3uYJ}@tI$Kvtw%D_nd8f9^%WRw;D>Vi23+oh@aHFY_kR>jbV{4`VD`zRrLJF+1Kf+J z``WXueW1H$TFJ+c{>MLu@YDvX#XX2p@iw>5KKAm}x5F#v@h!i^p7=a<{*_GUX$=k&hVnY8Sn&Jau{Lyl&f>#d}MZJWG!6%m23j@2l0fk3TAJ@82(f`S|PG ze}DJepOt@{Bz}Equ+y8g|8IUh)s3IM+IQ>Sr@M=G-;9knPrj~dd;6=+x4(}c|1Qt} z|M76*zV&6+?`Krm)W0_>p06(3p_HBJbfGDk_vcLS_dD;Ug?}yA-?m{{H3l zzdru`eq8K*l+e}Q2qTHEIilvNb=IoSEB`$x-EB90`A)CZ(|<0DY`bhCuYSWedF8C7 zsZL5Oee1riS(hCf`unBLmdg(xeBSJTc+%CQn^XQ==rf;fH22tc|Jd&Sj|+Q(`%Q$} z6rG#a7^oFrR6l)s*Ilu%i6`0?wmvFg`_+=fX6~e+d4pB(D!=xUkFvf_3naFvTw)G? z#Lj==#^Jk?q7&Y4($;wLVBN=>d5N*BmDZN2-r?^3sMNQqNicBQELr!TiVTk$A~JkK zuk6@)`{0bZZFaE{*>aI<|2ZaDcyK&x2)w|uExb0uJY&Nw-R=vHjq`Y~u;f4R*?I1C zagR%sn5{^XUm5F_j@6lSR`oh{et2}cWF_1>sN%P#h%e)oe{24fA6V=G><_08qOte4z1tq+9@t{pMys=T(06HMkx^NNaWl`^>Q{eH`;LyG5uSeg$wELu>&bL7XQ zyGvw?jHbIuZ9Uq+seZ@g$(8RZ^H_OHp7{Pr`ntiwhkZ@h=hvl7$Bj4Cl+KRZ|5LTo z>)Pbtta{17MIPK4JXen#@atRZ`P6%rwDyrjccPSTedQMRVz6G88kn=`%eSBZ>ML%f z{r_V>$t6qs;Gxw?3@@Bq-L9@;63(c{jy7we%dsa?PZv$xw^6Qc=92{VO^d!83KT}Zp7iEf6o8D{?AF{|LZ>3&9mG3nXz8V^L+5MtuG{0=8L;*I>fW&^U2u{ zC#;^i;$y$P?7e;7@^5#W++M}R>w7!xc8IShpS7L#>D4OMXD)v{>}=Wd^G5xvd3{ff z=C%Esvrp&Sc2NhtRP&za|9oB*Z{Pd>=~rW=hj|(Ok3Vjh!*}{YvPoC`-S_XhpDs_{ zb@#o%`{k3b>pWhru-CuGO-W(lrgpcGjQ6#hV+3oS&R()+i|o4f8m3$cD_m=o*Uz7> zY?}PajU|YO)jVO_^_#PoKiVQAC)cd>cKPR>vTPPB4rmriX;1(5#PFJO?&Fx&?MW?$ zi45x*5BMFurCMaEps~e8WJc^c1B0`NY_!9;dNyvDo}d|@*3BarwB%+&RKSM@ffmUh zo1K=P;p1Jt=JD;cxIG(|n|P*}uRFE!(WV5AhgY}$Nd2;58>5+balmxb$_$&0?TL~T z8jsw1yTxeQ;^ygE!eXLbu&U7|ASL>d+Q7;#6y(jkJ=r_i-FIG>>;JwyA<#;s1@zgi$r#vocpOS@a|FOU2#>?~6*JzT!1W)9=4uw|dt+^T?ek$+i=H>#gMcg54$No_Dp)3trEme<{uL z{LQa#eM|3#tdo8IP3zX>FP9%{?%cU=|H@DP`R9}EY((UzxyL+a$lsIkMfi1j#&DC+BL`U*pt0~Z^)k4`pmbzNqBDhBT19L zE8p|E{h#rm=&}AWRn5;!-ZLoOx4VD0cIW!;pMU;+`EvQ^<=w*fHk|+TH|_J$(AedN zFKbr%JbQR$ek{|4+QYsnTw%`Ihuqi4?5d6ny^#OG&}QcOHP0DtXunX@a(J56cSvy0 zl=KZ+A*Y3=X@uQ6p8D-b%&Y?@U)Wb()vMrSSd`nb@TgCEPxbBjd)Jz=$gaCyc%1Rh zx>KvNPc~(UrQ3F_Is1%piMkA%%=!#B`6(htIlijwn2^%4e4>ufrv!$H%O7+4D;;xq zwAs9kFW{(@LS3Ivqt9J7pW4G*1v#@{1to-U4m5w1`1(T2$_viFeqIn$NUO84kvW+6 zP2-Hn&ZC#k?~7(y+YqBX^J$h?@u34Vq?G%5ciqc>m0KybuKriz?l1MvH^s#@h<|R< znYI4fnr8D_W%F4vii3?$N8yV5(&GfGD)x+_Rd(4 zti5b(x&CIfzl3uaS7@x8uleZ-`JXRF$A!umo966!eYN01chjB%)g->ml$?egryegz znN;`vYHy*Ro2_LR$$e#}uQX3p9> zeX8@r337|7OtdxC@)xzr=ovYiya};ab!`*li>N;6B*0l!a=X+fd!gPV$xa1BzI*Od zrhQdgZ!phd(xKw!dHtn{QD5CT&OPqnsXp59y>s`GN6kzNQ#w_{?!-89PF%mahCA@t zM}srRH?I`Bv!Ih-@YC6nmiBr6b2iuRy?I#9Key7dZ5!vU#WQ-+w3tuYuhw=jnECaf z)`OpCjLb|Lc?wFOa?kCUd-R&ue~pwF$Lz)1w&ni+@Nn+;XpPcePv34!x{@hpVf97P z&?{*6a$d2_Z;b*`yaCrvt(-Z-d*XVj7u-%v>>(9v?$!xkfBi(>?prSRtSTLO#p|=P zSXXF&n8)f9 z%WALqU2EKCE@n897jXE1{se(n7OxiC-+pktK5E@by;DxQuaYW`37&nFwd|qlje>yw zRu|F8qW^ceV;&0qkyjTv{o1qo{>1$cB>k7P{Cc!wxn9zl7cUdq{?EwNcVWoV*%9c} z8u0w+%C~1`OjxWW|Bz2>`wZ{OZBL397O~m{G}Pw=9gL9rxG9W5VS?4hGtU*R_t-D` zwLbsTYyJIJTTUKsU+PzP>rKAzpJVrgKgD$kEMS$Cw00``bzIr6nW_D?m{6m z^s6z2Z@<#iFB7un)MRCL&Ul!5JjZC4`@h^5*3TD)+VP~EcJ8x0s=O-!en$%T(!%J4x=jtx?C`V@B7T!qN$3fphg+Bc~Yx>D4%wr~?&x$qAh?ZxnRa=HMK8~W2dfu$Rd9ZN^Z3LB^OfG(Im-$Z zG>5;J=#-;vPnS4Xt; z4;+1R;C<)nn-Ve|EnUhz%g?H`1thRu$zN=d#u&85&WxW!!b9SLrqARfNnK1cN;8WV zp0lYqJ;`a+52rH%D;7-Cn^T#7|9-^2`u~2VMlpQZ61);;nfzP61uc5?X>U>Yv=w@x z?T3W}3{QNlaOi#MYTJ6O??J?b3ak0&k3Zfhy*D$+lRr}5c4KpIH2boh2}js||FB6r zB%N$%xZ2OB@s`JCb?Ge})1z8WE^+R}XRuDXSUT1R1VH|K4!(5Cvk?_Q@xo88XZy!OV8cYEsAXP&yd@8-Le zO1m%eTwOAC!WEW@mY?1|d0f5iY0cM1Gb>+BoRX+8-F|NS;`0JIj+{CRgziUhp3!|L z?_)J}{-OTuA0`Tak#)RmQ}L+Jrs&^?mxac*X18~pF+Xeezx{LGpXc|}UAcoAtAc)> zah}h~UFaG*+t>7^!U0zw&s$wbmwtR?s_5!_VeRYw2M1R>o>KbWs9$N>Q&WC|`EB=v z#0jiPGsF}>91V(I92s6Ct{=`R8L(lievZbQ8+pBRFD9;epY1HZrC_GsAXA)wF|%RQni*4HI@Mxw=A zMP0yYS4_qED_U22E>FsoPT9GNMJ;BEcGi)NE@AUe*KgbVZk@EqwwX+}E41^!t;=qY zdi~WPFzL3?8qeJ)-a6+$mFieCg>lCQO_QeU62TW$U9Z_4aXmcU?a|rwe>hgTIc?|a zQ9fujX= zLGHqrl#=*O6$!F!$L@XUjL=%v@RP4$o>=A8DWQ?i?Nm9W+h4ApsiOMlxLB)1o57OZ zcgrs46y!}gRUNbXZCQ0~>E^q+8>Y__v)g+A|HsQ;KF&8&eR;8L|9#Wz)g~HZN5amz zt&qz6eXU+uWX^C`sair1|hXQaE%EM1xW z=Enxc^>=yIj?8g9UK(D=?a%Z7ZP`wH&yT_XLT2dH*UkNTOrrYNKAi_ej?VwA+WQ3_ zT3lkD%=6t=dxiMqT%IF8jukE0W~k)qyf-V4S2F(o-W~gEKR>#8-Te34*tqM=f+jPA z>{12KbM2K&a_P7AZ|uI#`MV@6E8QrXiQh47&4L+ycUY{~%uAcQQsGAH3=^gaB_EvX zet%@oY1=NrV{!k<(`BCKE2^Ekme}%pwQN3c%ERQ$#TieSg&wZTzM=Ns=vdTe(aWx@ zhV2xb;^e2AB3{I=Zh@reRcfCO_S#pGnek!m6auXx9azvu4lbviA>jw)lco5 zQTZY$#YcqyM8M(D!VB^reba>XbEh3Ni@sWy8!w`IVcsvlc=<&PkuODN^;`)GknLI$ z6!$D>rqkA#sO!^i=zeGt`MBfx)eVezMzy0y=f8@6#Yj+3yH~c>Lc93nsR4?rr3snW;Z^+a<-SzB- zS>R5o_}i}Gy7^z0x^VaJun-BjF}?OPV_oLSvIFdAzlCp|yI|7IKdZhOUEGzK=O3)` zBt2F{FI^RriXwO}lKl);x4x)n=dIw`E6H-+t1qzyIXy--ln? z@G76ll2TJ}iMju3nRTkx<5fDT3)aWK&D&q``One6U;pXde|LP#t6dMv`;Y(pp8xrm zh|1NCS6piBrgNKlR=PS1?J#S7!Q#j;ttxh6^Y;rgu_;R{m^2^;;L z7^Ams*R9p}qY|%42HuF>n#iiQ zPsDvgXIPj=?FF`txu2iC*5CL0*;W1hyZ(L(Yn&bYDER!5%72$S=JDEp36+GV!!eZG%RuROfcX?y6vs>U754w&BgwRFSz4cYnJw|ibH@F}aTo>N!WULW_u ztj)Kk;yv6p1)9$=^ zU^%1i)7!mD>{|jRCGagW>ML?qyPZ63L0(T<((9*~ zF>JhXIVkJsBpw61`Jux1PpD=*b}-y;!k4i?F+(F|SfVYO{@9-T03X-P{dd(7>2qFX0-);sy>@BiyR z{cui%$69vho`!lh=8R?wabqcE4z1hv`D@x4=Dq*;$i`=K=7wvVrR|ghI4^15cTk$D zlo6}BspzJ$vGx`reX|+f8-Df+RclAC;d$07Y09R+^vPiNiV0Q1-ErGGk~U=W2P>r- zX*|=J@BKkHts^#bhg8dHhUtr>42%rgbp>9}Xe%(feQdMOs#PmG1KYnmKK|*q{JpvU z?>p6MZ=HF=+}a%T%88>@)^VBiv%Fb-hfD0XCf9!}WxZUHvwL~_;er|#qqCU}BDP03 z8C+bBF5$ZswfEGV9hN)ZFbKaodiq_B^s4OeBRtZotFG*x_o4Eoj+YF_mAe}&JHD)9 z)iw{en4_MyrDf`d?k4dAw|Y87rTQu+u39~RhZ+xufslWC=p3E{D<(7OcUg_etx$lDx3C=cHNtGaEM^x#e9}{$D?T z)ziZdU)EUEznb*sS^S=zT3hGowR;F}T;Q&=Jn7O@X8-rcJnDKTJ(f&Z{lrMoM3GbN z0RMFd;s0x$(*$ZZa2z_!f52(Op0`??4kq+OEU5FyNLsh0`?cVQU#9C0>~z}a`no=F zk&!}y|LGMvs*y8uPc2eYO(;k&w~D@LFl(Jqa=NaQyrA|k`H8Mw@iIvVPf9=Qxw&=i zmPmEyl)c&)wDn?b=PUh7Xnv6_@#|qt-a%ID{pXID|5#_d&QQnQd8^H?S)|)LA+g&BN=NG7nh)mKn+xtb}gyO1OM|u|4ZD!LuH}%+#M*UAm_;e2$ z>OZd0SZ062OstPbaKk1?rRyTT(^>Spwp~hmm@s8)+11;kb=$5x8E0u^SFt6QtTGPg_=YG~b_HHlUwCwG=n{UiLqrS^lX2$CTcdki4x^CV2ZMT=* z3lHDT|9-jr{r?~B{~eaM`*YboNL~y=S|#(cG8mQ{(n)Dw;3a zer>}>v6?qqY}krec1m#MmmX#Lvqw!Ka*@Sa8!L59uF&O~mio^ZMR^?OsP|aUdCnrw zO`$|w;iaFejm)InN9>yCV|FFAEy>`W@aND6M_=aFm*1Wpovpu5`Sks3Kd*gQJHz;a z=B>|%Uv8}bbK~E~_Hudq+N1@`ZtmH$Kc68t<`#eK@0Ip@=gZg6ntzY)`1ix#|Nl9@ zJ^y|auTxg;Y?j1ctB(;%^SqNEOlgi2aMd#vI#=vc z_Nh6B{dIzKleqf=o3wum?)IFx+rFkEgR3KL&HBBU-y6+n)=uj3 z-f)lIdwHs-pVT3N2`vmW{G7TjE9R{JBktQ(Bj!A{@~Euh6+4BCQJwot@?5ogPj@M) zyWAEqQmb6!F6j5*x}v66!$jA0rJuF0<@}L33rl=HDyR<(F+by>0%py9?*~9cNa& z!x|pa(->&B=ATLJ#B=)le?Pka=f|R||M?pn7ro_Q6BO3lkjtW z^iCfBbihT`b-^{hHy(FFO?a%sr(L*RCC&b6VZITI)e6R-aASFygn1b!`k0R#&y!iF z7$3|KQ&;`n$M0gfit&dpn^VL@89F6nnpA^Wnj{XKGz#K9BycD-*DEYNe0Ns#|DyWe zmp;2aHHfjzEjFprJGI8XrsDs*!^$#^Y&{pwJ{EM+`)M=p_Rp7JGH2|Qf4-gPb*ysY z@)(^XY-^uQW#IkK-D4*n!1qDEi~ZKSghxF&487MF-rcNedMD(t(L-WQ&Cc9Q`XBBE zS8Khhxt#N*_Xk8`TGmifk%}@)9CN|6B^5@p0z5GRBQL%hVV? zn!7zcF5S++xbwi3p5W@z((jL|?6>u&Oqy>eXaBFlV&7}Ix)=BUKm79U_i_3EKmL7u zcp=B&-RZo|zkhu_t@ZU_)Y|QPdjDSe>hSGCUB&-}uXpY06W5sdKkV7&4%IOKFN}Im zrk)gRzOsDd6^9P4MLkiIC;YykS*LJu%I-y{173-|G5W^c@4Bcrz}YoNd$+Kfn!%b5 ziDm)&`u}^*D4ncj6P)?I=*@BcxR}4I{$2YqdH&OhU>dT)CK6dtI3*^8sge zlWB3U+H=p|7VcQxdT_ghFFP~SDjU^L!E=%f&!)E>y)=clE>F_xgoT*tu@l_i(s>R( zlD_+PVU_n2rSGTgk9gW~-#pLPW_E#b;_3#s(AhS({XaM|U$B}uY0L58>UaGTm;dcH zpS?crP5swr|E9;=)&KqV?Af!t{X4U2Y?`{AT;qC-^yZ&fS}>Kf^ZkdHFI}EIvU=CD zPRKJWtMKFY$D4k>`d0s|AoS0N`@dxvpNdKznIj-8(e$AN-YTQ+g8C`}jt9xg+a#}lRGeXV&O@&H(w>{s zGR^ncittw!$?X1kqjdS@pDW*T2%O%@crwpQy>{YUKl$I^{h#~){W4*hzvr(f**88e z-KD(#(S%bCiyp0caanXlA_E79*94X@Zmkb4PxKQmgbIc{x+8q06} zN8IpdI}%dZ`OuNAQ}Lq4v74n#`NGpU;|R25xZVch_E4 z(yGS6TOpF8b6l}B@V!M-EUzfnS$?(uF?w$}J8tydIqGU?BD12leOF)qguP5Lw*rox z-tDaOrOoy8CBMs0C(X6duGMifar?XH|DxA23MYgw91WimVfXyIzv)KVwC+VREruVQ zRqWoBZ;#(ov2|@n0>{}8J!iHpNoC(8YjtC8qZsGQU1hgT^yBt^SjMVax{_mM;6E#| zg3YYFYiruob7EITHMWa0Ww;1@YiDL!eX&EhZQccG_B#)%R!vwm$#wG0*6s@mY%!K6 z0ur}g->r4&^2+7H>@zv75~3F>ehU$Cp7A4Y?lhG}70z?HPu2gt+Wr6C{$FqP_t$)! zoxlIryVvIN`}XYLeJdt+Q|-FN?G3#NU_#K_^rFFNb%s=OzzrS;;*|Gz5 zR_jAb~$)cFT_?7d)hnNGQIURg~E&->vGG@L|R2iLA~17DqZioL)Mm z!D-QB`Mg<=>Sq6YcKWaW{=eUTe(j#F|L)zp_xbDc%x~{nx9Qf^gFF4y2R zkam%lxV&S|nQAVRdajLq8g_bTH!c+VAE_W+^_K6JpYqO!P4jv~-g?J>TjH7HDdjTx ztb^S*NiXN4+y4|r+ziN!_TSNPL~YFGOeQWk98PYrLIC3hT|D5QRk>xAn%bR_7eBf(zlU!h#%W3GO zoV)qh+8yki;;u>yj8AH^oOmG}@vARZe6C7<%(d|F1UI&gm-1B?vd#_k__wBw@tQo_ z6ayK?#92Sz{jR$l6Mz4nQPKTfFHW)DVJ;o$*ti`R)S=T31S39bcSLoZ^!f8=G+Q;m+pOXPl-zT^l+7 zZ`Y=i%MvBZqVs~%HGa&Tm%zJI;nL4FxvUFCgERIOIiC4BJ>%HUkWK9(bC18Q|E$07 z*s6a|*Vor>-LLgZH%qxZ;7p;+@_Ux_^?YM~diwxd2B=Vfh(TP2V*xa5&s}Rn26@p>1s%R{NS(hO|VnFJ?~N_WY1| z=#JLu7k4P%_y79(Hvj*t+2Z>9&zkRU>Dza6{=z*{jgcGX zr#{u*^x?A#$84pWS3j@wTleb8vsHh7J$*Mj{QLcVH6OpuzWwN?`0{l(mej_HIa_^i zSM=s{V(`i0E|d|OkR~|KW6sydQL9&rZQP=@Gej=2BW=}!z*#$(%voX$7ppBwc@@51 z{7M+B$)G%9vYN}GEis1^qja-7>?hXmB zIxcbe9h@b|dA`&B(N%4;SZg0hyy7@>DfXDuj;xBCPHR@5l}kxB4Upz4 z`d}8Y_@?WojWe=sH-C9NaffKpZ-Ln9XAYj&7B@LVEVIDStzv0U%icBx=ADh@Ps3cp zE|*F4F=@oWvsA4TDZ+qW={Jgq9FE6*3`}fP=uGCt!>R!&p7aP~S+P(XA?c?C{ zR^cx?8YWEtSu*R_w)Ul|FTLG1-VmG}%yF4z;sGzU6N~0ZI86>@lnRXSQ{#NzYSiW8 zX1eDB@5>KLjE9(eR_^%l+;^#-+2sY+vn5-@B5o|OmVRw9WvZsxfjdus-v9Tk`+nW` zqn~fT-nDY2f$qU`sSF0PeC>1dFYMe?BVv@O&YdElR2^rX=qJAT^{ZD|^~J@#{gu~3 z&X*kxUTxoH;T7JeWo#f85O>-)$mF^zL-Hr7_AS}g1>XahI1gG~+tSdyJ|b4?dci}hVgj4^db%b&+!o0D&27Q13!2dzSWjCp zv@C4(niY5T=>0#Ro?ZQ0yZ7$GMjp{u&u{1kI#%Dy)4HK}K#<|n!q&9&odOxHcRsVT zIo!VTTtzkFzH@(?-o0sWU)@=y@Zf0MoG9-TZ7fBS22PG04EG%@dBToN7UVhbSL0&m z%9|XQW-eD^iz^aJyHa-HSeel%pTuz7lm`-XZyaTu%}M!+jqEUqK5c(7B)pW5tBpSEL=ju*Z-L=j$?VJzGk`m zX^xv2I_pGc>dS8M6FM+);dYZ5E*bZ>1o!XBH`~6%fOX%+?)7%M`x>V05KU#VKghQI z!LBct{j`6pNqu@#bnl#+<9pM*ss68xT3GmBnonL7=Eo>iSMp_TQksb<(-QNmLaTbu zWlHm{efT^oWzy>dEI#vgG1?yTe<3Y%WRlV3@ajwpyYiIgM{aF5ts3ms8eG=!ygxrQ z-ZNM2sK5ojJ51sikJugmxu+wu@!p%O6LkZaY96~!+*G+i*VALs`t|enRut}Dy;nR& z@q%H(ADIQ#)%CUlX0un#=S$tVbNBAsVH0GR?-cmPIAfQ=^5@C^Z?5OedK(rwVOvvM z(m@spCiYn~CNMu>oW0ku=-b`C9aaycx&w~&pV0hvsh7)uwXouKr^SRh)6A~1oSI}} zANOCJ)y+Vkp?y-s0@>T;zd!B&xq5w^?d@b`j~f$h?^^Zg>UZ3+ul@Dq&re(3hlipP zgqx4B9d+^8%pj7|adewr*Y)jJ&)WSju-g0kWrU*uPyYkeRMxLddFOpsXRctLX1MOg z`CojurCQ}ZSrik6@Ba~W3BF?4DJIwPYSY1Ovt}t?oT2|jK>s;!j-GW(;3GVch=akpXLgj!ITopP|9t~C982lv*70G(pbOJpCvQ;{GXq6pWWlN>X+;z(E_J+ zrPd;>)z2EG)8?6V3O3c;;(9#)!S-iY&swY(`cisah+*9Y zuSC!Cg<_Xh{@(IF>f=v`HJlH&&HD4~8{Z|}V)O4R0y|QD6s8nuNIc0s8Q`;d>HUa_ zJ>L}jX7R6$I`iP9?u*x^EH}5TytGix>D@w|9jALpl)`Iq)7 zh1EQ_`zCV7=arHM3x7Jd`;7}RHw|7gtK1? zjtkKc$(m0$GEM!D>PO$D+1)Q2+OHTb`0z$uc)Ju|$NGZBZf81kkM0ntxu`m$Tyvg& zfLd$q+J@98d@OAiJmMxbVcwJW*8P2!|L<9T-2Q(}A>R-GE`Mw>VP}`M!3-0LhxhWj z{{Pw@zqh9F?v<_k?lO2fvn^Y2zr12!>So2i#dl@gda@H1KHRXOaT6Ou=JV6}dp^Ci znWQf`C->x?d-YZvYx*ahEZ?mr$EsMo#i>N(TdRn=k4od8uB9(n-aD{&J?<;HB;;Yw#4^FQR$w{H=7=n+D&0zy0#+s`%M&q6fd)9N~TO z@=|r3q`A&)$B$pP{rEQf`R&!|_gt56lWfv5>hwF&ntXNE0r9K{xs?U!w=;n8&SAS*qJGnF>+Lz+ z7!ZB?l*tA=F~%j!zY1J?uFJCfjf9|32v6;Dwg`P=?j8SMA76B%F``9l{_928-~+Me7}vhENZJy`y`M!m%2gp$HGOTw*1fm3SU-@QziTJ2wX6#7 zM2BTJKl$XX*!WMrdEL6|M;3>kE@);b;0WSEE1ufilK!H9R?vdD7X*V7i= zeE0a=de(i)LfYD4jmwl%?|yB3y!rX2m(KqGjN%0(MfGfy5KUaZ54W;Fhs}ROn0PwHa#}kN<4vJ5hM^;g3H*D>^%JSSxv_rP{Wo z?B4(T)79>ZwKG2TELrz{QgeH(`wyS?r0vQoo~u?>{`?~vDE;|Bym8QdhAk@#cdGsr z3|M+rO|ELS?C}k&Y7>85w79`|Eqm2!4W*fEH($g)k$0SV;6O7M8&6`{u_>>THm&kX z=(SBNdw)mw)hY95_I7svUR~c`_4U)!+w<$@q%&(|D6CX03Egw&YQ&VvFOvV9?YF$W zvMsKp#7ZWkxVDy+dxD8oPhg8{#uM&Sz2(==sb|`Mn<4bUa{li`AA=YEcXvGL35bvV zcyrpuds5q+#llP*OOHq#SsHqGZuC;cf|Pv*1tKf92zmLj7Je}IVd8bW9({5<+oCI- zA{WFXriu!6FHEf2#h@PlapE0=z)yk?EA_5~eGZb5dzPKffq zZgg@w(>>iCy}|bLZ~eQ`x0gS}V&1KlQEjVNA1gRL;i&XS#Xhy0tPcWYl=x+VKMJJm?6wwHAK1`R%D3^K&YdY6R^Q6ulCGPO`dUTaSD+wSU~TTZPd8fa zE{YX8&FV;*AhMCY$zWLrM?r)6?XHq5$JXEYB>UyLepvMt@z*vzdpzxb3bB5V&M!1C z{r5=nB1ct&==Zsjal5_tiu+DD&N4gJo{{NAuy|_jheLXPZ1MiFC&X5?FeF-^ey!V? z5Vy2=;ygRv)3;6TAGoD@`+IQi@0^%(e>3f}P8^=K)2CVB3)`uQ;gj^sK3v~(uQV&F zu{nFS&!d(ZA&n+wt%VDlt{gC1z;i3egki$%>W00%Cp)t48Fh0J_zv_@>7>#EJXQ_M=Dgtb?(DsB-Kyr|B|qFd5q!dKj3l;m_MC;31qdyT-d zy*HmdjOpy`oy2s1Ls;Gg2gf4rU&3i0Ggs|V40B3(GynGm-bMMX?lsewef(l&>onO} zndPc;&S&2KzV_tmR~(Y<6HlC)DRD~T_{*7*DNZ{j7^f}R`?`{+zffWRW%2JMzFG<2`+{J!JM z4?q4`WAn8=xZLh*?Ctrr+eHnNL(Pq5^&H=M_ADdo34yrSM(v(c>*YkAHf-|YoTIeh zf%Kt^uD4EAskXn{!pPdzz_(@MdC$13m6s1ZefUzay?dke|5wxZ@7Q%~zMud3_`KP< zyKamBUVZj!>E_%|Sr*%$o5i1M{PF*WoSc2_uOA<4r23a1F56xC^V3tWpHr{Iipv?k z`*i5om)htPGUIWV?Nb>XNyE?t5ou zi{~Uv7E4N@G3w|`rz(xdq4@UIygoTW01ma6X4Zz}FQW}eC_ zxS{jt^ey4nHq0u>_q?_I^I6|(YT>6pUVi&k?5zH!Mhj8C{LN2yDOsOf|7oM6gX8|U z_O^0;&eP`}uf3a__ioQ4uC^mzqcxAoG^Cz8~_ib(@ z1M6$2HH@hpvNPwOo4|RhopWL4h8y0~)fUOc&)lLKTD4ZLvp(`qUa1*RU^*z1FDn#Olv$ z{%i4`Q>)|o7N6!_+qPO}#_3DDxhDVHB6Du;@|RyO9#nd{=&+FJ88N@!I5Elg$ZLC) zgf~oh;b+LG@yqR4Qyk-BGpi0)@%FCl-5VHQTRc+qDh-~rdx!5-wu#3KyYIhln>uf! zwz5w%$CABqHY!su8TrT7Oi4+cxbdjQyy#WVA+ zPQ0O*a3E*t#TCf{Z+GgpFuhN>dH>(Cd)F>cKmYo*@2^L@_S}1r++F|gLfqT+=C^-U zR8)Nb^78S+ma7#NpC%pOTqexX;q^&7D!n*1H~)M1%^P)D@^v*ezrHN||KsJ44;4SY zd@Ql66lU=|^mU=?$Gb{#PUqO1I9vWGH|Xt8(qa`ksPb**#?-fL>hQFE z?)D}8NcI}Dh{{K2r|uD8`Q3Va*Uo*_6*UT1tmgXh^Uq(me*OPf`v1Gj^Y6>fvlGbp za(Tzu<^SL2?^}2I;dcG?{~lHTt(qMddh>nVpO5$dyp!Ko^Y`fV{CzG_iFS!PfBH7m z#x}Zt`E>95l({F%IfEz2cywp}d+a~I?tlCH|3Ci4ZrZ}|>svv~p;Nj{y$R_rcfRHH z@u~^$sCdABqD9L2278v%q9v!ab|}bgJM240sa9#5!jJX6_mZYEXipHzG1fXLVrQnI zHs|65-X6O<`6s2t`JWkZ{r`6M-^ZV`*B?(g{lH`HHm9{8*NQEYOc(iRbMnz< z%^U5v+v|QjJp6IFJb$~tedxM)jnR_R=MoW?!ycJUzx^# z_`|-{_H!ol*M2wolNh(I%v}H7x>+f9Wyk&f|6X4H?SspO!kQl?RIN0Uj4Uf<OrAk|yn(m_OG}1z31ba^2GE-IQCvUKV8B_x$ZQt|PaL<=3%F zo{QU4!5aJjul=9P-LYS1?|wb~_pen4Nd8~G{Bi$$x%cz$-g&eAS!)s3P6zgL zJd!un7M!u$P<*89f#r#_o^z~wqO8^I|NTfd`+oj=>9lb9xpKcM_nFKI zx89Mp?S7w!a^|8)!??-Z3-|kNJ9j)}lZ3~$fa(33&t|=B=PiHb5%GDoIPc?$_CCjd zS|oT1rru~1Nqh43ai+^OWs5YqT}sO<9FKM0xG-fR%hakKMdzfd3&(1V{jL8bOq4s# z`|_Td=lo>JYWbZvRS)wY-*JUEQ#W`RvMX z3!bYOyo}&@EFOHbM(f1s4mZ_BZASu}lzDX*7)^EEd3(Z?YDqz}-KuL|hg$CBSuM!0 zhFjS^OfG2oJtp>R!VydK?`!Y8p3A+3FYeU2AIrQpxpc4CAu?l*cqp%{v);91)BZm- zyZ^dmW3?q$^fjqdx75~bGoQCzcjc3bHM3PjavD6$MJ-xvZUp8%>wa&~YU2OdY4Z7^ zGq84zqsV_PZNP12Y>wy?Tug2mA3c!RE4$+@&YmC#-aXfDN1n>uaytiTo5>P8=CGSO>J zrgd-mwZQn+gnqmI^_At{YaV-Le|`3E-3^uV+pVA33cvL`?QFi!_PqQ&w+;6M6Qul= z1r#p%_Nko>$+)X(bno%4Q=631a(0L9)IGI5zrIPJ{P(i?-S+d&H!MD6*dp-IQ9Sg( zRQVaJHH9u0D&>W8@H}jo7g@aaq^q5)jmW((dJ_Geb57q{Fm-}dWyEi@iYaT3%wU%jf2F`_A4WxvI5b=eWAoNjsLHBqnpl(xJU_JGE_1x+{E(JAig)p z>9O4NO&5B^qEqK+h{j%3>ihQYpf~#?DV6KNDf>=sy%V-2=y;9unV*N<&#YA8@6w#2 zB-Aqb+$^~dvC0WNo5dZRXDdjxT4{coc}VecV;AElkHQ1z(;2sIJQzPNf~-#+6xF`;Rta}rd{?wDBK z*XUe0U95f1{q4yo7#4iivTwUK&F^(`rAi9J+Ju`6PjB|0aZRZ1NYof8%mklIe|@#W_(7#(xg=otSXm%tXpr-2YMFd)^yw z4A*$>aPszQX_veGZsEb=7*t_puuK5P$N$yQ8U(UwfZWOuf!M>hN)$N;_)em!CPiwyS z_x9Cp%Qnz`|7&5ds#K}BLhOF!5S1Ni<-S!;*SGfnt=ys0!)m#pTT5zoUgSxAkqbV@ zCd9>Yo|4VnYj?#;Ny%!3q@mvJi$azG=O2r(r8K)u<=^OX`9#*nxXhr+nX!A%ezx6N zzxDs7bNl{1`gK!0{*Q}L$(!@X4-1EVy|d4@)PC-yg=g1oD%-5;dD}4fhK%^38RC}5 zE@i3DxG2uv;~}BdEj2-E-m<0kJiD3nr|h>s*YBPpJJoT*iFq8%>@!Zpdo;0L?QAtP z+E7|~f&2G{wKt-C7^L4NrPoRq9+`5i;RM^o+0P_|b0n6pSipa(wJ>7Oq-M!80gQ57 zUMsvCw^Yqq@H?GfbYtidTMm#CQ%{*h;F(Gv8QQ>&2cauyQXC=;Bz54a9!s#;QFDHEE&#j;35o(o? z9;b2jaLmr^H#2ln%~e|B4)7m1wtPxuSAwhhljhUPK4D5G!i^nkYvVF4w644R=3ew@ zxcvH(p1$^-CndY5tlwT)^hVz9i+cgrSMG!RL^?$eiMt+Xnxt*3`s7wmUv^{m#@%j` zW$qKN{M>Nv)S+FehS|p*f~G7z%_j8da_dFsvYQ2|hbBnp)CaF;3qTag1 z9dA5dy^S?5otk4l z*OZ6JrfAzY{!E@E^YcOC>Q$>(EnJl*;~SS#_WSz!c?YF(?WY8;Ios!TDe%4eyX2)7 zstfPrmEHdP?egQ@{QdLo?B?3-v9I~_qol&h)>c;ekZk$w*qFTy+Z?KnFsxiR#VmYD zLfHq=Z``|6+GZUwEs8zKd9NX*pjTf>=;_4;F8>0XL?`#NZ3vs+DCl0;RC_5T+gLyJ z+MB$x`+k2vw{M>@>1?-neN9!k%z0<)8k_I$&wcLq|12+mE6vije%sESJE!S;zBk$z zE+N%_()0>@uDkFhrhmK_*tVOV@$bKTn_qrHqi>*s(sNbC(`;Tp_PJU8(3o|RKWCxR zhQn%876S=$4KYLA{weSbsnT|vUyooS!X=IiM&ytw^!MxkuD z9ba($%dRAAnOFB!PJVs7(!xIP{+`Mg+m=44%fEMTswv*@ z+VG_pZS)>YTg;Uk8I*1O$P-9Eg5`#0)zjEi~U_|6m*5VXWbF^=9A@O zF;B1fZP4Fdu}QCaqJTSN`8n?ovxTfqaXPwQ`~53={-x_>+8>Oueu<%iCh1Ps`%`l7bm{Ge*0|I zj&)L3=2oiv9{9NP@}gsPc4z-ut(myAl~LG6>d3|``(`Ta(a_tYd{J}rs)=n6*fcI~ z&Hlmm?@f~ULWYeJvp=RZZ}KyewS2wGY0X;C+qYNEh?_Tmd6w$Vm&f-7bX6I0Y-P)T zJXw-o?#&??gP79_TK4_`R3lHZy!=-b2fy_NV8AT z<9@K#K2G(ah^km(y-CE*d7LU|U)?&r@%AbzfpQd_THyw|Gx3}IW&OMI33SAF18$BeHpzx;Um<;#yhpMF$S zR!Pm9zpMNCrWzZ6&yx{i!S}C!efanG?`4M+x1Bq^>4esXqaSP9%DMRj+;Z)i>woz9 zXuqga{gVBa`Dd|b@cf+(zMS^U->5|$wr@BQyL!F8Tf-^A@U@q>>?|&qn7?*!cAYs_ zvRO%hprZFNujhMm?oIn?f9CPo>Fp~$uIJy6`L!_Y#>>|~KV^E@^H$c^?XzA|zWw_4 z>(@U8E?pe3F1zPu7}?%^{5} z){PuphdEs14o)v*y0+dvhsFQQi#I*zpDdcG=u|9non`vgzN7Q+*xhDGU1_+{bIYlm zJrNUoMLXU+aylfDxc$$+YuyXBtG>P-7SZ?G>m~CW`4ddhQ=gPZolw-gRQ>0~#jHtr z->UYcs`O3>FG*u--d&YDWjep)>J9y0zEza2nY(f3FXoQjIsI3hEL&vE6I7nKExe!1 zDS7Pq(FcoiEO_5nWE0gA$ zoQwFH8R-nlM2VmpkYS1EmFJeymW(=Eos8gY0|me0+L%b#w( zw^`-N?MoA1=t=ZsIoZx=3}4Zr+cx#lE%h@@e>IXs#cMwY9}#_^I6G*Aw0`O$@x5p3 zRQG1q*~q=WAHVfrbiAH-xs;ylpR^B|mLe6|LZ`!x8y2ow{r&yBY z%)K-}V(RUtgZS z-)@h=i%oWS95!DRP7Imium5=epV#yMJ?;Ph>-hdZFWdR$_tpHXs+0To>)~bj|9__Q z%h#*%-CCvCy=Ts^<;CAWFK^$i{m5{!#Jg!xKwP_D#$+Ntck(f?QCdw%pf&zyPK;RTQChJ458PEC0Z0m4gV z_$n&n%hqI9%V}{xE)PsE^}BMH|0*b-)pfq#pTEEVivG!_Cr>VF>GBuf;<<4xBv3coaE@N4 zkxt~#k5|;6KYyL1s&QPmx@}{xl7?9qV^mU7D4T2AE#~6;>o^bJJuJ*(YTlxxwC1O7 zW>Uqu@X8tg0|OXbx5yutX504krrFjWOPjAa5^IS@c-1QhizPU z&hkI)aO7A-Z;;H0?Bea}j)w9?i+i#Nyzi5nuw>ac z@%PF~NsA{Oci-c}+puWQm4(3*@+Mua7W=d{%1ODAT`sL+;VP~D>-hgqcpCollLha8 ztA%1AI!ZhDS1Vg3_Wl-p7&u>g-NCjC+SM8#U*`umE)}&d*uiAASL~du{7a(=9rhQ? zzC7r*o%8R;_s=W}i#}fS-?O+>`isoM`GO5XldrvyFI<*c#KnJjld!$s;zgR@%C&SG z+;%+Jud!Fw-NyHbw6nu``fu0?`J=B6H#T`Pa1 zQN>wjWxUy?fC{bXpHe3e9sYeU`t{?}3TL;Z{O@0z(fxFJ`PGlN&7bi0ZLeG0S!nV1 z$K@?su7BnI>S7+CvtM9SG?#VeK{bcX>#{rg!()V$KTptIzS92g{=a+=?dtyh`B3un z?)I~1AO5iEi{JHdQPa!4*JmC+^;hFmd((s8hm5LvTG?~%)E?U(bxJqN?_kBw!u!g0 zKLgV{;*+{hUN_*7_jXe#k$AS~TG^H`5!1Vd5iI(1dCQ7|R~|~f*{;iNt$NkG(_i}a zr>(nIUUJ?XS>STzLe_oZ^Os}v|2ib)cm2FDB{w+rY3~+w?^{Oua|Et3>PQ@)`03-v zr=9aw^c+upvHM}{T-pAgn!0koo<6pme z>y?2F8lLt4`}ZHpp2W>L>0A5Dr4AqOg&W_PAG~&&?xD%tZcAUX%u-wGDbQQzlvB0P z&{f57m#W{Hz!bTAGPAhaJaX2*{!sGo%cqZ#J8LWFT-e?}z42IdWBg6$aGqoDWj?Pn z=r59a#V;iHsbk5TmwAN>@^5s%9Jt7O2PO!UV$nYvGPBEk9`>U6;Ed04H_TRcy$E(>NxqZ!n zQx?T5LQWXg?fJ=J@O+Zp5R& zYZaI3pJA`v#+WN-_0T-}Vo&hj9@p#X|4%MIZq1d=>U!$ysu>$Lx^q~!U#LmYzGZe` zf=KO6S#~RKm0;#Khof1h41xBvV9-oEN;NzbEv*) ze`u(;^vCJ=e_!6q+d2O03;&v#eNXK3+r*tuj<(m;RaBh2Ry8k1?_GD{1OMI9$r6IP zG2&-;-raEj?V?X6Qu%+Z;ufZzWM}JIU%2pk;PDXQ_iK0Eh>G}NEyUTx{!wfkl4dd9;$V)+hs*B-uE@iC2Gz?HvOGX79R z%bauaQQxBZz8se<5Lv;Ot6Q=}%8+G$sKu^l8J(+tYYJMkd2Cp2yKMjezw(nxe9k{N zUdd%IZzR87_@0*_`#TM>xwkh4-I#eVXMIQOHrJm5G3z(fKDYVVe(v_OSFds-m)twa z|2NFj-tJ8N`DsRn+ISDGN{+Z_R$nV_*%Ozu{xQ2TP__-M44Q)C(KW zh&~9JHaYy1@8JiWdrvQ|uKDxp`sH+qq*$4MZ`=Fd=e(@G;s}gd{?=Yo#oL^pXdiur^m(NBuw$AFI&&^iNTIHbrZR_q6 z-`DH+oAW&kV4G|Bu=pz19Bxma1Ba&lZ@5t?(;MZ|L*yh zUGL`~OS9|YEPea1KxdYiy^CdK*yVlMA}7)&d`h(9znomJH|ca$=-(fSyS?p~UBAxz zcgorJ<6mAr=Rci##Of5kCYSxuikcq7?#+Vo8Cpxe-&ypp?XcwR6MOb+q<-6fqU+`V zY4bwPt_x@O`&zZa{djcz@47cTzf0XNQNQgG6X|bXEo}BEFvCYA?Bf3ahwTqU2kVFV zs44pITX3jiyVd!}fv;viUJz0(eXH$Q#LvqRwpiV;o!Zx%s%@+Tn#tk>l7=btuMaRUtc?8V>`Qq=l(sq`Me10;kd{@YSlU)J1rm^#M8z9@Xh@DO1iJorPnS9 zekq#Qx>RuDC7W+Im#ZC~opJh|%R||(H)dX&Fz`qcvz28I z)c7y`%FH`fZzRdJ>qEokhd)Q!TB-tjAbYXtl z;Q|eR>B>U}+dd~h(9qm}HG0k0*zLPk?)!VU_NIn@)!vh<|G)mefB*iyJ9!RQ&h5%t zy==|OS&u(zwfCmD-rle}Yw3jf%M-u${JK#3xcz1JwP&G)fBOIb{$KU?S(o#zRZI8F z*ZuhU|HpkhpXp~`uARWU_x<5Ckv$6^AMjn8eEI&pBiZ%m8}}rtcc|uPIL~;yQ^us#HT$1FHe36dxhKzRyZPJw_De56 zUH@X`s#819*xRd3UmyRu?$F7Nt;vC&6-w~p;&1JH?a#hXk3UhA<7EGF`mdY)u8P&nTAaEqZ|A(Lv8`s=_RsF* zu`Ta+Dc+cVc5{Emp^w+5xovyj`?BI=r}N{jiyap1fBbxY4)67+Kh{L8SM>V&@b{|^ zcYUv^-+g)d$IjdHmiGPL@JIB+_Wncr99Yi2s%dt*J>R#D~o0y0NkG)>JP+&YN!p1Gl+9 zJbXVQuDc_L*~p1kqu}J3j-?aV^eC5l`@A~u6m~_dXwL~lL)Tmj*XfsZW=>md6c>2& z(PkM#yVAYUKj-}H5PTfd{><8K#uIB7ue7h*uBT0$uxx@^k#uiH$dm9VFZ^;{1)pzM zv#UP##_v|qm)8|9W-SYKW|?Z&rXrW2t~gD9r@`TW|Car&|KI=r{OjGb;=d4gv6`omBGNI>mu#(21(8nLUP6w5L|6ZNqQ|n_QmdyR6_5O|( z`;@Ak-rDW;KU1I~(*MY5PezmN*~gcn9kR|ln`E57*qr#edz*vjENu?{)r_*j+f^Ie z-`!Yz_uBvK^`*P_{XXn$&TC)y?_F$A++81+Py4Zk1a^;&Vs?M03vMjcGcQ$_ zedyoP{Ctfhhls=JMcRjgl|KhXa;$pIUiUHf#;x~Xaz8f8hOE+Ox$bUxPPT&6)lXyv zH(v{v#mQx1HWTKR`tzCBSlqMy|B-+H?MsD=Yd#vTuRi&_e52K>6RQ^KKDgMmcl*Kc zY0`f<8aFO3WzYY!UUS6}NynMnLW9(^?VsMi`h9=x?se0rpXWb%zVg?kvlbWY4Bo!v zVk_J7N}z%7dAbwhZ@ph4MS^L2gzF#22tC@v=E%}2C!jz1g#LOBg&UI-6BXU%n%{=<7}>u>+5|F`^qUe>1b^82=({_=W%_4O^=pX{BZ^m6I`nz}!$Tjw|D z-Tw9KS4DaCYQN_{AAT~bkZyUi{PgAJ)7{s2x0dW%Q~z^2|MT_AN-ZUqFZF?={hy$$2$fc5e8YIO|{g|6j9@m)BR+)!FRXv!_1XJUoBW zn#@gmG*7*KKD|CR&LJ&-)%0M6XV=4zueI^}T=P=#`|`wB@s}Q@zG43~!PC0-m(kht zJB}>Xu&HNw@BK*VxsL2+CO4k#o~JJD_NrK}q2s+f!)2Ox=4QheGs>45=62MYt`XyX zKgptl)1@t$OV?%Jx22z&zdu;|!zbskV&?4!T*(*2wL&Loe#(4wnTq<{{Bbh)CRGiD%-UtId2SmZKLKJ zDR=LBcJ0oZsLTF~!}k~~e7n+F@^Z$U&#w;5oqW-h=}5WYXTb{VpXs;XRdtByw(O2* z*;nPyu{cX&*Z(!sb7lKK$LKqX`mXu0Ed0`&^|khQ*)JT*eo-yIcVFux0|h^a+se-zfre{Ytq zeSZ4e^!lGKel1y1`MvP4{M7^civu(^?`vo|){v<`rNrh<&r;c`r*D=P-ScP3_p$gC zboAej=^F*lT7Br?`(w`}y|yaFV#2KlU(Ew+gm=GppZV~nXb$gErmefVAI@BIa?71* z|DTy(m!BPP`{%>u-&UjX7Vl}>pY-7Qzx$W$G=n^}r4G%@otysEv@2rIw%yg1 z-}O(IoMW3}9A{M}qwmeM-F&x3Xl9e1LicU6C3b&IZZBJzvv=>6qR3gZ9Cw~rs5^P- ziQ{!S3o{DK3tlfxun@0*KH-N(XrGMrbx z*qwT?_w1X9!}GSJa_szZCG6nV?cej~>Ls^)&JGlBe5JKKLO|^H0okiBrHr%E%#>5# zq*bpE$o#*1X+iUh&@`7XAyeza)9(jG>3vR}y>G%}g|&=ge=0g2PcXA^FaMEsqWrhz%g&j{J#!0?dQ~%^m6+-v0ZAs|GCUxK5SRsQR|S+tw$H0yREfW z`aJXX!bsQ9UsI%4#j3P*Cz;;aB7d!Kga77_dw71uUn)KPruU#xzJO!$CnFh=i4$_U z9@H%r-r;g1@C&E@*CQo=D#GTxExr3Lyky5R)9-u?8|KfQdSs(WmFt7C_`3Jg4h67UcA7up+;V@%!u+*jlCJDruHEi`4ry=jde)r$mPdGY$Q(nK6H6F= zGiK=QVdK4_e_woQv0&TGoWA%;YitjPUyVAR+7!3-(IFNilR~}Sb5FWXc4lLmcv>l3 z8u{nh-K)pv@3*P1IrQ;&-XWXA zHu83Lnf7)z>mHwvYo99ns!I3rN297-UhAI6TY7(-z21Jh{j~7CieFZLemsBvF12uJ z^c97Nv%Va2P*yx5u%%B=Y(n}rXKv0-g1het`MlmRm0@}A`lPvn>>qgNt=`%DgyY7A zIE{kyRX-$W>puRlc1G~2FJ4!8EqvF{Do%1@eY=^-t6|ecNj1^si;@l>c_TdUB*Uid zxusFv%jer|v*Xx%>F$ei&+F5dpSFrU^YUcrA#uy8fA&N$UeDnEd6~pOZutjTPKOXzQvd7%~5|oMZqp_e#Ewgr+Gcjf1b5K{mP*@*O(*1 zKZHd%{il@gO6fm*{LB2h|G!OTuYE4IpQ@nI=q$gh_(zEZXJ2pB#AYp>hTs{O-_~k40S&Q3?+;oqOtdY@-I8_9adTl&d%t1x;;zi5_cBZDp2*riKIPGRVcsA2 z#|!=2rH;GoGrGVNE_gO-Zrg#?lUjmKvKO+4H0;^&tZT)Jgt;288zovh*GVet-)s7O z+tt-oRMnM#|8{q_dG6hi+}Y=YZ*Hmi!EH8L4j8{{ky-<_RRMD`TOmf z65hYh=btcrQQW6f-3`wr+hsSs*$yZEEx8+_82tq&{PXDr_<;=*3V?#|lj&3jpdAt8R7Po{8~FM8d!tlB4Oi}hEp zPUqi0Cc1R>NB(>E&v(*Ug+Gf#Dn#ZTEmcx~ z_4uFL*Q(b3xy;`+*Vm+dE%Ljr|GKPbXZJ6ruy4#uToPJ!X7-*_W01Khm8WRu)|};| z{aLRhgWIV`+w-8+@|%hEzBj|}tzWWY)3UOwHh=hR-smH z>4bLo`C_hoM=Vu?UfgBa?#0cORvK~4b@n;=6v?k|f{snS)irCH(FCRgb8SsmHFmOY zUiNRjrc&9IJ2DKOAL5ow_{@3z@OOpsDieKU7TpdrzUuE&}8U>y3y#1 zQ2K%=B@$hBCc8E#7L|YRuPDqZN!ooWuB6ZY)=Y^M<2`S#|FSRZI_D{Rr0LOupyzoi zXEK8ieR?s^FLS#iqc1;IYxF%O# zV{2b^!uUwitl2pdGFe}qN|a3xSjjkN#=V@?$Nn0Y>v-xiq)H!m-@I<_h25bSylNI* z+t6gI)O_!O+SP~@i~U>rOrE!%s{FO==Dy1E;_CYETW7yc`ICF=RZjFRyY)vOKiv|< zu6EaAj*<9<`2L@~U+Xu`Ta_cTt#Ib2qSx0RefbdN@U_WVVNKz3wgP6`+l&?OuGs%O z79Vnl<)~9YpV7k8UUQdkuyD-0HEW5NVEeld%w;7F6Sr?@wqBsEeD_ztq22Z`ga028 zOmKLjE5M|F_LJSa7rm=Dm@{Ulq~5!BV0W}j#>eipFTCUTR)040b!WTo%JZp6&bwIa zzud7ziG8IyE6qFgHPcq=>`_vfV-V3laW}ujq%H9)y!~Sz8au`>`>J?yKBu65%gMd{ zQ4f~1O)hkqx_-K{i5&alX|6MU=c-?{{4nKi_5<;TS>Geq&8hgfQT?fM~Y&Wh~A zRh)mE8QjEQ-JPg@FWtr7e6q6iVy4-)?a5ChwEQxXOf)A<)}A+`pkTy8J5(l+p9TZ(_w+Ev;7_l0BFf_Te>Sog|H z5n9K(7go6D1)Iw(;gRY%EAqfEk$L|X!F9RZC5p?vifY*J&sAsPexJ-P+rZVEAYSm} zVZo8D+^>b*1=nhHcnL4I`I+#c!>8fsJ?As8Hf0|PPbgv7bW^TO%zN9@qt z_Dp$pfA9Xkhu_zQntg3w&%ZtLtH0;9-0YtfJO5w){N>Y^rCz)0e(hQ3Us||3b^pJb zD&ySv^|$(6i>mgW`CC(4{`KX@ryoDM8grLk%G{)-b@^P>d2wehcQ+=bJ{ zW;~IG`Nd~a8M!1XYEDi%zUZa?WsB1A75h$Acbh%R-XA|P-0&k?^@IO2E-D*u>R6Su ztgjN5Dci5;*>OWvNg&q!itmM*TOF0UcU6Fu8^9-usT`{$xKO{R#;nVLCA z6=giQ1v9oNY%ws|BJ@c~2ajo3fYxHU!6g-`L-tv}|zo=H51+(%}wqzZa0Q()s zrYJVAy|wzx)A40qe==YQMo;&c~Vzvj#9((EVi zG`VQ0)%HL|w1HVtkcru<^ml`YUtQv{Jn#|QuLrBvfB#gN zTGO%kq)p->_sR+8XWTpOcp9RZ-X77|lVPQ`r&MNz#z_;gmy3<(^W2OLR88a%lbGbN z_H(30>wW8UhILcaJ|2<(n6Cai=77VE3vJt99PhLCP7U|`t~bl+mA1+1`~?<&d+K)u zzHz)4#JtDpX+(>C&C~50nR#1}v%Y;Q`{^Sab1NUS5|dA%h*0SSsm80!d)W8*d+gc2 z>V1CSkK5OeFE?5{-RQnw?YDx)$-1GRmO3Wnxa211?NE8`ysd8L^W~pkzWixAclX|FoRpxxR?wYKMVe#N%Z$3yor)bT4ZPdEKm( ztl=Jh?S_ox<3rIhQg5y>{+u}fZeoYU$!^`AZ9<;6s$*|yO%=I$`+x}h78dse!4Sn~EpdJo}vKc`mlT zs!rqDJBITsSFWD@JiQ=uLKRz>(n{qHri($>E%k04O*au$p7_KnzCTD}o%3O4P9_@} z@2+!wv37Fhz8`wHlU@lgV%0ev_@Y3tY2t?uNdf0&^tY@sy5#eu)T|<@O}@(L&YNqO zz0#`sm^=?uoR9tR`PtdI#;lKxthhq?LKYu!2)w%D?qBD_K?eMxItM!Y`))mSsjz0V z4Pw80ww*aJSG=%I<@_@v$Dh;I^Ju-|x8}Atd{PxG$y_B9MWjIvE1yQ&P6AD!_{P~+-6C7o|9A}Nw7;rVJ8E61MFjmu`P zPBJ@Q{XWbrQGaW)t6)9*EpJ{exIB= z;f9~jb@z=Smm@ai2GxBJvt*LAHu7uF{Mc}HgCld(O0FdicaQKc>zv*vx%0~M6@1tJ zvGp%Z3f{nUmVoHZTOlvMP7KWdTiK8}_w}B-SI?)fyZ>yX zrv1yp*9+Ipl2T23ZnHNT23)DR9c>168=g_M8l&& zaPGwdKjkp{sg^%O+EerQ9X_!lY|h6<#eH8g_gKZITL)IHoAy;trE1N@{(T|q{StmZlk9hqS?uvyFT%2h`O@^BUu8kxE^j$| zS=HC?`d@#{xE8VFY}%9p8yTtmoo&mfmCllT^e=n+akFf`=X!rXR7ABK?{%%a z6Mu@?#=`60XQ%R8u_2Gz6L$qpf5P;A9OZi{1IpL?98Wv=w{Ia~(a z$z z+#JSbi8|kkj$81$#(ZeMR#3TK_qqJedAo|eCNGN+&;PkgG&fAQ*jynw`ugRXnQUE_ z8?LfH@Xp}Wi%Mx&|7$_4so(d+Mvo(mO(%{es2cTYF1mmBr;23}^Si_Aey{kcyCry` zrPty9yPS@d7Qd!DeV%T{5Z2~ky?fEyR^<&UeGyL{Z)8q4f`C4#)3DH&0x ze`Z`TcKuPb!5Kjo+vYxp0!`+D?_ef{-w_iO&w zUcL8ts*%enU!G+TF4xX3Z-3n0e|P_%4^v9yCwe*yClgX53*tafaA zGfhn3s7|ZeOJRMrPqt@YG|g)}wCPQf&I#v2wu47!%f1Y8oc2=bOUCOXlVWGGC7(N( zH>ERCT2FA&o~3~{9=BHg6YU89_w4Q7^1UyQ#jSkf`|IZ~qg<&MGV2=`3r8H~XCeHUF zx1G&P?-?JOl)Rwp)a6w^dn)ad9dA!Fvfv1udN$2->=E#hwgyu9;`7FFF=p z&poi_nM5($%A?;(R9)EbURtA<^<5_;(d}*0RG(`zo^#|!cyapFM|3u*i9K)q#-4sq zAo@RN>`Em*&7`Z`ci;YcwEe~byMti@<~+w1{5d;Mt3U6;-sY*bI$0Bio*ta!@-!aV6*kYb?q3J3U@8TuGKknWQ{rdWQ{({)T!>g8LX1Um#eSQ1Y z^uq5;a}#Ud-`~4)|8C9fjG0g3eOGEo`LAEkc<1Ti>CffuYIyvX{oOKsqinH-7mJ&2 zQqzK&ffknQort`{#pf(B-G={Oi1bZ&@8OE9m1o&%Aqg%Cjf>2(+rS zzR@}P@zsR%x2=ljrq52=xqG+n?Ccz+CkHdHY!SSWayw?)^Ss)fIuRgrdTdsYq zW6#>RUaPbZctkL9i_YHYCg3Kqkj+_YB}?7R_Ey`&7bneozLjnE@`J}?cU&`mcUdm- zY($YN|E!H0|E7Ap|0i%Og?sm-@RsT9l^rvCr~UnE<#^KXPs7^p-{Y1UoaCC*z}>+= ze}U;A(NLy730<3OeVOM4lpnM%u6);Q68a`~)*R1V6`rOQYqnk5{b;pQXz~whO9sVR z+cfps9dncmUWjQ`-J5>fB=#BOu7@~r1|e<0np z(rfzXz+HCT^7B$dgYzzkOi&ND(7z@fH@7n%6!>nwF3G=c)_?D0MRfJrWGjI>5z`j)$t;HHA|B6e zON-ZMbM+Q7urJax77n$XdgM)0C)4`X-9?dZWx~0}SyuTBJ>u7VwSyIcPv1+PCT1s> zdyet$aphgm)3_p~{Bx`QqSSJyx{Z9b$7QW?@4LURE@&>5{3%`7$iOWUl~A+6zB^7y$f4qBlTG2dTSY4$`WtXeZ?k4SAh_#m zwu2($vEx^!E%7WWpITpBA8yvZ@U`HIFFS*{m$9c*7Vn#LOY85mySrb%&R+iegD11` zBQ~kHkh6xz!{+ac`S1&GY`=-SnrXD;SEd`NqoG zi1PaGY%1RRb;q;i`~KI=x|A94v8-cWhs6dVh0F3#UOg{oc|N(j$z{Ra6Ha(XOh|sE zKhbASXvh2mD?XTib=6MNbJ|l`Jv%5>$n4yQg+3dv+upn`r{pS7^Gf37O=d0Db#4!y zMLw3vy0s<2C#b@iYmb@!<3P6Ex+i#l{LI)qJvHk0&C`t^AFrHUHCgkl-W79WyU^vS z+x#9I2~HOZ-`ov{iefq!-Yqk z3F0#9saF>$E1Z~`zo)a*Se*!axQUaQlMQ(eK(^_f6*fZi2`3Hb_E ze$(F;OxwkECHkhZTX^Th+5)%Dtg)I)?FBDwv*Tf2S!`zW_@_nS-OsGmlHJGTj|N!v zFgJZ%xrXmF&#~#kS!`V`%#4k17N~t(aXsPi?6MsnxTa-(TBtVVXV|*^JayOeuH?Kb zyMKcFWC_>g<)_RGuA19#aeL8UBKr1?`{v>Z^)u>n21QFAc#B(3h~Mkp-*!#mN!a=9 z$=^=gwOTjJTp>47RB}Ss4)e|r9@^I~Y)O52JGrp7_Ob2y<(&^MJ$y1R!t>b`{>Sg@ z{@%U&R(6`@w&UA!8E!rcD7IMg`rTRStmU6yekzfw|MN$Idr4v2QuT%Jchp3hD-`@# zQNFn7u=JVxyzCnk1sCjIY^3!?qGPKw8-%Ax{we8KAcF@CsQRNm#nxdS7ev-67 z;xkF(HIGd$NE}?-{xkWn3%eWh{=i-PmRBB9j6EaV;%n8L$@ymWwq=WFYwnkGSeeVy z9WIG7nZ!bWzKNI z{+ie&IY)8hjHHQrT9Y?@>U?_Zx=p8kbQ#N=Rl6TpHEmAqdT=7_pQszT>&?y#2M+-im%~b9n8dk3U_r_=49N z`->ur58W%gd}mMAHRfdNW!Jy5E!Vmgl630ak1sBb?vZx$9Jy9|gyDm1a=0JMLv5C|=Q3p6k0y$Cq%G7FZcu-v zyT$GB?|{Y|tG+CXJ5oGN!1B!hjA};*zTT~>k7jDBd^xaDaL%%nD~Ijb8q+fRjl`7h zl-J37U7Z+`eWXSr_xeJ6w;uVwxt5%}zr`%7u~8~#o!M*ZZTN3-;#@e}w>i<((TAbRT)9~f* z?BB~)dI2=Lsz=9|yf)rGxlvd+%>Y%neOVo3b4PcIJ# zPb!)JJGZJ*sV?c|#HChim|LFm{y!pGIr~!w%dU%k5u$&t8E=n!)?U&lHML9bS1Z0<)}!}FU;Cf*9LkDAHO zX1UdJ($` z$RfsF@+PvTW98*ZFLidf94lW^C4J}Clamt_MP|Ax$z>e&d$`{$>-fh@%S@*Ruw^g1 zxmN31Qx>b@=`7`*M%VLS9l35;>V4THy{ni#(0j{~2iFcwjQXf++B&;p-x1jfcYhZc zf3wgz>bm#;C$~L;Za#|K9XVGV)n0Eib-(DghD}-YYjgi}$>$>b`ja>co^vmakVt0x z*cs=t=tiJw$mNd>KRj#_=7=X9z5BK8=wwSPCE1-eWJQ z&K}!ClWr*75l>FFmDyzOIr-t_LZu4koY`gPPOqQ-_0z-RyL|mqwmr8ipH({DUQ3ZZ zZ9~xCBdq4jBBZWro|(Oe*_J`i_TWn1s|qe}7W45ZM(eR0x|C@D$ki;LX9>$mAMe6d zY_`wiy7eY&Z9IA|qxq>2!!ven$txPpoT>_)#@Ef(*sOoO>&JxqclSka-ne3Ujqd8( zZ%!5WEcJezKAq!g>6yot;n)0yd()UpZA-sTnX`P&ZI0G88)tktc_W%NZOxwe36r*T zNFJYh=>CHv(hP~8y*GYY7kjGcQT*A?4X^lCe&usfKfGg+?5*#oZ`nQ(o4Us%WQNG5 z8<92#YQ4MUCi3mg$XaMLb-J$Y30YxDt)h2s*K-;<)_oED*`qF!GOJVEE-r&JcFNjg zYxBQ)hxDF`{V9KUv+12V*9UWBi&dmG}JURXD+Am*7&B`Xgs)s6%L<&av1$Zyd$0 zJ~C0yGu7Ro)q2ywc%1u{g2cApMTk?p7odC&%m`iL`-m>&3^H8rOh+7y0jF#rg zPs@LO$Ky)I>wCk^8ttv-Uh=)X^mW{^b*~Hce|BwNxo*aXeUV}Fk1WtUHm^|HU8+;1 zO>@UWn;y9}ZufFMc6B#%<)_4Ci)ueTXxeD}dE1^%T8?ff-`I@Ttiqkgq}%!+wjL>(oC~Cwe^ZK8B%y3?pTn!q;&qF{sV^k2hH^o1L_`V6||^X z?%S}8UtQ2^ikPgoeoO31Mz6jNiTo-XH}pP#RC#;KLg|9?6)F>W{2Q+qGXyP2Tp$tq z_|X<_Pu9-|;`YTpGxSx@d~fz%B;fL!n>XqN9VV{gatjb#I-^G7M0?%)mCHH;l}x@K zYH>PU;r;&ayN$nCdD!-(if>(HmVMsRrHc3V!(+;S=Kr0@KfS0vM( zt}M|y_PI-C)uF2#9pcyDL|$9CXhvDlR{51G=l2})y`bLjb+V;;pP|R)P9dQ--Id20 zl?7cyxbIiiJ~(T!{=0x|MEK1*^9Ih(uAMS^+upbT+r#sI(%W*4%$fYzztYa$PCPZ= z@8t94{=#ok>MXwg|M&3e)1Mb_Z%|}Eb12ecufRP9JK-}de0p9T-kaPP-P>AG#PDWK z7n4MImXz!f1@p7+Odat{#7&>xxD(5oEE)BsH+|X%W|Ov!C8F_LWNkYfA8wqX@Qih_ z;H-sdmv26Bm6#fKDCe0>kb;Cm*zqdk_s_p9^ZRAA-%F!jL)D}Bi|dlzas_jC)kxp3 z{%?}A*EBkc`N~}j`#8Vz=QTF#f2#j&^z?af1nblnH#RO{PB?nq*Ko(`>4~Sb_L?ln zayWTSn`O?PrJs_I>@bKw-JZ#My3Ody<#`Rgj$8`o*BJ0+a#=V%FlI4*HE)>>bJQgB zi2GdApV^kSti2hS{`C0fzls`mD{pN~JCpUg;^*w|tsJJyb1j26dp`@RNHt6iE^qs2 zsp=wP`nc@n^F-|x>94EivhTTil;KPB+pCRYZf09`SLtV5*~8E0vE*eWi(t5f!mN3{ zS5N4-NW{;c&)IFYWKLkl0=BnV5lPNcYvM2PPGsbhlCpAop(oJw{yV46w}~Q&d{SQ- za-Qm_Y&#fWr1suEbY=zTo>d2yUu?Z)kb0a&TGMZx_!RfFRps42 zPy5P+KY!%5JV3eZNuytnI<%o!Qz3Ji>=nV~sp``#kTg zDH15;FP?g`eB(a$9n1gf@H%b2&wnC`JD;U5Fvs(`fa7`7iLIA*WQzyAy2cTn#KyF0 z5!=yM^Oj6%6ZYD@H}K%Bt2YE5ailG*U!KRo{3KY^qsKG7>`H%Z#RCq{+c_E!ze*Ws zu8xYIrvF}Yt;F^(x!ILw;zxHsFLjz*VP$*F^4`hY_Wk$8zTV!`Cz>94nxQ>$N6F*q z>-7zOW&Qp6^Hcqw<@NU4EK0d%DjwW<#3%8z?2@Q*NZXXKscQ~#5vyY)&ndOnx zt4Au1Yy#3>KKX`d6)?ooQf=><{3n%_vvVD?)-G{!%E2mko6gIGL zbLc-Tpft(vT~A|LL0WNpPR0C{)5~t&5c%mlSH0l%+8tFNv^Lo_H|dG);8-0yJ$dpo zbMwb{W6Pgbok^Lof6L1WPfq(!|M%sK>CE%fQzg$k&HEsBxKJ-J%~aXNZ=T>HD(d++*&2jh`-6bo|+% z7kg`YT>4G9nq9)p%yS+He(v~{e6Ed`%hR1DY)^5-!#jDRyaBw?f%`lfYd5YrI(+r34R>nye3l7X%mtg))XkDU$vC8e@T?gYE&k2~Q zPr7lYzpa4D_0Gar>kmotS6-gwI3uv4r|pZCVbgt&bEiLay`SolqAq0_6Z$E9*WMX9 zE{@L^ZG7}j_}J=2v&1g2=AJECXdt-$%foXm)pP!Z&F}dpdf9g2#_Yzh*hLQ%^MB^8Gwo}^ewV4MBImIB7cxz4l2Gj0FYBq{!#<(hIn(y23y(|21p(Ged12|P zn`IVCPCtF+h0{JY)8upu)8mQp0jGne$o)LKgH=X-je2*|imm65T)Z%=nStYe`uc=x zG7IZxPP04{+<3IfBXe!&UQ?HfW#=Rx8$2>ewRq8_zaWeKIfK9?t66f_OJv`zKlR!< z`aqBU-1mDrbShWOd9L?bNFaCiw-4X`e)_j>x9z-jvmV*Zzx-xn?l1e=x%PIovcLD2 zT|Rm{^SZEX+i4>Kw&l|v{`fOpaD|7BH%Ikc)6lq2dU;FYCY&nrJo~x(p>c?8Z0L$4 z#>2eF>SL1BLw>D&>k_PG{fzUeTZHh2XT2H+o{0#bxluQzBydCJwMf~M3@p!Hh)ZU; zb$`A$t5xOosY8)c9$Q3x4dp-gK3RU2dy}@|)hRCxGrJ7m-HH~U*Q2<5zs}FgPZM|5 zoVd2|{`%)iN=mgB`6sz0p7Z|w#eCH8m`HP;U^`3unG38=ZH2d|?z}6$lqXK`@()4p z#9Cgiv_u;-Wf`rH>nDHp+xw&SNAtIiHmS5w(QMU%Lq%$@tTc~jxu&^s>aGy^FSU2p zWVH+b?^-|Kd}88LhC|Qp^=`9x5^gY=^|tv$mJSaUCN|qc!q-$orJe8E&g*?AqI-Ma zO*N)ZUypy}2-{@aw_%sY<5Whq8wZ18_qK%Z&{JFeWQWj9fob-;HTUjq+Timfa?zt{ zh62_iE4MCiQPz+vQe0a#t#Og^@6gU?@}CWVH|Q)_b?{+Omei5nXDKo(`+3&iyjh)d z`i0U9j^ycWQYUAnr%6Y%u+E@rvwc0%+S^=lT07_J{GONX!FWoR!LI2wM5d!9~XMoG%fA3D7kkf;?!1`h^5lj z6B@H*BVX_8>fFQ087mtQ8Ys3<9+o!M8XxJhX*)0R{N5=Yw&`5y6~p$$ zCI7-dfAQJP#B{s%hQ&mkZB=DjvA--2o6g>J^8LDHzsq(m^7*>Pysxn$HTM3RpMM`u znEU(ft{p0mOWsXuFaG#-PRVrn?N81h-qp+etVhaxk|Wpr2<2~`?NaUzw7}=SUrjM_*mXx2Eetp`d>9PVjlX#@~d^q{E8Wu<| z&)cB-l;ca+k55r~`5Fhrgn8QBg0)_YSF}F89{Nu8Woc&YJJ+3lhI6KWVoS-s{PtJY z%B(JiJKjaTpBVS*?3BFx?aI`bm3uwe4Sp=$Z?o^r&p&g{#0P#ql5wVn&tt3G-Ym&^ z{pY0Jv?BCWauhGkaEQLP-_3x>zC!Qt$2pD)TnALnZMym8WJ9m1xab3u^^coOoeW=0 z?mc3#>-r+GZ!5NZnKMb9VO1EL#PYbk8=H?kZazA5u7Y~ggRS+>+FvUEuVbr^{j<<9 zj8nxnvg*X#q?%2vFPh$0<{oSAkPF(x9VjTU@Rag48M%wzCoDowbKGcD{7@r#jy*M85Zq!?`{NC-aRc|k%!X;x4_#t7 zyUvxdZ+i=as;<@|c88psT_<;MSFm5Zi2Lts=i8^W|0Exsvi()T(*Lh`jIu<`HD~Xc zAj6W(@FIlgc;xjZhR@R$+Z08%CrYiDuG_AuE}U@dI}TR_5^wckDRZzN|FR-qvdC@!2!$8p~|BJufhRUFP@5W%jBsRS7ZtahG*#*QZCz zygZj7C%U&tTUOh6efm=2!x6?EP3GnbWy}}OxI9x>(7i0Qs9$JBYlgAWvS&|u#RS(K zR*P^@ITjGbZJq4rcdP{RzE( zG3vmc^t2zlzVO*E5$Y>0DsWrz;G)bU5l-i&i-Y%1d0E!7>z>T~v^bEzH z>zOZ@boloXaTlXqC4M1mqbDR4Y|d@=6|^waJ$3Hb@{9iB8HZ0=ddw{U`Du}i_~rk) zjF!df!i7b%vSvKyRdGJ~vgmb;%^bCYd=Z8Oj8Al}0f zA9=58Ou5D?Rcp%o_V=fE-`~Hh`@Q<{+3nY_Z~y%DEx+qe#k~(2j)uB=N$qOn=~9z1 zc4MEhwWYLK;=qj#;a$veDx3Z0J+j}&Yf%v8+>#XDd#L)z(Xv~U^2?WYnmVO8`6VXA zIX`%kWuLz|Smr?KfmxBqSuaW)kyPIqGFd#lQgFNBS#E*rDxpb6+&T|pX7yb9vPR|a zVf$L=o!fX8-}q$}@45NY#P~^WKg^PS_WD{du?v4w%{o@F!6m0-JulbQ1L-{}zCJzI zjxIduz<6`VV%1}52PZU2t)1lJ6k!}0AkC)O{KaI~cX7df`HxE({oc>>DRE-Se{egt zV4uPqNmb^D5?}bIy!Q{C-*_&!ZJhwKMy%Eo(}{f-m;bmK+rLHW$FZ^t2J7s&rdld+ z_HCJWvD%vOo>=*9+R_}p=XEYm!!EbU$EFH>p1N_hx~xC1&_v^c_ z{i^VJ**!%DvqCG~Ca=7uDQ_&lc6Hg7)8;(E9u8~&J?Fi|Q;e{=c8f-YR@&vnPNregCeHd9@3)*jVovI~dzPexPkUDMI>2 zR^vunWZ*-AWYZg@WMIS}$hs+n&OF7sPDK4v;@=;(M#cs*VwrQy56Kpm z|JrEHc2HvKMa4#Y27zshT6vk;kD8_=2)P`-{3CYGX>rzu{T6{OsHdUkWnec{`CA7N$2j%t5}XS1oUJ^Z}?=^c(LcSs2R^(CaGYt&olTB z-}=6BRnx}S^&gurY5Y0!RZ98mEFLZOc9GA2FQrM!%beV3EgN?93xCbEp8^(>Lc7+T zXX9I)q4q(<`6Tbs{eSr1HuMCT$tZmNXBT>kztM5WX}>)Ox&E>9o#^oJ&+3d3&+$D! zMNrCxu?B=oySumP%R-CvRP|d((j8Mi~gvWpHS6ZFUryXsCDuJt`hB? zGpGMf{(5(|`5xw$3<2rH@Vi-a@9)|F^XvWn^}nA<-{58B*(i1VYO(*7gyrv6M%UNL z#mD)bI9Vtn+1P*L8OucexkY|F^39=>{5Ulem|vT$xNzv=WS3v{EDD+Tdbrt-?=x9Z z{CV-xrvH;CvN|#`wm;&Qza*;I5a?;V&h+b|;^d>^S;w6!<%$&;`gOSP&s!0a;WF*D z)gO+v9P-a)0wslxw5+?VGq35Jon8L=XTSdC+CQ=>oIZ;B!T7MnciZ{Bkui;PEtx`&Dqdt3diFj4-RGaFGL!S}iO<|$_GgwTV;w8+ z9Kq!>Y7>`wwuJ9g*4q~#Ud*tlIAn=%VAQU+2W2K!CwC-9YDBu%lsIW*v-~YC`gO+J zG2?XME8cYyrI$`#zGBugY2v{hhD8CowVive_^vZNH(e|CaadJ_PDA_a&Ica-Z@<)w z+w}RZaJ%PjaUiy4)=Ho1KLO9=`g0a`Z^=0Is-cv#FkRR{E06b^0Y}TktfvoDce)F* z6ml}HV1H?H)v983h=Wk2#KL4m6Z#}!pOex5t|!__@4olkPkLyrsmcUG-> zAvop4DG%k4ZF%jWv}S3`&ey90 zY=1<#=0xmH)7y37VM?px`^jtjGdJBm&QZ9_MY2HNeTLZ9=&uR~Z|q*kRrBRVtWzrY zz7XyAe{ysd2ww4dC(gjZZM=8m$^3BdDV3K@)QqOcN0+-K{Kz={C#-=#3*;8%36W`xAxnKMJY^Ri>mqG={-0176C%5na_iy+6{rgw0-_xkAXkPW) zr76pD+L6co_VeuPe_h%ZXvrmZ;?2|}EwOjL%$lI!(37-ci&~e=frJ{8h*S! zm1OnJTK{0c#1%>HHeU{w-|9GDu{dv9=(%G{UYeJ`eE2)xnuTGm_a5uK1tH6Kod2Hx zYW2791IlsHmeVtxY73l>#v3zMH+3)8c_hNJccI>zc@}ry&f#Bne$LO0 z!4+(Q4~~XJ9NM$Sc5mEK&iJa&i)7w(^cfgVD~)r};aR@v+EnRM2SAp;K;#KCpo0T!kbGjjZud7Smrd-fb`UAv{`)(lCuP|2j1J%N_zOdeZC?K|wTK%dXy zh16a#_D$8-`Ht?(5t0>U$Xa%K*^JcW|X>Q(Eh#_I+aVZS$zjxctIu z62DdOC5NzG^S3>^`}_6&-_PSqUmA#fj#*c{)AwgkOzKO+pTEoR*Zh8VdsRl)HpNYb z;!m|Bem*`ueUs+-3D++R^J-12y2$OEQ@Y0d$W|kbQ&YnZ7@fR7`MpZQ-1=z!qUpNF zm#nkp^1jHk^N#oRA0E+LG8S!)IwF2kdb_F+58IrMy*GW$OVb@sPxjd&#r|^pn;q) zrKQ%!S)ZAoa{lwLmo>lsRJ@b7-rymxa_(x|9oEK*&HJ~kN{{@g7&Ga_HOIr86WRV8 zd#bLvRp$PjBfksOS5+)ODt$ftWjV`~r}nWI8nZ5jPg>Hl`vFU|U)ICL_6KGuU!MP` z-Dri;WA>hJ|NQ4QOk{qfdaJ|k>aOSZ#`CQmm_y>Mk5ukS_hON@<&k~E;j;KZKVR62 zs(izDFS%<)q$^ZCITlRqnKyo)*MWZ=}6Mza^Z ze9f;fw@t0?`5-Q8t>_fQT5xvpo@ciAzH{1bxNyT=XlKvsD_KAIe)hgtD6799rRG#} zKy;Ai%78`74++%T=}(cGux#1Irps2n1yfR2o9w#c^O$S-w&s87Dq`nKoQ_9wCJF3a zr2Bc9&WU;Rt_n{)9PN;QIiWC<&{rR!`pHAZWb6|7ej{vTCxwhPS8@tyy>+}|w zykxy6>BXa3vF^zmag$}HCa>L?XRr5o`Q_=$oU+3kKZ@O6q&U0D`KMfvTh{fTYnxw8 z_>uBkPh~3m9k%sfONzh9aj9teY^-tNySK$#Yd&pe*P*Swgx{r`Z40) zuk!ch{7LV*&K%EBD2%PYd@s)Qy4bqP!WzjU`EB!JpFLbYy?wsD&Ho>tUw(3GTiDbj zuCUvnE&Sl+ikD|L9G$yxv8LHZVUN}NK?%G(f(l1O3i;+&l^4C#_UhlpWpnTnOVshW zC%MP-X76)x`EsOBrKU}O;P={M>GiXh?%e4}oFy$fk?llf()q?0>-z8IP1&)#C$?zj<+qiQ`{wBP z*W0BoKfYBW$3~_{{?6e>g{}#iPfxP_Gs@89U3fA)Kw3~jQAzG?x8utd*Nk^@#Xt1GviSMm6viH1z(x0w53x9Xz6|ysEG2FRQXsN;F@P>8G zj@Cvcsr(zAwTEVF=^s8EZXoncOR7iX&8Fl_lPC7DoG;nz-+FA#oX`z_F0MXPw0!nl z=F*iMcP7M6I9ROd+k2H`QiP4rzg5z^otTN#3>0uI+{0+pR3cW{o1UyGst@Wnrdeq{->6`M7`+7{O!rXDevq5 zvdJG-&$w>zS;=7Gk8cIFq4GCf#M_Tww)yHDXm{?ee}8}dpXu`Q|9)})Yl*PaQy2dA zRG^~wV#gCf?n8H3WM16crSvbSEk%4z@7ln}t9e+IR%M)ABYxUt150jnojY%jK;SLz zjYfj`E8BM`giKG9cHhuxIp4i_XM*xkQ~&o5gcz6gC)zD`m2nQ*H_!9WNi&A(_124W z8x>@ZxEq!I@Hnav9Fm+MJoWLcIlDgk-ZDSCcFLv`&zHIFoUXvW-nROgp4L^qPw>lJ&4u&j8lNu>Q1PEKdui_Z=!M7T$z+{c z^TCKyZr-z#Y$6#9ms5A~owU*M);Flybo0p(!HtiaB8(2^t~OZp+TfJw^ja%^XT4+b zfijQ1Kkqg_ULyUi&5&uG&~=^1Y_M@@I!TW=RX>AYFvzY z=Da(!E->ybOX!aCkI$LBnN_i#E$5A7pmT7OOPH&^h$BmG3G0O&S68v0pUk~jR>^cx zqayn@wtYL3jxE`EIV)LdLxSp`A7L|$4QzCszAsBNir$hFZKzk4B9r;nH2PF@_-yWQ zmi-D&%+<7d^b7G`IQ^tx@c>y$oJ{ko20QUASp9xs(A9n=;+ zq4E0yYwzQ8uV0zIjC;7nFLGmrx(v&ggF12kJ(pfLuiP(R`^NmJQNs@YSs@}XeMCNn zFQ_&U+4n6P35m$&s>eP{}VbZ*|MD z5M#IYw1sa)(uv1CP7)If&el%QcHa?^+Try<;Xswqgk@Rc_mW+`Z*m{2 zUN!Ch{pK0pdRwnWDpvP&KmIiO|99zXO)>t;z^z{`UoLz2-17I&*N0zztlT}PGyGWk zZ#y9;vE~nnnwJ{CZ#}RjWPuUqg+qUoJ(Ua(sJHmAI?T}UxjFSm+=8Q4H+BV@^$IE9 zpZq&@DMw>@Yg}QJ%xReeEB+J;Esni*Nx1IOy`I|G8psP) zGfhohmCmOBy#2fS%+AHX-%VF@+tRh-3p;;Psb%Ey$%?GeS95>NH4_Qsbg(}$jn`~) z?`$T6zc;rY=GT8CU$5$<*2v%!WyYeuFx2g&SkB4l%G3|GanEGoo_DgC zN`AU0ecU0eaor02Z98*cFI&iSz}GgPIsMB^wt(L!BbmE6{QVkCdyMs(D13*I% z=W*U8_L*&okHW5bZtgq+iaBjZ?|2K*1x6Any}<*ZwM zAkg9Kf)#uhp83^kh6Q*0X}T%9OzN`Z4X@1~q8?kaf7^SBUGYj%P~fDAog3A(lC&?z zF2DJ-=>DAJj+?hHl=>CurG37ASM$_PkTCdhF7;(XN~#d zk|_T23!j$7@I6!b?jCw>XM0w~$!l*|{8B6RoQgHrS4fwC;Mn@)>7;eNms~%Hc8C33 z`Td!PIM4f%(gzIew_AUll}@(0F?sXSqPR(InP;_|y0pFq*3CG+ z?qMwX@nG*VyAwYc3SM7%RV#TQ|DLt5#-!68Kl|>^Jzn5w!9GdSW^=$ZU%8sfJe7T zn$$CO8H2(p&9<4U>wauaG4Os6_(s9=x&5xfABKXNYp04G-~H#^ zyM4I}E}7`HE2qite)sR=m%O!~b|*V8tFtJ7ewqLF7y19+>wkZ=uaDKcCoMil`Epy= z#fYa7*V}4bihAEjylZOXeyQDgSygmG&H1{{Z5;h7$NJQ+mQ3nja8u#&y%ohwi3QSE z(~imQGS%leQuywOf(zFqlj=u}hMbT2bFY~B`OK_4Ag8f9c#Gp&2CYE5b8!>fzvUQD z3voXglAayBWUKj$8(q$m%+AJby&P%3Y`ORKC$*-_!}U++PF_2GtG$Z-wX5^(ZEF91 zUY=WdM6Gbv!6)wJjdDWkI5(d*T&IN4MQG z4o%RRKlk$|sSEKZ`W`*jFh0Cw#gSiyLb^uHrxMRxy1XFs$J#2BRm^&>B028&*9TvH zapKg9b&0*YT&(UP*%izc>AS9L@0IYq{_&f9n{C|E`7;-sS#iX&Suw^ZFqF%s+mXlp zhEVazgd;r7UXwPeJ*-%39jh#)>Jfj^ynB~!!{JS)Uw(NP&g7O%e)guD&rrQEY3i4G zzTM$lm2OFV=V*Ttwq(^!+r`gjPpV_P`!YRt^Jx{MZK9F4J|8aNp7pVA(ZZ@*=F5*A zwX9%iR2O=c-ov)#timBjB_I73*>_jU@9y#M^4`U`e#ysQI*%sh9N|{}BpCHP)Jkjf zoY*}~w=Qj-9mE+IMjp;d+x--gc3N(bCi zBGQ%iSWir^@j1F>|Ko-O+P)52*1-oF4Cfxr+p}l7NZH40tvgZ!_|m0sbUc;Wcu~*t zsrAO_8R~EMoD+KC^Dtj9>!8^p*0|ph4)YIbKX}QX@;H>&;^a2j33EQpT$mXwdo5IU z=W4HwpQGYab6+RtWw!ZT?R|Xx_;Zu_>nHvC*8X|@Ea$?ym-+nlxu5HQez;u!tH1u_ zlI7iPUbPaDktS1l1n(ErE&8y<;A^I^dbr^-m&&NM2_KFISWo&8{^9sd6;&Y(5+9Wfsl-98ZKDP9tqmbPc{hk)rC z&+V=qGhu9-_LFI=vew7)gK{gJ3tfv!lBSa2}th=i!QTB3%=ftTF=(vQ@37}^@AeeU1?C2--%kVTHY z*=}dM#ghE^c{CprgQ=iD;) z?6kF|K_xb-m5Gnfi?41R_xCWC#O5uTrjBi~OoPH-O z{BAz{dU>2b>v^jwzSknooD&h$TVKj&cc=eh)g`CmB7tca^rEi+oOmv_>)YhUwYm#T9Nx*D)KH#tgqew7+2o{2 z&#%z^GrZ+Xxen@NxOx>EEr_s`SUo$q==`QIzRvK94z|nEhn`QlIz5S7g5i}xpZKCC z7uG4>#UIpWG_EdsdO9s&>7(;otUe1HGgosc?eGlGyLu%~f~nzx!7S#@nOsrE3S7_b z^F;?m%Rarb{DWNCQmN9K$a_u^3nstcZ~3JCSM2mFe}65F_&xpf&i7xczWu4nd^vHE zzUTgu=kx9A|9r3ieJR#z&&?-|jhAn|)Z%7Za_dvg5q_oKh1RzeUX(mg(9+`h&bOnt z=CI&GvAJT2z1sXC?hXn+kMcaET_LJNLL> zMYP~j_1ez62J#=KFg#hoTPGEMK1sX9gnMyDZNM(>Hw^!k3?I6$kcpb~Wuf=^*m^%> z2KC6_t9vGHch_3`d1=(KH&6dO{rEG{Vy>RJ#;%Z?)t@FStW6bYSQC8EktMqLRG+J1 z*n;MduU~$g7;ksy#B;L~yFPd=pThil?vgDc8zPbqY?QdtcOdtC@uS5q#!+qT2dvc3 zYgKBRJynxGIH5+Gv+()e(7QZS4@HQ~Un|tZQ;-k;(CWdp#o4Thh zF5IMDE4fZ)V>ZX{lR=y!f|3D?WXiVsgg*2>qE*RV`Z#Z|>Z-V#RZO?8hU~6md>LVA zHGQ*r`;vxsjmT#b3ajMx7p|LGyTGtrB0sy+*dywvP{zDTMUHtom$u0;u3Ue+c@5Lu zLuC@ml6=i!M_4!BdH8Y1qIvfk1&k)HJtzJl=Rm^i7_leOTodaW*Itd-`0lriY1MW& zp9ZlxQ+!rhYcK>FTsdhfcVoqkq6@+1?k5!PZvOmZ$(&7Jeu;5A@A`Pq^Qi%s|ABSf z%r^CHH_xd0c~?$LRMwJUfAz%nzUA^~Il8Bgu-iQE+b_f|<38mIXWo-m&s}Z47bPoR ze<^Cv>?*zBq7z@FQy{IU{;<65z)kft3;Q?RwViPF*IeC8YYW;Aor+@U2>!#e`1poy zmf%|p3r`qUalg-OiLd5!iQVF{Lc!-w6I=b_)`_}HB3xBPcT0FVDOh@VoNN(0Qv7@g zOU%*TUhIwrQWySn%{MxfC|YgQlv)y%`|4bv$F3EfdwmwY_UV>sVK;ua+uVG^huy~w zYv0tLTi$-$y(~XQf1~Q*%zu9(&cFWk^Y!QD{PDY{pXYym)%akicn0gAvvzfD1?#SD ziQKk0-J?V`<|d1Hu$f4c9@FPDJ=`5r z**uJa(s9AWM zZ9jMGl9=D>6+7gY%$^b;$9LVY{^7JI3wH{$8?NokJujQQQiyR2;fhym zsaW52LE(3GpysPb!dEt_G@qPmb0}f+p$@=-9(xt|9NP82KR!D3{NrCQ3mXodGyi(w+3n-)KY#k<{weoAZC@Yv|KqcF z+wa$3-?x9^PXE7ORa-n{yC%Y89UH9&2ov^}RA%W)sQ-8tP znWj2HCk`K&__uXuDcfVFlpBr_9T)1?G>7zXsmVODD;Bybd%7iYw{glN(b9IkeG_A^Y{9Ncbw>3;v$}!-p%xzLpWxU?scv;cRw}y1e&c&c~IdLp>}S@L4Cm%gJrKJ zi;QO)H&;$9ef%Y?G2X$IJL`YpoNBHl^*dh`9^BOVJnQ!&$K~^Wc2p^3{7~ZC-K>0& zVW#4h2OHjK*gV>1csZq1qdKiw{r<~aSElSvTY9-;S=O0GofFQ%Aq;8m@i|v(YR*Q_ z7jeCuaj@x_mG(5-2~)bxo;315-x9vtYTCxjXWy)Svr+z7uUnvpTL<2qy2%H*7hE}~ALEonOve_pWt|NLR$t~sAmRm`?5z0+OK(R%uv-Peql%MF*k{8cyW{5pooHrC93 z7RmEWSZ|$`7I_%hE3xO%gfjPq7Cg)i-b{LVj-O4O6ehm6vF0^8X=mZr+pV3M`<16; zF(b2_8SB~wM+-k1zS%Qj^D+rkN+h6BcJay21 z)3|@HU0lm!(Yf~;G&WB^AQW@r#TC~#X8*X=b_h7eThCnmYRcj1&s~??dp2{oxk`c0 z*~V=-DYqDGm?Cw|Z9?0Rzjs<4e`PK2S0N*L0rx!dprwX>t7iTD=)oP~G;fBRv(gr) zPR2bO#cyvAt$(!i{89CtlNfU2j;)Ejrf;xEQ`w&9FzYpqwCuC7No-3VC$HKk-4dL6 zW7Fj5Sxd~Vzu7s+l;`y4I-gh{9V*y9^-_PV>kW0yDK1?%Bn+Ml=kba;EZ{yL`Kf4I zvfIogNvoL}3;TJ@zdJG5+_AAZfArep<7(fEN^i9t{GFp5Vo}*_mt%5Pm4`XE>a%W> zo6f}#hL7E{zTOi~+Il4AeDO?SxxWVd%r8~WDBB)&_9=S76Z)Vc;+XS|hVKmvKNJ^) z1zbJ(A*@tQPhtL!H@4+!cRZv&?ENL5?8{iknBOYMzwv^p`_CT+CdlZhp($ms9>!I7^)1DthBdE^QA(!&fw4)-DjU!`mm2%gJ4r|Qj| z`}_BDKXaF`R(X7Fs*BUIe`{ARlTiDZU(R>`$6=|AYeGrw-mf+D1%3U6s*`T0ytMIQ zV>z9^bmkU`n#;mceivV|YdMLrT-{)(*=+c>Y2MQ1DMeLc-IrY^aJVrgNX%6-)P0ch zBOo%}J2q!+`LwP*eq1H**!FGb)p&Q|%GDQlw-!w7`Dc5vXVR>ym9Gn&S6VJxP&i5Q zPNLL;Sq`$%?H`}c>5qQ+*zd#=S%vM5G54l!y|scwgtgoTRf} z^Uefumf~mpYagvBk*&5o=*zZ9`G8-@hj*#F>aUnVzuVJUd zGVWXQNUh;CKQyy;k=ntAjMJS;9VTq6SDi|lQa9(y(7Xa zvtx$Jl_`&x=5Dy6dHz^iPgC5&%(!_ThZ?=V@i4v$UA`ng!`frjlc!wnyzbY|)-G~> zSG$m5Q)gqs|Mz*P@26eOZ~JuJv-q>cLDheyFaABx`MERepT)WQH^u%_TK%%aeOM%cBgrf(nEA?|2s<(Tor0O9@a2 zC|SR-HT<;W{g+EVh6@~t3(VF?JwC2< zAbY0Sz56cl?u>nsn{TiC8Jv4*`3{@sk=cUWEEVbx_o(+d+Rji7jkz7bnfJQ#-OHsh z#gk@i4!F&fywoqb;Kbaq0d{;Uy8?Sw)8<+e>*!#h&MY|)a6a|{tj-FKD z{_N3msJd}#6Hiy|MCEf*8`$$dTr6m^VBMKAAz6LT8FvezhE^wy?}3hUnuKb#!ZrHu zO%gp@q{BJs@5RbFn-tjQl&GB)@QaZCIOje~z!lCyDbso{i5=greVDv;p-n=c*qUNf z_Zdb$j^Y(T{2P>7KPn%+-u=?AxWFMO(7Kgp#y*ye(#V$vQip9jZB+YT2g+n_GM(po zZrP<&D~+q3GcHX%5wYoGkkYZajSU{A7L^JMS6^T9W#2Z2BW8~d<+ucO<%hU8)>Uu#b zc;>-Pdbi6w!(uocUZ&5_Gs^aQ{&>-}bDwTImxTscio4p|=f6(gS9|Kx`P9P5?&pR7 ztS0R~zDH{Pwzso2U#m3UP->VjFJH*Cue++AMP6xQy zo|l-l&-k{6|J#qtGe4M@9KF7PiNR5Mo6P5qFU?&mpYPdm>-?6kSO(sGOnoYK>!wP? zu&%0{tm#^JM_Q>XT4B} zakoWhZt{N=ES^&x+)IIo=J;bb7%JJC51wq zjGC9ezLGh&sZTkEXZvb?-sIk=ffJe}x9jc^ytSvq+3KmP-PQS%+WQu1XwP1$>3P5C z#uDFIT5C@&lK*|~YC@@`_NCo*r#{`XUN&`U`EPf7o^opgo$#|K6& z=dH_Pfd?7pG`AD=YlEGV{Y+hekuO-xK2J_Ld#L zVfuAv7H{LS$zD-=H)4%ros^lFv zi$DF$s#@{u^h2xj_vf6~pI~&lH_db1r0ti#wI9Cw*{HsE_Q7xkd9llU8eLoZb}x|H zp{}c}mSuic;I?br$^|=88uFRpC zjG|ZDa2?lLz53U(ReZ<)_(tBFzxmUuMQ`RU{}S>jPIpTCiBy&^9j3o>cP)ABx}r_` zqm$yf505Uns6W=wejnNQ=GWeg6)!EXJ!Q!|V$5-K$A&{4oNXrpICd!9`r~pmhjIP% z^e15_gkOawi|{%u>d<7_+tj;T(r(-G)9n8Q4FcH~Z*8nq*7M1!$ev@P6Iaf+>X8wb z$&YVpv#lph4K&qi@MzcQyL00C{?|JUCjNS!QRe7x#N^}mPbIJ+c|~g9agLx_sSFqN zW_^5Yw}j8-3ya|uf%8o6hFz<*=RWq(SU3CHZUGLKZx64C9qyha9y4oo#NL3g6)p)J zhi@qSZkQLjIZo8sG+f#CcS`#EG&`5XnI{}isJwZ`b)t2Jz^TXoHrh$b%+sGZL2cR3 zg>wX!@3Y+Ys(7ci_^%>HA-mT?WxTIBwWJn$+9|Hf%zR;}ebpjUK6*L}TcN}L%`;V%+#brcP5#7jEAzHX zqK`amL*+X%n7k62HY>!am5Y~8 z-Mge~OZ(j|wps;B(LGyPw3Gaw&r=G0p?_@i>!`GB&K=9;I7J?)-AeY(ZnjvbzRYxv z?TU3Vnq^JZLT{J(1R3qsls|N8O`&DFe#+&C(G7LWVvqG$sTThYo>y*Y9?CegCN$+u zfnvB`=3xi3Eb2|@dy(v& zH%-)5`hA4`p|dCN=;js*&TYFV#w&Zo>#3WHv#y~=@{grH6Sr5rI)6j`#O9S6=^307 zUUs^%XH^dh#a7Oqn89hNaC_W(o?j;w`aD{*z{c?NQvY`G z_f01QLc5wjt-Gz_rp0ky)9R|)v4i3t@>`fX1H4@1nymg^a`OB6gRS>eaB0}0783@x$fEq$H&8%y^-ERAE!PV`^LVcNNa%3UjU=y!_3P zd#A^3r!Akqo1b<0dPwky_lv3Z8G7>!&%O|t7T{iY{rkg5uFr&$_BrpB@SU$Arj@YP zd+sCV*S5mGZXdMtj}}C->H7y*2biQhkzJ)0B(v2%@<5A7tD-gAJ9ho#JrUyfg*Lmc zC@cGAwqepczI$1lX9%B2i!GmEc1q0PPk>YhWnbis3J$+*S{KSPb3s1cfH({vbE=McjQU|NzJf1+maRqN0eQa*nHrA zoUxEuhphTk#)CUHyi*8y$)dfJjVEy7?MvT-uLtk7kUz_9#1NOH6K;&+gsiw~l?&p*O~RWL7*qzEgJ}&!(Mu_gdP*MQj<$euT@t&NcpgCCR7C zQ0AlL^6ZPhgO(VtV@y(D<&b{8?4@vev``>h`T?Pb0tF(CC)tx;bp`3(yLwJ8A@|U^ zCC$uM#lD%FavvBS@-12MSUcduzN?!%Z&rDlb4(2Vw(V+-`GbJ!UyOTW;aKYZ_1cQ$s~(ceRW+4_EyvTYKA5dflh?!grs)F4c)Y;2dKh;&J+l^9{*|H}CO& zIQw{y=nIcYj^)cvvig1&%kGKGILGfbk&i9rLq5}L2E&qx&+chYKIp!0i`ND2qYTp* ztL@Bwf1`bh{2e)4^C|2-`sb=2=1ZCAz3p)#xZ%tjV z=zWJzBHeuzgV|-qlRD9vOiPYve{8eNO)I)FsgJv})nRVC=KopC^e0>{wemk*Fl*`E zz|uvk9~A#sRZ3j@m~{26uO-i!+!+r~XZG)3DR@ixnAInNo5#{qG%wq+`YAt=ej&Ir z?)mb{l+ue+FEwxPQ7GybNX=i8u5OqhB;#Qr%>8KUo;#cQUdZ!w^gLZrmeF?np)TvX z&P=oXl*CqT0smFiUce*W}p!?eGjjKtQ>W)?pfe5L>LLO-5)k#iaP)?Ga} zD`?A{>2($FmN`pLKN~l-^qy^v#<5fG{*9l%UOjlVt!MN6$%}U6K9_lAB0j68RUaJ9n5AM1ER2QAAosp~1bY>PPWh*(jf`mp`Rioaf%F6U_|>Iyf^k zN$PuW#Ec^+V~Z#GbzQr#f8)~mKa_k9?+q2Ox#o3#^Kxr9_WqPC&94fThrQeGQWlx{j{BdTK_9>w*_329( z*BF|-5V~0GaY-y%z)r(O&ecNSqiIsi1H*GK56%u@+^M{9>%*3hDQ!mH`*!O8+h}uV z?bqk#uPbMrs;QMaJ%3)0Vr|@Bfpf{#wso`f&o6&?dFIDE60cr+JkBmUHA(o*)!^cl z`)Xfu_GMhWq^P+-OjQ8mH(f1PSF7_Iy5Ce-h-JFobbg>=w9lc*@vCR9+sBm; z{cPN1dEdV;l+ba@Si3CZ&V1)BI$|szKl+rM|vqjy3Bod zMHL&<;}0fI)QxdHvtYWQ{Wa5#%lV%$ZJqsX#WmcsYb~%T|5lXXf92rQ$=zk_}51WggI5WgPcLUJ^v{Zy57Py1Nrvk8ik%6bOAM&YjJAK#8c_>82E)uegw$zwfa9`%wK54}T|K^B@8&K2QOf+%&ScAl zx#vFLsdyMXS$@^aFGoJ|ep-;pq;2#(a*=Ya2>-#qlH$rSb5q!L8JZX$dvSBz70&Eh z*0kiY|JLt&KT4%Fh#p;3rte$u@5q$(%*81hE_++oe`sD?_D|r{kwOarcJUwsqoqok zVoP7Vsmi=D=aJebscMGTFIhGnxVEf@qJ(>JuI^SwR0iEsZxK@L78&xV@; zGr8Zy9XdQ?F^A9GtDScZf}GkqP5a-e?X%*m5uTN<#?~KH_1>3(!T7MJi(|-(6>DA} zI~ru#Xq&Gj=@BrgEpg^d{+h6~;eOa7@_R|k zoLRTxPIavbZ*IO?y6UBm`w^jCR}Zgff9*DHcgbu&>3{7LUd|BT?pdXxA}Mmaf_WK( z@i*UF>Qh52qfI>;kL@mguT#f0^TlVTbA@M?r+Mk?&CAtnU-mgh?5ktu*PwC)YDIF_I;4`ds-`d@p)L)=wYDBmH^GS(w+kAHI2^_dsu zUwQ99UujNX`sq1d&}!g$y@3SPla+0|NO`q)w6VJU}w8%kK6fM3tg%l zWei1rX~!M4TT~O_Tv95(=gQ`c6*Ke-ZZysPdAxcNTVSi*bH)IMNXfa2lJ|u@-@>r0 z^WIE#rUQB1YdO5Hob&jqFeg&fGwV|QpR;S1XKd&CXcH9Z(fP&2lyjSs!K%cZE5CcD z$x865%@7MkAa>z)!y{dXYNGc3V_4z8_W*PK1NP;v znVGz=lJgJtC3qsF zOzf@S+{dANOxZtFP32u#{BnkK^q!r9eSB&a${Md0uTt53M()sq3Er_$0aZ_1GZSXF zKVV64%H-WWS7LGEgb?nG^b_g_gEE`fyvc8cLvV92=c3`-3_FnMG%aV|S^ZeQ}n0wr)Rkyi(L}&*P+7j(IYf*Gn~*+}M?QcE>pbkBEz`h3{`M3%xAcAphPV zz*;tOZS(3R8Ov+Cti*2WxH#^!s8qJl)Dc;DF!1G+g!M5;o=sv9QjB=CWS8Hfos9n` zcro>H8)-DFN35tjv zT>ISO-~afPzqZVs9a5OG%Ku7?JI6-T1^54c=aw()zbm^$;A91VT4YzGm_mZff||PH zE*ECceKX^2(WSpN68}0E3km%&7kYbN+bbcfLp147k~IHo;TlB{&NuD=^4|KcczIiUp+^|YtptIk=s5y@V?vn zJEXE*v7_pwfeAM3K_?(sKBE1!E{Ue{U` zljYsLK^sfosoFMtdJ>f=$(iRR%$IWT+o!_c*M1u=yEf_j?Ty;c1*D$MJI?D+E`LC< zG$E-o= zJHX?hrXtGpYk_puOUY&BpABsGO7i}2P3Kq{Jxk?F#V!U56T!uE_wZ%)uHLfll~Miy zrd4w}`A%F|K2yKg$RvM*Q|PxJCpPT5#kfaKp{lFLL*!7%L1%9xze)F^Z(4IZRvn!` zQClr#E+fO{X{NR^e5H?_(r=}>G^LB{5x+CSp zN%q>dsyVkj#y*|L7yN&G44cAxqSPw? zIcHy#m1KI}cem|JAGCJ`Xl5})Y)_SJ^R_>Jby81vpm+$o1amRd_ni_}YhGu1EIVgC z``FW|JU8x4+OdlB{A5d}H7dQbw*uO=wKZni`ZjQ{nqtALX7U&^9r7 zPcg5R8&A#(ySGhJpQ$gbKq zofE)0#jjJ>cHOkqo1Q6f{pecb73fjl~;<_p^P)j zaxxQAP6mb8Ihgp)N-4GCn*aUUkK0$O`Zpc5PW^FY2~W9^`$M}OjQMjPc%R;sb!ubY zrsPnO19l%@p1O9+T0=LwK63BL`w5whzr0RuOkQHh>lfd+w6s`bb<~nYzAGl4-*e&3 zrR10KJC-R0y%%~{RNpkg#I3MlmFh8*W804jpX}N5d0@4I|sPz536^Gr$$UZ#QI+EHj0OFwIZ*8JI7Spm_d2KsyLj&mG6acYf@ zTGzD1Phz$VKTOVwE3Nc6c639F&zDyck=Ko#BZ9?WoLjIhVN=5IO5OIYY}EzXELwYW z6}~d<^9V59P$1EFoufv?oyDKc*fL*ARhxN9h3K!ZYTde1yV)E~Q@(8C=sthWd;Y@m zsI!g9$^K!?N+mk&))5tNJuiR#Df@2G@|{M>?V4{pEsvP2-j*WgKH=cU%1p&r?*m)- z?lm6Ic1x<6)_S|`v)oBGceUhACpRvw^ya&Ke2&=>-U8_s#n#!Oe+zHww@#_;5UBny z(bmr+FT21kE^fUK*AnY2hDoBSVvp}QH?S!~a1+yMb^k|V@aErsf&LNzGweH-z=Dt_)MlL}w0=g)mAh6OvA&i=3=RyttpvZ^%l?olzkYJ=?0fOm+gFv^oewB$W4|<6m|RdVSin>>@l9JoTN*>WrSijFGW{Fn?w`qx=wg0i zQf5ANq3sX868^*dIZkW_vu5;4aTi<{7POAHW}cDKb8g|W2`OE*%|f#$IY{(rANwJ8 z`Qf@#i4&q~Q}xap96zym?@IPZN=E-DG_X7};@P)6>(J&CEN&88MOlLaubnh5QIK%B z<^MwRM?qVo3Uh|Z8sqOR5&pg04fmQs{%_|%7pGiJ@Bl$AnzHpgF zm-(0YN-rmE1q&|**0h-`We(inzI}m9ansW9b;+OG^v?aujKS1L0wBkKoke@2k$Qw!F|9ckJNz z?fHVd`U$H~8$1&&X`b{k_gz=*t1N#q1nUU)6DX zI>B*KQkzOu5{KbjUWEsHYyJAB2fV+~rcN7uy0$QpEZB^vNPxI{@9|@lo~y=()R5%nG3fqa+B@-r)L%(WnH=Ni2VmA z$0Ji`Ea8xG@Y^UY6&3Px%F{O4J;zih6-9*9bp1&&n(CXGV)w{e<{Wq60pa}loC_Xy zTQPebmh9C!uNLW$^Vp>9u+NIidqnj*l(hLj$ylFSyCL4~W&>{lu06z77l z#4S2J_ub`x*&Gq=eHeNvdeO-<#~5Dk`8+E;>t)+1xzE+NxXxB*i?T48YF%OuWPZNY zI7E7akI@pJ!m8H!EkD>N>{)cCb^fju7gOhWIlj0x`9)=+>d2XXz~!Y}RSr_Eqr)h?>kXP(i$G4D|0i{8#c`&ET} zYV*=3r^FZuDe^6fsQBF?z1llNULjS|cSqC*t3sLY_Ko#ZKQ1^IHvRrQ-SABF#ZzQ9 z7?+%yDL*0V^U-rIzkHS#h0c|cwyry~^MUTQ2TNP6US3$imG+E9^{=8?;Y9t`^Xs3j zo|w2QF?^vzbk_2Pjm8$I{PNdSE!=ga>9b8mqlwd!zLtb7%94>?+oxSjOg^S{S!G&> z%Ji3w3%fFP9;Sc1#D6q-$ud*MbKN@{c3!+Zg@K`T366U*nZ zZk%xVokU`Tc}kpkGq>u`yQQyLUF~A}K7=czKYNsYQ+xl5%w_9t9<#W+$VOpxeMga$ zle`x9V&I%mME_-IY&hGK z(u*?v^NvrSDB==4ujfYW`!CE!k1|wA}i1B{4L_c zx9r%wZEMrl?AXMyVEy~_AkUWqaX#lSwlGUK8Y}l2q|B(iSEYNrPUUTu6SvL`L8}#f z+J7!AnPirEGvdBTY-{t%lrneq2g;8`?`>W1=BtZvP>SiS@-v9jT7)7UXze{bB?nR^XYz8+#KU`w01 zu(%?^$m!;W{Sqw(ySr@s6SM+f`#oJC`N!{4^XHkj4fK`?r6^}UcKag0UH<0uWR8hP z<~%*#QxTJ5a$v^dH;fB^=1jje^`JymPDwMLK|q?t)#Lr|ax1gG_Psxu84$8WxqZ8^ zm-Dfm+ExlHlHNrpbKaNfP)Ln4c`|Dv&+&Il{_nPaVJmoc!v+;=*ME!o-KJXR%18Jc zZVNRvHP@Q3DanM*A-}sgbX|_Y!3VXuSLOKq9D-*0r0Bj9ZjzI0KlypjMV`kTvNKi` zcUqN1E_9xCOU>(~0m3T6#%_MW>;`Yp^9!NN!}eMT{0`bP z+x3k9FP%2EOI=NOqbBz095palE6$jtb;SMb9!KVk*=rt1?$KRuDHpIL;z1wB$ zpBLYpJP{|~(9z2i`){-4Tgm)}hIEe`jFQ?W?=)F?;;vYk96z9UeCPXVc^{N=Z`|ZL zK6B5p6Y9U4W;&Z!uGC)4W$XUp=~14-4?YdSQPO?$s~MN7PxX8lcPw{S$t(3~8$Ql! z_E}`y%XC(Gjik*nQ_i%%0ry_%owyZvu)J+vS1hu&ICQ@}(vy&woFdDz$Su_N+3mRvi``@zG&XJP+I(Fq`rd(b zw;KjB8}IkHUs5u&UFg#ibY)V?p7>c4UpOwB$$RQ=Nl$?tPtuFef-PTf&h&V?bncU) zH|*I;+qp`X)Ti9T?N#WgGHHRxQMlX&C8>|*o^U@c#xb&dih3(^7^I4k< zo!m@#r{_o~T{>p!y+R_KJI~`}+pj%MY_qNMnd=_3W=Dn{KlOC!ku4_n=2MJ zol~;ku+gZ{%DT~HM}L-Mc?z4-+$x!6`xvE8JmN{*R~aidOW~_kMAsadTV5x7EIpW6 z{O{bhXH{A%^zh_AgKHJlZ~PdX>C$qzFZWi$hg9) z?Lgy62Q5aEHwVP$>2`~}3kf>Yl=FGU;d#&JZCO+u>C1L;nQuU#(X)?zD(4uNOuKf$ z^qYr9oo_l@#gY|sjxGGEw#{OKngQoBp|c<2XH7Wb@y=7)*mbY$%(6gn5#K{j-5p2E z3?lAa5zWzP4?OC7r-9$y{e;zOMjJz^l8l}S?gC={YdT{TPArSBY}etEc9vDK=Bv1jkhbN!HJbs{ryoK^ToCePdA+EdbnDfKlJ|}7B)c@B`ao2r61GEHe@y? z^m$xoe92V%v&8bq0{>Sz`IV~_pQpU5eK5x@WjDh{ckemQpKp0>*4qJ=B`p~Xuk5`%cxJg3VY0F zh+5#j+vMToB^B%|%TF?{{nWbLFuv_$s>G(kPc1wr=RKTy@0jSF&&QlAcBKCBeEzrkf36nk=FMrc#Tl0zZ^FFmekCvp6Kk zI_880&yMTovtezTCCeI@-aB`CTl3_bhpum%5Kwj6;i9UE%_O(yCKB#1%B~dOdthmcc1u%V=iWWZs~{Km?w8dUj)b;%7Uw2wtov8E zpr=Y|VaLRPoa>Wz>-uh*u#QzY?}tOxfvqPePH%s%C;oI5Cx5HKq-BY}JS5+7p9&~Z zd64O+@||Jh^dD0S-Dh0xJUDaL65$1{=UODrxmTVS`OGt4I9=IxWn)pGyt2#ADU*#m z4(2VZnPgvKxi)ya)A5f_9pcW4Oy!B6beiL|;>`tGyErT!-C3a`lFqv9M8rSl2N|5H z33oqKzAMW$DCWz0mWZwEo4lF{)uFqDq%@U0$adDt$)K zd!k}eX8Q)OLXY1n5(U#fZW3ItqQ0a=9g3XI4#mDg1rToS6-o zt5~{puJ)?+<}8m6THdCWY&pNC>YQ)#ith)X{+$}AKXcliW*M&|4HYeQLgk$y^=_TQ zV!tF0ypz)i$WfVhqQ)RZy=9N2#LSaoh0+(Y${t;u?U&5hFkP`&=KiHzlSe;Ver!^D zd9lCzkVyHS^7H_kzq;R7wOF((q!j*m_9)F~_afChmx84}TU0t$f*FwwZEeZ~sqI~+;b{?N2 z^NydL*;OdBEJdxkTO&n1$S67e#h!zXmP~;fVO6qj`keE59ln+{iL!BMER*V=w1RWy zQP0Jq@d_K(mTzh0nI~Uvc93OijKbqLFI`_XHJqBxvr+o(YEFM)<@U?o+jbuLsV#HA zl4ttXhp$^^bYJ}ZdN;H0gwsB{ZoGd3P6PxcY;MWgpX*}Plinh{ZIa-ppYOJNr1L$n z@YFk=slC&FZrRiX*3z6*$9#8F!QrBeS<@!) z|IhYp3$$0_OL)6vfBMFf)(~mSxvw;ZcAcs@V9VE*`y!L!^4y(%Yv(IwrtI63lRbZv zgX{SpH~Y&T%$ayFaQfix8SQ!> z{dLEu_$BV-=AEJBvDV1FFi8pHnbk*wE?2I*H}5(~?BJ<((zn+KKBAWL15! zYAcZm|I$43#_iJ#S6DYK)N(1Pby{~dChr@#M&mT1X8S+g0mTqOJ-Z2i| z2VH(^rzliAZb=lM!?Z8CC^gnc=}>$&QS~DWwsm=>6hlrP`&quN!v*1GRt0_(ygIega6E1WOUF#eXUnmCd#C75pAW-6G_QEkcGv0S*rBY&dO5Azia z83#iTXlEVi{$ZJVZ=S5Bmd4(b#ZroMgSjdlr%#!4!aef8(_De`ohr`?1yqi`X56AH zoxJSMZdRi;a@}P|eBK?l5kFSYY2CV=1FWB&KW z6Er<(gnpL{-{^T_0RCgNu; zQoNFHXUtl5V5wwxtT)ReliJqz*{vU}zZdTkb5%&*9rTsMQMWgtScWA*iD$a6U+O)z z3pESePq-^Pi3M=PSSs9ddF?18FeCO!Pmk(9SAUL>7t3A;t<-X=x!)_eF!4?wGwCC()^EHKc+`*x}JP;c}SSw>1JQyBiySQ znkz3JeIy*UyJE4^rMr*jN;U?4Tq=>WJmibQbdw9Bb9d-#eacwe-IexjiR!AU_fC8= zm@!S|s-?BBFXs*k3kPSLpEXPm4_0n@8>cn*zOj>a=98^={Z3UcY~TKrdCSVFHjEqM z*~F7$KcA}p^qQGPb(>K4kAp`fTKC`Jot8KCv82&QneT-~eB5knjx7A6Fk@@ednWbn z87oa}5_v1cU&wCGs-0FMveNK$O02VVUzqm9hL}#w+BfEWfj@?b4Cj&jAW6gOis?$i%%maO26uB=y91pIyU$q#O0NrXP!` zKihE9?$5mBx7$w}*vvoSxJxI&Mbnl2$VUI_!n&Re=C*=ZzT;INt|=StnigKlxG3n4 z0^@}2-D+yWYZCdsO1!>vboc24_68;KKH0VAF)L)+7Ihq#Z{$`fvGTE+(;k!+$$iQr z=Y`CK*UUBR<<5H`^+DqR*Rq%Zl}UWwWz0Gr^Un3~7iQ{63e4ttzD7EQ)Ax2* zn_ctNrBOF5CW{?%_?vX?>J$;?gUc(`7iN8Fm{a97mFKb1myImW;%m*$x9sX@y!LFT z=;Nk8-gbpoEK+$&ehGI6U$HhdTeY3RP~F*b?+M1DS2tfS(}{i+>o0t?MmTj|_?ZpT zj(*Dwu2xM|*OjOXSUdHJl8i^$A>$8$MTY@Rdjs$MK4T!3XC*ICS-chttHhRC++!Hm}=(3_6b| zOB~wJ%h<4uVVy3Q!e6}=57oOpiO(1cyAV$Pv||Faa48oo<%Q~?J8oddFHcDrfl&!bIBzA z9d8p)R|zD2V`Su+ETD1xr-h=${bN!2)pxY?jS5xen?4!N-^>=@=UJ1$a-;vMk$~Rw zcdYVrW^A0re8$i(U$DWWeG!v&OYM>e$GhD3+3ZTO*?;iB9*NfK9cdGGbX9Te&JjJ_ z)Zlb`+m^_M4_|IPky7broy0xIX8qoeGhNuT69dv!Qa7eK`agDMY|K$wr^$qfv86R8@XQ*z~r7>05I-=cd|J>}OMJoBqV-&X${Z(k>qi zUKzOXi0t;XC{}^FT}5Y%m@eqD)=0`7(i5`$^=SLFmDks_W|pZmo;$vcS!P}(o7{t| z#jkFOL@m8(VAsP~e`%B6n?TeYh`v#1EiyGjRb*`j@-_j=cC^Xd2QXnx?2d-dwZ zLGj|AN{dBOS2nJAe)PuMh&>$nQ?DqnTf68sx70M(mBc(bH+KgAyhXu5!HW(XUu2Gu zQjSh$RkN5hnKO`izDTl$5zoVF7S1VRyA{PZWEE%F`{k}QN}RHfP#U1;MAF6z2H4n}=_R>4(r#k=Q?#YpQ z#or^ddTyQ3Nbsr7I}m=-A&ikpOx5`4i=)PGLW55zpZnajc$?Lf9t|d+rB0Sx)Q(#% zesSlH%~^Ks2X7x`@(Bs;do`1Hw{q>QOK;MpW4M@BUZ40_MQW`Ao0Y{eE{i`o3zqAy z{gC^-s$g~jlkIt%6tPR*leetPVz?8%EAi@1x0uf}J0L+EO*U<{gL-Mz8_u@5&UK; z^DF+zmRhWTSols?eQ1~#^Zn_Yj~>8o{bjQ`Zqw%JrT30a644efRJyY3l+uBMfSlE$>AQGh6mrxbJW1!`GpoF1@K5k@ z#-+Zi4NGSnHVFF?>EzSZmb$CZ+Sl{>-scy~8W+x4-fJ@AK_J}Fj-0v#b8?!ub@oaiF<1-K2oNZT9J7ms3J#cA5 zM!w@T!?1$+OIEC(ppn_W>vHt{DK}Y`P3S7HnZzu??7X+=->ny4uJI)nZ%>&%+I{m*&HH%0h(qXJL!!L|vt|JnEM;$=)O zzOS!0^~xp*mC9wym%9roWgWP-zFR59XH#c{&DA$I_8ES@Rc*STX-p;=B&C+sAGmdlvOL`*UpR1fDZDFMpeHFY9gJ3|qhbi{p-}ce_Y?cU#6D zS+j_1dYI_`7N0GbPa3E+KU#fsYI2gnB|pO>_vF=bxyn6~_lkdyTz{r_E=$eszbaff|DV~Cc&+PKTt%`lK9NBX_Jg?|hJe+KvY1p;Zzo=WIO|Way zy6Nwpm!0O&zw$Hc(&^Wd{!TpxcJtq5Kby7f_uGPYtETDS{l#zXoN*?yPRi|LOkdve z^qs--$|Y%pgXvwj zq||OEty^Cv^nK;iUoyW{4{1E)^IZJ=TE?U+qVC=Ci&Gl|^QM`?Di|@A!32b6~r)MUrik zkcXkQQ0kn%g_%|g7d1oVG|FVV8#4aZ?E0%2{mJ+D?dk7-bRK(l>gt7WpMMrMG6bk( z$X_nDyz}huCC=+nmsD0yIsCI^>eBPiPw$Dny61FN)w=%uZ?Er>xwf=d^8KAATmSNd zJ+T|kyxbLcLcsOf+&%t2+YG;L(OsT8bNPDjO1Ac0OYdBG(>`m#``n*DtJU@Q?eDw( z)O7oV+L@BRvrI%J>tU31REyg5Pw*M;S zZ7CVsnBNcOb6m2Q826u*I?iq_b#&VN9;pN7k%|FNU#LyA`?zpAql@RKt46b4`ySnr z=iJz|%;Hk~2GQ^*v#ModuIyIR@%|~>u(ay2?N`e^*CWN=wXDtkzi_X>n+A!4dt|QP z-c+8!VVnNPm&@~X#G2VM-kaUcuWxZT@o`Wu@d{t5vM=Oe{C2a7%0G9Ljz3&q^T{ga z`r}ZGRra?eF3EWOK5XQx-4&So(RTlr&$sfws45>3RF#j6%;xzWnQYjwY3sWuYs94abBJIh>3;dLeSMQZuV@_qK1c6{R(^e;Lh4 z<`!J>zx=_>(-z-Wn{5cX#B@kS$aS&_r?k^9o-ZN_ZI8Cj%eHL$mt@%dmhINvN{^=^ z&zCOP_itO^Awy&4<_?if44>`IJ6cz@EZcurmZfRalACoqG!9sll{o#1Tt)jLsZ3-2qU$-Au-Jv8~{&e}>iqHR4wpwic`E`@Wzs~_%ElbSR)AG(; zs<`3Xd(28<`T@n}nETsZuQRC?_F3s~xP5V>$LaSU1m1sMosd!3%5dk!BQHmZ1&)!5 zuhL~x^GnZP+xD(#k(f)S{)r8HUm5d6WcE&s+;Z2Yahq@^XYkH->U*1?p1QN(>HKNz z4_b2VSLZ(#3>IGNV|PbL-DEpE+vhvs3Jp~(3+#R8a2pn~?Y;cvw|uO)aC_2`PeKd~ O3=E#GelF{r5}E*jcsk7h From b0e083d61da44613302b78ad1e3a2af2de5f054b Mon Sep 17 00:00:00 2001 From: nashmuhandes Date: Sat, 21 Jun 2025 22:07:41 +0800 Subject: [PATCH 216/384] Go back to the built-in dark theme for the launcher (for now) --- src/common/widgets/widgetresourcedata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/widgets/widgetresourcedata.cpp b/src/common/widgets/widgetresourcedata.cpp index a4e2c027a..849b6d553 100644 --- a/src/common/widgets/widgetresourcedata.cpp +++ b/src/common/widgets/widgetresourcedata.cpp @@ -23,7 +23,7 @@ void InitWidgetResources(const char* filename) if (!WidgetResources) I_FatalError("Unable to open %s", filename); - WidgetTheme::SetTheme(std::make_unique()); // GZDoom uses a different theme than VKDoom + WidgetTheme::SetTheme(std::make_unique()); } void CloseWidgetResources() From 94e73cbe1522ca961b402e90db298c9e33d72ced Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Mon, 23 Jun 2025 23:23:12 -0700 Subject: [PATCH 217/384] Set correct git tag in CI --- .github/workflows/continuous_integration.yml | 7 +++++- tools/updaterevision/UpdateRevision.cmake | 24 ++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index a5c2fb4e6..1321833f1 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -73,7 +73,11 @@ jobs: wget -q "https://github.com/coelckers/gzdoom/releases/download/ci_deps/${ZMUSIC_PACKAGE}" tar -xf "${ZMUSIC_PACKAGE}" fi - + + - name: Git describe + id: ghd + uses: proudust/gh-describe@v2 + - name: Configure shell: bash run: | @@ -82,6 +86,7 @@ jobs: - name: Build shell: bash run: | + export GIT_DESCRIBE="${{ steps.ghd.outputs.describe }}" export MAKEFLAGS=--keep-going cmake --build build --config ${{ matrix.config.build_type }} --parallel 3 diff --git a/tools/updaterevision/UpdateRevision.cmake b/tools/updaterevision/UpdateRevision.cmake index 619a86862..61da4c4bd 100755 --- a/tools/updaterevision/UpdateRevision.cmake +++ b/tools/updaterevision/UpdateRevision.cmake @@ -15,16 +15,20 @@ endmacro() # Populate variables "Hash", "Tag", and "Timestamp" with relevant information # from source repository. If anything goes wrong return something in "Error." function(query_repo_info) - execute_process( - COMMAND git describe --tags --dirty=-m - RESULT_VARIABLE Error - OUTPUT_VARIABLE Tag - ERROR_QUIET - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(NOT "${Error}" STREQUAL "0") - ret_var(Error) - return() + if(DEFINED ENV{GIT_DESCRIBE}) + set(Tag "$ENV{GIT_DESCRIBE}") + else() + execute_process( + COMMAND git describe --tags --dirty=-m + RESULT_VARIABLE Error + OUTPUT_VARIABLE Tag + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT "${Error}" STREQUAL "0") + ret_var(Error) + return() + endif() endif() execute_process( From 028256f06ffbdabc2b4677d7e604fb8860832ecb Mon Sep 17 00:00:00 2001 From: Cacodemon345 Date: Wed, 25 Jun 2025 00:23:45 +0600 Subject: [PATCH 218/384] WorldRailgunFired flags are now passed properly There's also a new DamageMobj flag to indicate railgun attacks without having to rely on damagetypes. --- src/playsim/p_local.h | 1 + src/playsim/p_map.cpp | 4 ++-- wadsrc/static/zscript/constants.zs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/playsim/p_local.h b/src/playsim/p_local.h index 6575ad3e5..f45aae071 100644 --- a/src/playsim/p_local.h +++ b/src/playsim/p_local.h @@ -456,6 +456,7 @@ enum EDmgFlags DMG_NO_PAIN = 1024, DMG_EXPLOSION = 2048, DMG_NO_ENHANCE = 4096, + DMG_RAILGUN = 8192, }; diff --git a/src/playsim/p_map.cpp b/src/playsim/p_map.cpp index 5b427e0f7..9cc66b98d 100644 --- a/src/playsim/p_map.cpp +++ b/src/playsim/p_map.cpp @@ -5587,7 +5587,7 @@ void P_RailAttack(FRailParams *p) if (puffDefaults->flags7 & MF7_FOILBUDDHA) dmgFlagPass |= DMG_FOILBUDDHA; } // [RK] If the attack source is a player, send the DMG_PLAYERATTACK flag. - int newdam = P_DamageMobj(hitactor, hitpuff ? hitpuff : source, source, p->damage, damagetype, dmgFlagPass | DMG_USEANGLE | (source->player ? DMG_PLAYERATTACK : 0), hitangle); + int newdam = P_DamageMobj(hitactor, hitpuff ? hitpuff : source, source, p->damage, damagetype, dmgFlagPass | DMG_USEANGLE | (source->player ? DMG_PLAYERATTACK : 0) | DMG_RAILGUN, hitangle); if (bleed) { @@ -5630,7 +5630,7 @@ void P_RailAttack(FRailParams *p) } } - source->Level->localEventManager->WorldRailgunFired(source, start, trace.HitPos, thepuff, flags); + source->Level->localEventManager->WorldRailgunFired(source, start, trace.HitPos, thepuff, p->flags); if (thepuff != NULL) { diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index cf1140a10..8bb1af77f 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -977,6 +977,7 @@ enum EDmgFlags DMG_NO_PAIN = 1024, DMG_EXPLOSION = 2048, DMG_NO_ENHANCE = 4096, + DMG_RAILGUN = 8192, } enum EReplace From 4fff12ec7834a24e2cc3c3dd17deda8dc3da1785 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 24 Jun 2025 18:37:18 -0400 Subject: [PATCH 219/384] Clean up network warnings --- src/g_statusbar/sbar.h | 4 ++-- src/g_statusbar/shared_sbar.cpp | 34 ++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/g_statusbar/sbar.h b/src/g_statusbar/sbar.h index 9141009c8..86ba90686 100644 --- a/src/g_statusbar/sbar.h +++ b/src/g_statusbar/sbar.h @@ -450,8 +450,8 @@ public: private: bool RepositionCoords (int &x, int &y, int xo, int yo, const int w, const int h) const; void DrawMessages (int layer, int bottom); - void DrawConsistancy () const; - void DrawWaiting () const; + double DrawConsistancy (double yOfs) const; + double DrawWaiting (double yOfs) const; TObjPtr Messages[NUM_HUDMSGLAYERS]; }; diff --git a/src/g_statusbar/shared_sbar.cpp b/src/g_statusbar/shared_sbar.cpp index 94715337f..16e611dad 100644 --- a/src/g_statusbar/shared_sbar.cpp +++ b/src/g_statusbar/shared_sbar.cpp @@ -1222,16 +1222,16 @@ void DBaseStatusBar::DrawTopStuff (EHudState state) DrawMessages (HUDMSGLayer_OverHUD, (state == HUD_StatusBar) ? GetTopOfStatusbar() : twod->GetHeight()); primaryLevel->localEventManager->RenderOverlay(state); - DrawConsistancy (); - DrawWaiting (); + double yOfs = DrawConsistancy (0.0); + DrawWaiting (yOfs); if ((ShowLog && MustDrawLog(state)) || (inter_subtitles && CPlayer->SubtitleCounter > 0)) DrawLog (); } -void DBaseStatusBar::DrawConsistancy() const +double DBaseStatusBar::DrawConsistancy(double yOfs) const { if (!netgame) - return; + return yOfs; bool desync = false; FString text = "Out of sync with:"; @@ -1243,21 +1243,21 @@ void DBaseStatusBar::DrawConsistancy() const // Fell out of sync with the host in packet server mode. Which specific user it is doesn't really matter. if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) { - text.AppendFormat(" %s (%d)", players[Net_Arbitrator].userinfo.GetName(10u), Net_Arbitrator + 1); + text = "Out of sync with host"; break; } else { - text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client + 1); + text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client); } } } + double y = yOfs; if (desync) { auto lines = V_BreakLines(SmallFont, twod->GetWidth() / CleanXfac - 40, text.GetChars()); const int height = SmallFont->GetHeight() * CleanYfac; - double y = 0.0; for (auto& line : lines) { DrawText(twod, SmallFont, CR_GREEN, @@ -1266,12 +1266,14 @@ void DBaseStatusBar::DrawConsistancy() const y += height; } } + + return y; } -void DBaseStatusBar::DrawWaiting() const +double DBaseStatusBar::DrawWaiting(double yOfs) const { if (!netgame) - return; + return yOfs; FString text = "Waiting for:"; bool isWaiting = false; @@ -1280,15 +1282,23 @@ void DBaseStatusBar::DrawWaiting() const if (players[client].waiting) { isWaiting = true; - text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client + 1); + if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator) + { + text = "Waiting for host"; + break; + } + else + { + text.AppendFormat(" %s (%d)", players[client].userinfo.GetName(10u), client); + } } } + double y = yOfs; if (isWaiting) { auto lines = V_BreakLines(SmallFont, twod->GetWidth() / CleanXfac - 40, text.GetChars()); const int height = SmallFont->GetHeight() * CleanYfac; - double y = 0.0; for (auto& line : lines) { DrawText(twod, SmallFont, CR_ORANGE, @@ -1297,6 +1307,8 @@ void DBaseStatusBar::DrawWaiting() const y += height; } } + + return y; } void DBaseStatusBar::NewGame () From 0427ac10f6411efaafb6088f0d5a438c8ebbf661 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:04:52 -0400 Subject: [PATCH 220/384] Fixed segfault under Solaris --- tools/zipdir/zipdir.c | 95 +------------------------------------------ 1 file changed, 1 insertion(+), 94 deletions(-) diff --git a/tools/zipdir/zipdir.c b/tools/zipdir/zipdir.c index 4ae81d8e3..4ab50929e 100644 --- a/tools/zipdir/zipdir.c +++ b/tools/zipdir/zipdir.c @@ -36,10 +36,8 @@ #define stat _stat #else #include -#if !defined(__sun) #include #endif -#endif #include #include #include @@ -499,103 +497,12 @@ dir_tree_t *add_dirs(char **argv) return trees; } -#elif defined(__sun) - -//========================================================================== -// -// add_dirs -// Solaris version -// -// Given NULL-terminated array of directory paths, create trees for them. -// -//========================================================================== - -void add_dir(dir_tree_t *tree, char* dirpath) -{ - DIR *directory = opendir(dirpath); - if(directory == NULL) - return; - - struct dirent *file; - while((file = readdir(directory)) != NULL) - { - if(file->d_name[0] == '.') //File is hidden or ./.. directory so ignore it. - continue; - - int isDirectory = 0; - int time = 0; - - char* fullFileName = malloc(strlen(dirpath) + strlen(file->d_name) + 1); - strcpy(fullFileName, dirpath); - strcat(fullFileName, file->d_name); - - struct stat *fileStat; - fileStat = malloc(sizeof(struct stat)); - stat(fullFileName, fileStat); - isDirectory = S_ISDIR(fileStat->st_mode); - time = fileStat->st_mtime; - free(stat); - - free(fullFileName); - - if(isDirectory) - { - char* newdir; - newdir = malloc(strlen(dirpath) + strlen(file->d_name) + 2); - strcpy(newdir, dirpath); - strcat(newdir, file->d_name); - strcat(newdir, "/"); - add_dir(tree, newdir); - free(newdir); - continue; - } - - file_entry_t *entry; - entry = alloc_file_entry(dirpath, file->d_name, time); - if (entry == NULL) - { - //no_mem = 1; - break; - } - entry->next = tree->files; - tree->files = entry; - } - - closedir(directory); -} - -dir_tree_t *add_dirs(char **argv) -{ - dir_tree_t *tree, *trees = NULL; - - int i = 0; - while(argv[i] != NULL) - { - tree = alloc_dir_tree(argv[i]); - tree->next = trees; - trees = tree; - - if(tree != NULL) - { - char* dirpath = malloc(sizeof(argv[i]) + 2); - strcpy(dirpath, argv[i]); - if(dirpath[strlen(dirpath)] != '/') - strcat(dirpath, "/"); - add_dir(tree, dirpath); - free(dirpath); - } - - i++; - } - return trees; -} - #else //========================================================================== // // add_dirs -// 4.4BSD version +// Misc POSIX vesion (eg 4.4BSD, Solaris) // // Given NULL-terminated array of directory paths, create trees for them. // From f6b4740be3172f8382c0b06011b78fd416ae937a Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:05:39 -0400 Subject: [PATCH 221/384] Fixed conflicting int defs under solaris --- tools/re2c/src/util/c99_stdint.h | 257 +------------------------------ 1 file changed, 2 insertions(+), 255 deletions(-) diff --git a/tools/re2c/src/util/c99_stdint.h b/tools/re2c/src/util/c99_stdint.h index 571860431..d9e0fe9f0 100644 --- a/tools/re2c/src/util/c99_stdint.h +++ b/tools/re2c/src/util/c99_stdint.h @@ -7,260 +7,7 @@ #include "config.h" #endif -#if HAVE_STDINT_H -# include -#else // HAVE_STDINT_H - -// A humble attempt to provide C99 compliant -// for environments that don't have it (e.g., MSVC 2003). -// -// First, we try to define exact-width integer types. We don't -// rely on any particular environment: instead, we search for -// a type of certain width in the following list: -// char (C89) -// short (C89) -// int (C89) -// long (C89) -// long long (C99) -// __int64 (MSVC-specific) -// (we consider even insane possibilities for simplicity). -// The size of each type is defined by autoconf in the form -// of a macro SIZEOF_ (set to 0 for nonexistent types). -// If we don't find a type with the required width, we don't -// define the corresponding exact-width C99 type at all. -// -// We define other types and constants based on exact-width -// types and C99 standard. -// -// We use SIZEOF_VOID_P to determine size of pointers. -// -// We use SIZEOF_0 to find suitable 64-bit integer -// constant suffix. - -// C99-7.18.1.1 Exact-width integer types - -// int8_t, uint8_t -#if SIZEOF_CHAR == 1 - typedef signed char int8_t; - typedef unsigned char uint8_t; -#elif SIZEOF_SHORT == 1 - typedef signed short int8_t; - typedef unsigned short uint8_t; -#elif SIZEOF_INT == 1 - typedef signed int int8_t; - typedef unsigned int uint8_t; -#elif SIZEOF_LONG == 1 - typedef signed long int8_t; - typedef unsigned long uint8_t; -#elif SIZEOF_LONG_LONG == 1 - typedef signed long long int8_t; - typedef unsigned long long uint8_t; -#elif SIZEOF___INT64 == 1 - typedef signed __int64 int8_t; - typedef unsigned __int64 uint8_t; -#endif - -// int16_t, uint16_t -#if SIZEOF_CHAR == 2 - typedef signed char int16_t; - typedef unsigned char uint16_t; -#elif SIZEOF_SHORT == 2 - typedef signed short int16_t; - typedef unsigned short uint16_t; -#elif SIZEOF_INT == 2 - typedef signed int int16_t; - typedef unsigned int uint16_t; -#elif SIZEOF_LONG == 2 - typedef signed long int16_t; - typedef unsigned long uint16_t; -#elif SIZEOF_LONG_LONG == 2 - typedef signed long long int16_t; - typedef unsigned long long uint16_t; -#elif SIZEOF___INT64 == 2 - typedef signed __int64 int16_t; - typedef unsigned __int64 uint16_t; -#endif - -// int32_t, uint32_t -#if SIZEOF_CHAR == 4 - typedef signed char int32_t; - typedef unsigned char uint32_t; -#elif SIZEOF_SHORT == 4 - typedef signed short int32_t; - typedef unsigned short uint32_t; -#elif SIZEOF_INT == 4 - typedef signed int int32_t; - typedef unsigned int uint32_t; -#elif SIZEOF_LONG == 4 - typedef signed long int32_t; - typedef unsigned long uint32_t; -#elif SIZEOF_LONG_LONG == 4 - typedef signed long long int32_t; - typedef unsigned long long uint32_t; -#elif SIZEOF___INT64 == 4 - typedef signed __int64 int32_t; - typedef unsigned __int64 uint32_t; -#endif - -// int64_t, uint64_t -#if SIZEOF_CHAR == 8 - typedef signed char int64_t; - typedef unsigned char uint64_t; -#elif SIZEOF_SHORT == 8 - typedef signed short int64_t; - typedef unsigned short uint64_t; -#elif SIZEOF_INT == 8 - typedef signed int int64_t; - typedef unsigned int uint64_t; -#elif SIZEOF_LONG == 8 - typedef signed long int64_t; - typedef unsigned long uint64_t; -#elif SIZEOF_LONG_LONG == 8 - typedef signed long long int64_t; - typedef unsigned long long uint64_t; -#elif SIZEOF___INT64 == 8 - typedef signed __int64 int64_t; - typedef unsigned __int64 uint64_t; -#endif - -// C99-7.18.1.2 Minimum-width integer types -typedef int8_t int_least8_t; -typedef int16_t int_least16_t; -typedef int32_t int_least32_t; -typedef int64_t int_least64_t; -typedef uint8_t uint_least8_t; -typedef uint16_t uint_least16_t; -typedef uint32_t uint_least32_t; -typedef uint64_t uint_least64_t; - -// C99-7.18.1.3 Fastest minimum-width integer types -typedef int8_t int_fast8_t; -typedef int16_t int_fast16_t; -typedef int32_t int_fast32_t; -typedef int64_t int_fast64_t; -typedef uint8_t uint_fast8_t; -typedef uint16_t uint_fast16_t; -typedef uint32_t uint_fast32_t; -typedef uint64_t uint_fast64_t; - -// C99-7.18.1.4 Integer types capable of holding object pointers -#if SIZEOF_VOID_P == 8 - typedef int64_t intptr_t; - typedef uint64_t uintptr_t; -#else - typedef int intptr_t; - typedef unsigned int uintptr_t; -#endif - -// C99-7.18.1.5 Greatest-width integer types -typedef int64_t intmax_t; -typedef uint64_t uintmax_t; - -#if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // See footnote 220 at page 257 and footnote 221 at page 259 - -// C99-7.18.2.1 Limits of exact-width integer types -#define INT8_MIN (-128) // -2^(8 - 1) -#define INT8_MAX 127 // 2^(8 - 1) - 1 -#define INT16_MIN (-32768) // -2^(16 - 1) -#define INT16_MAX 32767 // 2^(16 - 1) - 1 -#define INT32_MIN (-2147483648) // -2^(32 - 1) -#define INT32_MAX 2147483647 // 2^(32 - 1) - 1 -#define INT64_MIN (-9223372036854775808) // -2^(64 - 1) -#define INT64_MAX 9223372036854775807 // 2^(64 - 1) - 1 -#define UINT8_MAX 0xFF // 2^8 - 1 -#define UINT16_MAX 0xFFFF // 2^16 - 1 -#define UINT32_MAX 0xFFFFffff // 2^32 - 1 -#define UINT64_MAX 0xFFFFffffFFFFffff // 2^64 - 1 - -// C99-7.18.2.2 Limits of minimum-width integer types -#define INT_LEAST8_MIN INT8_MIN -#define INT_LEAST8_MAX INT8_MAX -#define INT_LEAST16_MIN INT16_MIN -#define INT_LEAST16_MAX INT16_MAX -#define INT_LEAST32_MIN INT32_MIN -#define INT_LEAST32_MAX INT32_MAX -#define INT_LEAST64_MIN INT64_MIN -#define INT_LEAST64_MAX INT64_MAX -#define UINT_LEAST8_MAX UINT8_MAX -#define UINT_LEAST16_MAX UINT16_MAX -#define UINT_LEAST32_MAX UINT32_MAX -#define UINT_LEAST64_MAX UINT64_MAX - -// C99-7.18.2.3 Limits of fastest minimum-width integer types -#define INT_FAST8_MIN INT8_MIN -#define INT_FAST8_MAX INT8_MAX -#define INT_FAST16_MIN INT16_MIN -#define INT_FAST16_MAX INT16_MAX -#define INT_FAST32_MIN INT32_MIN -#define INT_FAST32_MAX INT32_MAX -#define INT_FAST64_MIN INT64_MIN -#define INT_FAST64_MAX INT64_MAX -#define UINT_FAST8_MAX UINT8_MAX -#define UINT_FAST16_MAX UINT16_MAX -#define UINT_FAST32_MAX UINT32_MAX -#define UINT_FAST64_MAX UINT64_MAX - -// C99-7.18.2.4 Limits of integer types capable of holding object pointers -#define INTPTR_MIN (-32767) // -(2^15 - 1) -#define INTPTR_MAX 32767 // 2^15 - 1 -#define UINTPTR_MAX 0xFFFF // 2^16 - 1 - -// C99-7.18.2.5 Limits of greatest-width integer types -#define INTMAX_MIN (-9223372036854775807) // -(2^63 - 1) -#define INTMAX_MAX 9223372036854775807 // 2^63 - 1 -#define UINTMAX_MAX 0xFFFFffffFFFFffff // 2^64 - 1 - -// C99-7.18.3 Limits of other integer types: -// "An implementation shall define only the macros -// corresponding to those typedef names it actually -// provides" -// and footnote 222 at page 259: -// "A freestanding implementation need not provide -// all of these types." -// -// Since we don't define corresponding types, we don't -// define the following limits either: -// PTRDIFF_MIN -// PTRDIFF_MAX -// SIG_ATOMIC_MIN -// SIG_ATOMIC_MAX -// SIZE_MAX -// WCHAR_MIN -// WCHAR_MAX -// WINT_MIN -// WINT_MAX - -#endif // __STDC_LIMIT_MACROS - -#if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // See footnote 224 at page 260 - -// C99-7.18.4.1 Macros for minimum-width integer constants -#define INT8_C(x) x -#define UINT8_C(x) x##u -#define INT16_C(x) x -#define UINT16_C(x) x##u -#define INT32_C(x) x -#define UINT32_C(x) x##u -#if SIZEOF_0L == 8 -# define INT64_C(x) x##l -# define UINT64_C(x) x##ul -#elif SIZEOF_0LL == 8 -# define INT64_C(x) x##ll -# define UINT64_C(x) x##ull -#elif SIZEOF_0I8 == 8 -# define INT64_C(x) x##i8 -# define UINT64_C(x) x##ui8 -#else -# define INT64_C(x) x -# define UINT64_C(x) x##u -#endif - -// C99-7.18.4.2 Macros for greatest-width integer constants -#define INTMAX_C INT64_C -#define UINTMAX_C UINT64_C - -#endif // __STDC_CONSTANT_MACROS - -#endif // HAVE_STDINT_H +// since gzdoom is built with at minimum c++17, we know stdint.h is available +#include #endif // _RE2C_UTIL_C99_STDINT_ From 21a90ab7e138d1460a26bef10a83ed6e5438d36b Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:08:59 -0400 Subject: [PATCH 222/384] Added missing Solaris checks --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 92e7d6702..1088d2483 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +find_package(Threads REQUIRED) list( APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake ) include( FindPackageHandleStandardArgs ) @@ -302,14 +303,13 @@ else() set( REL_C_FLAGS "" ) set( DEB_C_FLAGS "" ) - if( APPLE ) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13") if( CMAKE_CXX_COMPILER_ID STREQUAL "GNU" ) # If we're compiling with a custom GCC on the Mac (which we know since g++-4.2 doesn't support C++11) statically link libgcc. set( ALL_C_FLAGS "-static-libgcc" ) endif() - elseif( NOT MINGW AND NOT HAIKU ) + elseif( NOT MINGW AND NOT HAIKU AND NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "SunOS" ) # Generic GCC/Clang requires position independent executable to be enabled explicitly set( ALL_C_FLAGS "${ALL_C_FLAGS} -fPIE" ) set( CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie" ) From a881e86ff7173babfa43391ccd8cc41af0afa124 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:11:22 -0400 Subject: [PATCH 223/384] Fixed typo --- src/common/engine/i_net.cpp | 2 +- src/common/platform/posix/sdl/i_main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index 773f8db92..fbab91f56 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -278,7 +278,7 @@ static void StartNetwork(bool autoPort) #ifndef __sun ioctlsocket(MySocket, FIONBIO, &trueVal); #else - fcntl(mysocket, F_SETFL, trueval | O_NONBLOCK); + fcntl(MySocket, F_SETFL, trueVal | O_NONBLOCK); #endif } diff --git a/src/common/platform/posix/sdl/i_main.cpp b/src/common/platform/posix/sdl/i_main.cpp index 4f29bb76e..2ecc14686 100644 --- a/src/common/platform/posix/sdl/i_main.cpp +++ b/src/common/platform/posix/sdl/i_main.cpp @@ -131,7 +131,7 @@ void I_DetectOS() break; } - utsname unameInfo; + struct utsname unameInfo; if (uname(&unameInfo) == 0) { From a4b7c951539f9e2f86653f23a943d7c11999730d Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:11:54 -0400 Subject: [PATCH 224/384] Added missing import for Solaris --- src/common/platform/posix/sdl/crashcatcher.c | 2 ++ src/common/platform/posix/sdl/i_system.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/common/platform/posix/sdl/crashcatcher.c b/src/common/platform/posix/sdl/crashcatcher.c index c88e0fe8a..f0924e461 100644 --- a/src/common/platform/posix/sdl/crashcatcher.c +++ b/src/common/platform/posix/sdl/crashcatcher.c @@ -5,6 +5,8 @@ #include #include #include +#include +#include #ifdef __linux__ #include diff --git a/src/common/platform/posix/sdl/i_system.cpp b/src/common/platform/posix/sdl/i_system.cpp index 2c28368b0..db793f04c 100644 --- a/src/common/platform/posix/sdl/i_system.cpp +++ b/src/common/platform/posix/sdl/i_system.cpp @@ -53,6 +53,10 @@ #include #endif +#if defined(__sun) || defined(__sun__) +#include +#endif + #include #include "version.h" From 525e6b9f9941036d2f3872e3dd4bc4cb53a7ccc7 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:13:15 -0400 Subject: [PATCH 225/384] Added _msize stub for Solaris --- src/common/utility/m_alloc.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/common/utility/m_alloc.cpp b/src/common/utility/m_alloc.cpp index 5884b48e8..b42cbf612 100644 --- a/src/common/utility/m_alloc.cpp +++ b/src/common/utility/m_alloc.cpp @@ -43,6 +43,15 @@ #else #include #endif +#if defined(__sun__) || defined(__sun) +size_t _msize(void* block) +{ + // TODO: find an actual fix for solaris + (void) block; + return 0; +} +#endif + #include "engineerrors.h" #include "dobjgc.h" @@ -54,7 +63,7 @@ #endif #ifndef _DEBUG -#if !defined(__solaris__) && !defined(__OpenBSD__) && !defined(__DragonFly__) +#if !defined(__solaris__) && !defined(__sun__) && !defined(_sun) && !defined(__OpenBSD__) && !defined(__DragonFly__) void *M_Malloc(size_t size) { void *block = malloc(size); @@ -127,7 +136,7 @@ void* M_Calloc(size_t v1, size_t v2) #include #endif -#if !defined(__solaris__) && !defined(__OpenBSD__) && !defined(__DragonFly__) +#if !defined(__solaris__) && !defined(__sun__) && !defined(_sun) && !defined(__OpenBSD__) && !defined(__DragonFly__) void *M_Malloc_Dbg(size_t size, const char *file, int lineno) { void *block = _malloc_dbg(size, _NORMAL_BLOCK, file, lineno); @@ -194,7 +203,7 @@ void M_Free (void *block) if (block != nullptr) { GC::ReportDealloc(_msize(block)); -#if !defined(__solaris__) && !defined(__OpenBSD__) && !defined(__DragonFly__) +#if !defined(__solaris__) && !defined(__sun__) && !defined(_sun) && !defined(__OpenBSD__) && !defined(__DragonFly__) free(block); #else free(((size_t*) block)-1); From 65ce7b7c298d64bacd7d0231039747c55d1d2f05 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 16 Jun 2025 16:13:41 -0400 Subject: [PATCH 226/384] Fixed conflicting import under solaris --- src/common/engine/serializer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/engine/serializer.cpp b/src/common/engine/serializer.cpp index 97ded1adf..73e3edf88 100644 --- a/src/common/engine/serializer.cpp +++ b/src/common/engine/serializer.cpp @@ -1052,6 +1052,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, uint32_t &value, uint3 // //========================================================================== +#if !defined(__sun) || !defined(__sun__) FSerializer& Serialize(FSerializer& arc, const char* key, char& value, char* defval) { int32_t vv = value; @@ -1060,6 +1061,7 @@ FSerializer& Serialize(FSerializer& arc, const char* key, char& value, char* def value = (int8_t)vv; return arc; } +#endif FSerializer &Serialize(FSerializer &arc, const char *key, int8_t &value, int8_t *defval) { From 128379195b111674b38257a5cd1b179facaf5169 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 17 Jun 2025 11:05:30 -0400 Subject: [PATCH 227/384] Added missing check for solaris --- src/common/engine/serializer.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/engine/serializer.h b/src/common/engine/serializer.h index d72e44440..b2120f0c1 100644 --- a/src/common/engine/serializer.h +++ b/src/common/engine/serializer.h @@ -230,7 +230,9 @@ public: FString mLumpName; }; +#if !defined(__sun) || !defined(__sun__) FSerializer& Serialize(FSerializer& arc, const char* key, char& value, char* defval); +#endif FSerializer &Serialize(FSerializer &arc, const char *key, bool &value, bool *defval); FSerializer &Serialize(FSerializer &arc, const char *key, int64_t &value, int64_t *defval); From 6b2b8a198f04d2aaf7c335f5eb736e40768f0667 Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Fri, 6 Jun 2025 08:49:08 -0400 Subject: [PATCH 228/384] - implement limits for state loops to prevent infinite state freezes --- src/playsim/p_mobj.cpp | 8 ++++++++ src/playsim/p_pspr.cpp | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 8b0ac17f4..24aec14e9 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -874,9 +874,17 @@ bool AActor::SetState (FState *newstate, bool nofunction) if (debugfile && player && (player->cheats & CF_PREDICTING)) fprintf (debugfile, "for pl %d: SetState while predicting!\n", Level->PlayerNum(player)); + int statelooplimit = 300000; auto oldstate = state; do { + if (!(--statelooplimit)) + { + Printf(TEXTCOLOR_RED "Infinite state loop in Actor '%s' state '%s'\n", GetClass()->TypeName.GetChars(), FState::StaticGetStateName(state).GetChars()); + state = nullptr; + Destroy(); + return false; + } if (newstate == NULL) { state = NULL; diff --git a/src/playsim/p_pspr.cpp b/src/playsim/p_pspr.cpp index 54ae4b6cf..d56e1868c 100644 --- a/src/playsim/p_pspr.cpp +++ b/src/playsim/p_pspr.cpp @@ -488,10 +488,20 @@ void DPSprite::SetState(FState *newstate, bool pending) WF_USER1OK | WF_USER2OK | WF_USER3OK | WF_USER4OK); } + int statelooplimit = 300000; + processPending = pending; do { + if (!(--statelooplimit)) + { + Printf(TEXTCOLOR_RED "Infinite state loop in weapon state '%s'\n", FState::StaticGetStateName(State).GetChars()); + State = nullptr; + Destroy(); + return; + } + if (newstate == nullptr) { // Object removed itself. Destroy(); From c36ea479a809028e2f53224227b924a3c8f31742 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 25 Jun 2025 12:33:09 -0400 Subject: [PATCH 229/384] Move engine verification to front end This can't be in the backend since it uses game-specific information. --- src/common/engine/i_net.cpp | 16 +++++++++------- src/d_net.cpp | 13 +++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index fbab91f56..f665271ad 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -76,7 +76,6 @@ #include "printf.h" #include "i_interface.h" #include "c_cvars.h" -#include "version.h" #include "i_net.h" #include "m_random.h" @@ -187,6 +186,9 @@ CUSTOM_CVAR(String, net_password, "", CVAR_IGNORE) } } +// Game-specific API +size_t Net_SetEngineInfo(uint8_t*& stream); +bool Net_VerifyEngine(uint8_t*& stream); void Net_SetupUserInfo(); const char* Net_GetClientName(int client, unsigned int charLimit); size_t Net_SetUserInfo(int client, uint8_t*& stream); @@ -785,6 +787,7 @@ static bool Host_CheckForConnections(void* connected) if (RemoteClient >= 0) continue; + uint8_t* engineInfo = &NetBuffer[2]; size_t banned = 0u; for (; banned < BannedConnections.Size(); ++banned) { @@ -796,7 +799,7 @@ static bool Host_CheckForConnections(void* connected) { RejectConnection(from, PRE_BANNED); } - else if (NetBuffer[2] % 256 != VER_MAJOR || NetBuffer[3] % 256 != VER_MINOR || NetBuffer[4] % 256 != VER_REVISION) + else if (!Net_VerifyEngine(engineInfo)) { RejectConnection(from, PRE_WRONG_ENGINE); } @@ -1181,12 +1184,11 @@ static bool Guest_ContactHost(void* unused) if (consoleplayer == -1) { NetBuffer[1] = PRE_CONNECT; - NetBuffer[2] = VER_MAJOR % 256; - NetBuffer[3] = VER_MINOR % 256; - NetBuffer[4] = VER_REVISION % 256; + uint8_t* engineInfo = &NetBuffer[2]; + const size_t end = 2u + Net_SetEngineInfo(engineInfo); const size_t passSize = strlen(net_password) + 1; - memcpy(&NetBuffer[5], net_password, passSize); - NetBufferLength = 5u + passSize; + memcpy(&NetBuffer[end], net_password, passSize); + NetBufferLength = end + passSize; SendPacket(Connected[0].Address); } else diff --git a/src/d_net.cpp b/src/d_net.cpp index b92831db9..91bbfa840 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1651,6 +1651,19 @@ void NetUpdate(int tics) // from the frontend should be put in these, all backend handling should be // done in the core files. +size_t Net_SetEngineInfo(uint8_t*& stream) +{ + stream[0] = VER_MAJOR % 256; + stream[1] = VER_MINOR % 256; + stream[2] = VER_REVISION % 256; + return 3u; +} + +bool Net_VerifyEngine(uint8_t*& stream) +{ + return stream[0] == (VER_MAJOR % 256) && stream[1] == (VER_MINOR % 256) && stream[2] == (VER_REVISION % 256); +} + void Net_SetupUserInfo() { D_SetupUserInfo(); From fc7a480fe8c301a135ff48df04810405156fb2ea Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 28 Jun 2025 12:19:26 -0400 Subject: [PATCH 230/384] Tweak tic catchup mechanic Always chase the available tics. --- src/d_net.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 91bbfa840..7ffc55510 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -1990,7 +1990,7 @@ void TryRunTics() // If the amount of tics to run is falling behind the amount of available tics, // speed the playsim up a bit to help catch up. int runTics = min(totalTics, availableTics); - if (totalTics > 0 && totalTics < availableTics - 1 && !singletics) + if (totalTics > 0 && totalTics < availableTics && !singletics) ++runTics; // Test player prediction code in singleplayer From fae43b8120509f3ec3e6a495f0bdb54b96e7db7f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Fri, 30 May 2025 23:18:04 -0400 Subject: [PATCH 231/384] Added ready system to screen jobs for multiplayer Readds the feature to allow players to ready up during stat screens and intermissions instead of autoskipping based on whoever closed it. Comes with a variety of ways to tweak this behavior such as percentage-based auto starting (with a timer), the ability to unready as needed, and who can control it. Players will still be able to skip through individual screen jobs within the runner while waiting to ready up. --- src/common/cutscenes/screenjob.cpp | 6 +- src/common/cutscenes/screenjob.h | 9 +- src/d_net.cpp | 174 +++++++++++++++++- src/d_net.h | 2 + src/d_protocol.h | 3 +- src/g_game.cpp | 44 ++++- src/g_game.h | 2 +- src/g_level.cpp | 7 +- src/intermission/intermission_parse.cpp | 2 +- src/p_tick.cpp | 5 + wadsrc/static/menudef.txt | 10 + wadsrc/static/zscript/engine/screenjob.zs | 59 +++++- wadsrc/static/zscript/ui/intermission.zs | 82 +++++++++ .../zscript/ui/statscreen/statscreen.zs | 2 +- .../zscript/ui/statscreen/statscreen_coop.zs | 63 ++++++- .../zscript/ui/statscreen/statscreen_dm.zs | 62 ++++++- 16 files changed, 493 insertions(+), 39 deletions(-) diff --git a/src/common/cutscenes/screenjob.cpp b/src/common/cutscenes/screenjob.cpp index 6220291ea..edad781d9 100644 --- a/src/common/cutscenes/screenjob.cpp +++ b/src/common/cutscenes/screenjob.cpp @@ -123,12 +123,12 @@ void CallCreateFunction(const char* qname, DObject* runner) // //============================================================================= -DObject* CreateRunner(bool clearbefore) +DObject* CreateRunner(bool clearbefore, int skipType) { auto obj = cutscene.runnerclass->CreateNew(); auto func = LookupFunction("ScreenJobRunner.Init", false); - VMValue val[3] = { obj, clearbefore, false }; - VMCall(func, val, 3, nullptr, 0); + VMValue val[4] = { obj, clearbefore, false, skipType }; + VMCall(func, val, 4, nullptr, 0); return obj; } diff --git a/src/common/cutscenes/screenjob.h b/src/common/cutscenes/screenjob.h index dce55e3b0..e619f46d9 100644 --- a/src/common/cutscenes/screenjob.h +++ b/src/common/cutscenes/screenjob.h @@ -19,6 +19,13 @@ enum SJ_BLOCKUI = 1, }; +enum +{ + ST_VOTE, + ST_MUST_BE_SKIPPABLE, + ST_UNSKIPPABLE, +}; + struct CutsceneDef { FString video; @@ -47,7 +54,7 @@ bool CanWipe(); VMFunction* LookupFunction(const char* qname, bool validate = true); void CallCreateFunction(const char* qname, DObject* runner); -DObject* CreateRunner(bool clearbefore = true); +DObject* CreateRunner(bool clearbefore = true, int skipType = ST_VOTE); void AddGenericVideo(DObject* runner, const FString& fn, int soundid, int fps); struct CutsceneState diff --git a/src/d_net.cpp b/src/d_net.cpp index 7ffc55510..0095d0f31 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -96,6 +96,13 @@ enum ELevelStartStatus LST_WAITING, }; +enum EReadyType +{ + RT_VOTE, + RT_ANYONE, + RT_HOST_ONLY, +}; + // NETWORKING // // gametic is the tic about to (or currently being) run. @@ -120,6 +127,9 @@ static uint8_t CurrentLobbyID = 0u; // Ignore commands not from this lobby (usef static int LastGameUpdate = 0; // Track the last time the game actually ran the world. static uint64_t MutedClients = 0u; // Ignore messages from these clients. +static int CutsceneCountdown = 0; // If enough people are ready, count down the timer. This won't reset between unreadies, only on intermission entrance. +static uint64_t CutsceneReady = 0u; // If in a cutscene, check if we're ready to move to move past it. + static int LevelStartDebug = 0; static int LevelStartDelay = 0; // While this is > 0, don't start generating packets yet. static ELevelStartStatus LevelStartStatus = LST_READY; // Listen for when to actually start making tics. @@ -151,6 +161,21 @@ CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently de CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_disablepause, false, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE) +CUSTOM_CVAR(Int, net_cutscenereadytype, RT_VOTE, CVAR_SERVERINFO | CVAR_NOSAVE) +{ + if (self < RT_VOTE) + self = RT_VOTE; + else if (self > RT_HOST_ONLY) + self = RT_HOST_ONLY; +} +CUSTOM_CVAR(Float, net_cutscenereadypercent, 0.5f, CVAR_SERVERINFO | CVAR_NOSAVE) +{ + if (self < 0.0f) + self = 0.0f; + else if (self > 1.0f) + self = 1.0f; +} +CVAR(Float, net_cutscenecountdown, 30.0f, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, cl_noboldchat, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Bool, cl_nochatsound, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) @@ -363,6 +388,9 @@ void Net_ClearBuffers() gametic = ClientTic = 0; SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0; NetEvents.ResetStream(); + + CutsceneReady = 0u; + CutsceneCountdown = 0; bCommandsReset = false; LevelStartAck = 0u; @@ -376,6 +404,91 @@ void Net_ClearBuffers() NetworkClients += 0; } +bool Net_IsPlayerReady(int player) +{ + if (demoplayback || net_cutscenereadytype != RT_VOTE) + return false; + + if (cutscene.runner) + { + int type = ST_VOTE; + IFVM(ScreenJobRunner, GetSkipType) + type = VMCallSingle(func, cutscene.runner); + + if (type == ST_UNSKIPPABLE) + return false; + } + + return players[player].Bot != nullptr || (CutsceneReady & ((uint64_t)1u << player)); +} + +// Check if every client is ready to move on from the current cutscene. +void Net_PlayerReadiedUp(int player) +{ + if (!netgame || demoplayback) + return; + + // Allow unreadying in case a player needs to leave momentarily. + if (Net_IsPlayerReady(player)) + CutsceneReady &= ~((uint64_t)1u << player); + else + CutsceneReady |= (uint64_t)1u << player; +} + +void Net_StartCutscene() +{ + CutsceneCountdown = netgame && !demoplayback && net_cutscenecountdown > 0.0f ? static_cast(ceil(net_cutscenecountdown * TICRATE)) : 0; +} + +// Allow the game to automatically start after a set amount of time. +bool Net_CheckCutsceneReady() +{ + if (!cutscene.runner) + return false; + + int type = ST_VOTE; + IFVM(ScreenJobRunner, GetSkipType) + type = VMCallSingle(func, cutscene.runner); + + if (type == ST_UNSKIPPABLE) + return false; + + if (net_cutscenereadytype == RT_ANYONE) + return CutsceneReady != 0; + + if (net_cutscenereadytype == RT_HOST_ONLY) + return (CutsceneReady & ((uint64_t)1u << Net_Arbitrator)); + + uint64_t mask = 0u; + int totalReady = 0; + // Bots will be automatically assumed to be ready, so we don't include them. + for (auto client : NetworkClients) + { + mask |= (uint64_t)1u << client; + totalReady += Net_IsPlayerReady(client); + } + + if ((CutsceneReady & mask) == mask) + return true; + + if ((float)totalReady / NetworkClients.Size() < net_cutscenereadypercent) + return false; + + if (CutsceneCountdown <= 0) + return true; + + --CutsceneCountdown; + return false; +} + +void Net_AdvanceCutscene() +{ + CutsceneReady = 0u; + CutsceneCountdown = 0; + if (consoleplayer == Net_Arbitrator) + Net_WriteInt8(DEM_ENDSCREENJOB); +} + void Net_ResetCommands(bool midTic) { bCommandsReset = midTic; @@ -554,7 +667,10 @@ static void ClientConnecting(int client) static void DisconnectClient(int clientNum) { NetworkClients -= clientNum; - MutedClients &= ~((uint64_t)1u << clientNum); + const uint64_t mask = ~((uint64_t)1u << clientNum); + MutedClients &= mask; + CutsceneReady &= mask; + LevelStartAck &= mask; I_ClearClient(clientNum); // Capture the pawn leaving in the next world tick. players[clientNum].playerstate = PST_GONE; @@ -1965,7 +2081,8 @@ void TryRunTics() // Listen for other clients and send out data as needed. This is also // needed for singleplayer! But is instead handled entirely through local - // buffers. This has a limit of 17 tics that can be generated. + // buffers. This has a limit of one seconds worth of commands that can be + // generated in advanced from the last time the game updated. NetUpdate(totalTics); LastEnterTic = EnterTic; @@ -2746,6 +2863,10 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player) EndScreenJob(); break; + case DEM_READIED: + Net_PlayerReadiedUp(player); + break; + case DEM_ZSC_CMD: { FName cmd = ReadStringConst(stream); @@ -2987,6 +3108,55 @@ int Net_GetLatency(int* localDelay, int* arbitratorDelay) // //========================================================================== +// Intermission lobby info +static int IsPlayerReady(int player) +{ + return Net_IsPlayerReady(player); +} + +DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, IsPlayerReady, IsPlayerReady) +{ + PARAM_PROLOGUE; + PARAM_INT(player); + ACTION_RETURN_BOOL(IsPlayerReady(player)); +} + +static void ReadyPlayer() +{ + if (netgame && !demoplayback) + Net_WriteInt8(DEM_READIED); +} + +DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, ReadyPlayer, ReadyPlayer) +{ + PARAM_PROLOGUE; + ReadyPlayer(); + return 0; +} + +static void ResetReadyTimer() +{ + Net_StartCutscene(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, ResetReadyTimer, ResetReadyTimer) +{ + PARAM_PROLOGUE; + ResetReadyTimer(); + return 0; +} + +static int GetReadyTimer() +{ + return CutsceneCountdown; +} + +DEFINE_ACTION_FUNCTION_NATIVE(_ScreenJobRunner, GetReadyTimer, GetReadyTimer) +{ + PARAM_PROLOGUE; + ACTION_RETURN_INT(GetReadyTimer()); +} + // [RH] List "ping" times CCMD(pings) { diff --git a/src/d_net.h b/src/d_net.h index 8b2b6649d..b62f4be05 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -151,6 +151,8 @@ void Net_WriteBytes(const uint8_t *, int len); void Net_DoCommand(int cmd, uint8_t **stream, int player); void Net_SkipCommand(int cmd, uint8_t **stream); +bool Net_CheckCutsceneReady(); +void Net_AdvanceCutscene(); void Net_ResetCommands(bool midTic); void Net_SetWaiting(); void Net_ClearBuffers(); diff --git a/src/d_protocol.h b/src/d_protocol.h index 10b38f801..f4388d3bf 100644 --- a/src/d_protocol.h +++ b/src/d_protocol.h @@ -163,10 +163,11 @@ enum EDemoCommand DEM_NETEVENT, // 70 String: Event name, Byte: Arg count; each arg is a 4-byte int DEM_MDK, // 71 String: Damage type DEM_SETINV, // 72 SetInventory - DEM_ENDSCREENJOB, + DEM_ENDSCREENJOB, // 73 DEM_ZSC_CMD, // 74 String: Command, Word: Byte size of command DEM_CHANGESKILL, // 75 Int: Skill DEM_KICK, // 76 Byte: Player number + DEM_READIED, // 77 }; // The following are implemented by cht_DoCheat in m_cheat.cpp diff --git a/src/g_game.cpp b/src/g_game.cpp index 703cac8bb..6e4b445dd 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -1104,6 +1104,37 @@ static void G_FullConsole() } +void D_RunCutscene() +{ + // Only single player games can cancel out of the screen job via client-side logic. + if (ScreenJobTick() && !demoplayback) + { + if (netgame) + { + // Only the host can determine this. + if (consoleplayer != Net_Arbitrator) + return; + + int type = ST_VOTE; + IFVM(ScreenJobRunner, GetSkipType) + type = VMCallSingle(func, cutscene.runner); + + if (type != ST_UNSKIPPABLE) + return; + } + + Net_WriteInt8(DEM_ENDSCREENJOB); + } +} + +// This is used to allow the server to check for when players are ready to advance. For singleplayer we can just +// use the net message from the cutscene finishing to know when to go. +static void D_CheckCutsceneAdvance() +{ + if (netgame && !demoplayback && Net_CheckCutsceneReady()) + Net_AdvanceCutscene(); +} + // // G_Ticker // Make ticcmd_ts for the players. @@ -1205,7 +1236,7 @@ void G_Ticker () C_AdjustBottom (); } - // get commands, check consistancy, and build new consistancy check + // get commands const int curTic = gametic / TicDup; //Added by MC: For some of that bot stuff. The main bot function. @@ -1221,9 +1252,6 @@ void G_Ticker () G_WriteDemoTiccmd(nextCmd, client, curTic); players[client].oldbuttons = cmd->buttons; - // If the user alt-tabbed away, paused gets set to -1. In this case, - // we do not want to read more demo commands until paused is no - // longer negative. if (demoplayback) G_ReadDemoTiccmd(cmd, client); else @@ -1261,11 +1289,7 @@ void G_Ticker () case GS_CUTSCENE: case GS_INTRO: - if (ScreenJobTick()) - { - // synchronize termination with the playsim. - Net_WriteInt8(DEM_ENDSCREENJOB); - } + D_CheckCutsceneAdvance(); break; default: @@ -3044,7 +3068,7 @@ void G_StartSlideshow(FLevelLocals *Level, FName whichone, int state) { auto SelectedSlideshow = whichone == NAME_None ? Level->info->slideshow : whichone; auto slide = F_StartIntermission(SelectedSlideshow, state); - RunIntermission(nullptr, nullptr, slide, nullptr, [](bool) + RunIntermission(nullptr, nullptr, slide, nullptr, false, [](bool) { primaryLevel->SetMusic(); gamestate = GS_LEVEL; diff --git a/src/g_game.h b/src/g_game.h index 208e91dcb..fcb9a1974 100644 --- a/src/g_game.h +++ b/src/g_game.h @@ -116,7 +116,7 @@ FBaseCVar* G_GetUserCVar(int playernum, const char* cvarname); class DIntermissionController; struct level_info_t; -void RunIntermission(level_info_t* oldlevel, level_info_t* newlevel, DIntermissionController* intermissionScreen, DObject* statusScreen, std::function completionf); +void RunIntermission(level_info_t* oldlevel, level_info_t* newlevel, DIntermissionController* intermissionScreen, DObject* statusScreen, bool ending, std::function completionf); extern const AActor *SendItemUse, *SendItemDrop; extern int SendItemDropAmount; diff --git a/src/g_level.cpp b/src/g_level.cpp index 4367d4b1c..f5921948e 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1048,9 +1048,10 @@ DIntermissionController* FLevelLocals::CreateIntermission() // //============================================================================= -void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, std::function completionf) +void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, bool ending, std::function completionf) { - cutscene.runner = CreateRunner(false); + // Make sure the finale can't be skipped, otherwise the intermission always needs to be skippable. + cutscene.runner = CreateRunner(false, ending ? ST_UNSKIPPABLE : ST_MUST_BE_SKIPPABLE); GC::WriteBarrier(cutscene.runner); cutscene.completion = std::move(completionf); @@ -1139,7 +1140,7 @@ void G_DoCompleted (void) bool endgame = strncmp(nextlevel.GetChars(), "enDSeQ", 6) == 0; intermissionScreen = primaryLevel->CreateIntermission(); auto nextinfo = !playinter || endgame? nullptr : FindLevelInfo(nextlevel.GetChars(), false); - RunIntermission(primaryLevel->info, nextinfo, intermissionScreen, statusScreen, [=](bool) + RunIntermission(primaryLevel->info, nextinfo, intermissionScreen, statusScreen, endgame, [=](bool) { if (!endgame) primaryLevel->WorldDone(); else D_StartTitle(); diff --git a/src/intermission/intermission_parse.cpp b/src/intermission/intermission_parse.cpp index c27f99e4d..211653011 100644 --- a/src/intermission/intermission_parse.cpp +++ b/src/intermission/intermission_parse.cpp @@ -991,6 +991,6 @@ CCMD(testfinale) } auto controller = F_StartFinale(gameinfo.finaleMusic.GetChars(), gameinfo.finaleOrder, -1, 0, gameinfo.FinaleFlat.GetChars(), text, false, false, true, true); - RunIntermission(nullptr, nullptr, controller, nullptr, [=](bool) { gameaction = ga_nothing; }); + RunIntermission(nullptr, nullptr, controller, nullptr, false, [=](bool) { gameaction = ga_nothing; }); } diff --git a/src/p_tick.cpp b/src/p_tick.cpp index 18589a01b..dee5fd56e 100644 --- a/src/p_tick.cpp +++ b/src/p_tick.cpp @@ -46,6 +46,7 @@ extern uint8_t globalfreeze, globalchangefreeze; void C_Ticker(); void M_Ticker(); +void D_RunCutscene(); //========================================================================== // @@ -92,6 +93,10 @@ void P_RunClientsideLogic() if (gamestate == GS_LEVEL) primaryLevel->automap->Ticker(); } + else if (gamestate == GS_CUTSCENE || gamestate == GS_INTRO) + { + D_RunCutscene(); + } // [MK] Additional ticker for UI events right after all others primaryLevel->localEventManager->PostUiTick(); diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 44e324612..4e7413357 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2441,10 +2441,20 @@ OptionMenu NetworkOptions protected StaticText "$NETMNU_HOSTOPTIONS", 1 Option "$NETMNU_EXTRATICS", "net_extratic", "OnOff" Option "$NETMNU_DISABLEPAUSE", "net_disablepause", "OnOff" + Option "$NETMNU_READYTYPE", "net_cutscenereadytype", "ReadyTypes" + Slider "$NETMNU_READYPERCENT", "net_cutscenereadypercent", "0", "1", "0.05", 2 + NumberField "$NETMNU_READYCOUNTDOWN", "net_cutscenecountdown", 0, 300, 5 NumberField "$NETMNU_CHATSLOMO", "net_chatslowmode", 0, 300, 5 } +OptionValue "ReadyTypes" +{ + 0, "$OPTVAL_VOTE" + 1, "$OPTVAL_ANYONE" + 2, "$OPTVAL_HOSTONLY" +} + OptionValue "ChatTypes" { 0, "$OPTVAL_DISABLECHAT" diff --git a/wadsrc/static/zscript/engine/screenjob.zs b/wadsrc/static/zscript/engine/screenjob.zs index dc50f5535..fb9fd9c29 100644 --- a/wadsrc/static/zscript/engine/screenjob.zs +++ b/wadsrc/static/zscript/engine/screenjob.zs @@ -321,6 +321,22 @@ class ScreenJobRunner : Object UI State_Run, State_Fadeout, } + enum ESkipType + { + ST_VOTE, + ST_MUST_BE_SKIPPABLE, + ST_UNSKIPPABLE, + } + enum EInputType + { + INP_KEYBOARD_MOUSE, + INP_CONTROLLER, + INP_JOYSTICK, + } + + private ESkipType skipType; + private EInputType lastInput; + Array jobs; //CompletionFunc completion; int index; @@ -335,13 +351,49 @@ class ScreenJobRunner : Object UI native static void setTransition(int type); - void Init(bool clearbefore_, bool skipall_) + ESkipType GetSkipType() const + { + return skipType; + } + + EInputType GetLastInputType() const + { + return lastInput; + } + + protected void SetLastInputType(InputEvent ev) + { + if (ev.Type == InputEvent.Type_Mouse) + { + lastInput = INP_KEYBOARD_MOUSE; + } + else if (ev.Type == InputEvent.Type_KeyDown) + { + if (ev.KeyScan >= InputEvent.Key_Pad_LThumb_Right && ev.KeyScan <= InputEvent.Key_Pad_Y) + { + lastInput = INP_CONTROLLER; + } + else if ((ev.KeyScan >= InputEvent.Key_Joy1 && ev.KeyScan <= InputEvent.Key_JoyPOV4_Up) + || (ev.KeyScan >= InputEvent.Key_JoyAxis1Plus && ev.KeyScan <= InputEvent.Key_JoyAxis8Minus)) + { + lastInput = INP_JOYSTICK; + } + else + { + lastInput = INP_KEYBOARD_MOUSE; + } + } + } + + void Init(bool clearbefore_, bool skipall_, ESkipType type = ST_VOTE) { clearbefore = clearbefore_; skipall = skipall_; index = -1; fadeticks = 0; last_paused_tic = -1; + skipType = type; + ResetReadyTimer(); } override void OnDestroy() @@ -418,6 +470,7 @@ class ScreenJobRunner : Object UI { if (jobs.Size() == 0) { + DrawReadiedPlayers(smoothratio); return 1; } int x = index >= jobs.Size()? jobs.Size()-1 : index; @@ -433,6 +486,7 @@ class ScreenJobRunner : Object UI } int state = job.DrawFrame(smoothratio); Screen.SetScreenFade(1.); + DrawReadiedPlayers(smoothratio); return state; } @@ -462,6 +516,9 @@ class ScreenJobRunner : Object UI virtual bool OnEvent(InputEvent ev) { + SetLastInputType(ev); + + if (ConsumedInput(ev)) return true; if (paused || index < 0 || index >= jobs.Size()) return false; if (jobs[index].jobstate != ScreenJob.running) return false; return jobs[index].OnEvent(ev); diff --git a/wadsrc/static/zscript/ui/intermission.zs b/wadsrc/static/zscript/ui/intermission.zs index 657be98ba..b42616c57 100644 --- a/wadsrc/static/zscript/ui/intermission.zs +++ b/wadsrc/static/zscript/ui/intermission.zs @@ -1,4 +1,86 @@ +extend class ScreenJobRunner +{ + protected native static void ReadyPlayer(); + protected native static void ResetReadyTimer(); + + native static int GetReadyTimer(); + native static bool IsPlayerReady(int pNum); + + bool ConsumedInput(InputEvent evt) + { + if (netgame && evt.type == InputEvent.Type_KeyDown + && (evt.KeyScan == InputEvent.Key_Space || evt.KeyScan == InputEvent.Key_Mouse1 + || evt.KeyScan == InputEvent.Key_Pad_A || evt.KeyScan == InputEvent.Key_Joy1)) + { + ReadyPlayer(); + return true; + } + + return false; + } + + void DrawReadiedPlayers(double smoothratio) + { + if (!netgame || GetSkipType() == ST_UNSKIPPABLE) + return; + + if (net_cutscenereadytype == 0) + { + int totalClients, readyClients; + for (int i; i < MAXPLAYERS; ++i) + { + if (!playerInGame[i] || players[i].Bot) + continue; + + ++totalClients; + readyClients += IsPlayerReady(i); + } + + if (totalClients > 1) + { + TextureID readyico = TexMan.CheckForTexture("READYICO", TexMan.Type_MiscPatch); + Vector2 readysize = TexMan.GetScaledSize(readyico); + + if (IsPlayerReady(consoleplayer)) + Screen.DrawTexture(readyico, true, 0, 0, DTA_CleanNoMove, true, DTA_TopLeft, true); + + Screen.DrawText(ConFont, Font.CR_UNTRANSLATED, (int(readysize.X) + 4) * CleanXFac, CleanYFac, String.Format("%d/%d", readyClients, totalClients), DTA_CleanNoMove, true); + int startTimer = GetReadyTimer(); + if (startTimer > 0) + { + int col = Font.CR_UNTRANSLATED; + if (startTimer <= GameTicRate * 5) + col = Font.CR_RED; + + Screen.DrawText(ConFont, col, 0, int(readysize.Y) * CleanYFac + CleanYFac, SystemTime.Format("%M:%S", int(ceil(double(startTimer) / GameTicRate))), DTA_CleanNoMove, true); + } + } + } + + if (net_cutscenereadytype != 2 || consoleplayer == Net_Arbitrator) + { + string contType; + switch (GetLastInputType()) + { + case INP_KEYBOARD_MOUSE: + contType = "$NET_CONTINUE_MKB"; + break; + case INP_CONTROLLER: + contType = "$NET_CONTINUE_CONTROLLER"; + break; + case INP_JOYSTICK: + contType = "$NET_CONTINUE_JOYSTICK"; + break; + } + + string contTxt = StringTable.Localize(contType); + int xOfs = (Screen.GetWidth() - ConFont.StringWidth(contTxt) * CleanXFac) / 2; + int yOfs = Screen.GetHeight() - ConFont.GetHeight() * CleanYFac - CleanYFac; + Screen.DrawText(ConFont, Font.CR_GREEN, xOfs, yOfs, contTxt, DTA_CleanNoMove, true); + } + } +} class IntermissionController native ui { diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen.zs b/wadsrc/static/zscript/ui/statscreen/statscreen.zs index 94343f80d..6603c2e06 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen.zs @@ -80,7 +80,7 @@ class StatusScreen : ScreenJob abstract version("2.5") InterBackground bg; int acceleratestage; // used to accelerate or skip a stage - bool playerready[MAXPLAYERS]; + bool playerready[MAXPLAYERS]; // This is no longer used since the server needs to track this int me; // wbs.pnum int bcnt; int CurState; // specifies current CurState diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs b/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs index 73b397c07..baaa633b4 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen_coop.zs @@ -27,7 +27,6 @@ class CoopStatusScreen : StatusScreen for (int i = 0; i < MAXPLAYERS; i++) { - playerready[i] = false; cnt_kills[i] = cnt_items[i] = cnt_secret[i] = cnt_frags[i] = 0; if (!playeringame[i]) @@ -216,7 +215,6 @@ class CoopStatusScreen : StatusScreen } else if (ng_state == 12) { - // All players are ready; proceed. if ((acceleratestage) || autoskip) { PlaySound("intermission/pastcoopstats"); @@ -239,9 +237,9 @@ class CoopStatusScreen : StatusScreen // //==================================================================== - override void drawStats () + protected void DrawScoreboard(int y) { - int i, x, y, ypadding, height, lineheight; + int i, x, ypadding, height, lineheight; int maxnamewidth, maxscorewidth, maxiconheight; int pwidth = IntermissionFont.GetCharWidth("%"); int icon_x, name_x, kills_x, bonus_x, secret_x; @@ -252,8 +250,6 @@ class CoopStatusScreen : StatusScreen String text_bonus, text_secret, text_kills; TextureID readyico = TexMan.CheckForTexture("READYICO", TexMan.Type_MiscPatch); - y = drawLF(); - [maxnamewidth, maxscorewidth, maxiconheight] = GetPlayerWidths(); // Use the readyico height if it's bigger. Vector2 readysize = TexMan.GetScaledSize(readyico); @@ -282,7 +278,6 @@ class CoopStatusScreen : StatusScreen bonus_x += x; secret_x += x; - drawTextScaled(displayFont, name_x, y, Stringtable.Localize("$SCORE_NAME"), FontScale, textcolor); drawTextScaled(displayFont, kills_x - displayFont.StringWidth(text_kills) * FontScale, y, text_kills, FontScale, textcolor); drawTextScaled(displayFont, bonus_x - bonus_len * FontScale, y, text_bonus, FontScale, textcolor); @@ -303,7 +298,7 @@ class CoopStatusScreen : StatusScreen screen.Dim(player.GetDisplayColor(), 0.8f, x, y - ypadding, (secret_x - x) + (8 * CleanXfac), lineheight); - //if (playerready[i] || player.Bot != NULL) // Bots are automatically assumed ready, to prevent confusion + if (ScreenJobRunner.IsPlayerReady(i)) // Bots are automatically assumed ready, to prevent confusion screen.DrawTexture(readyico, true, x - (readysize.Y * CleanXfac), y, DTA_CleanNoMove, true); Color thiscolor = GetRowColor(player, i == consoleplayer); @@ -369,4 +364,56 @@ class CoopStatusScreen : StatusScreen drawTimeScaled(displayFont, secret_x, y, cnt_total_time, FontScale, textcolor); } } + + override void drawStats () + { + DrawScoreboard(drawLF()); + } + + override void drawShowNextLoc() + { + bg.drawBackground(CurState, true, snl_pointeron); + + // This has to be expanded out because drawEL() doesn't return its y offset and it's a virtual + // meaning it's too late to change :( + bool ispatch = TexMan.OkForLocalization(enteringPatch, "$WI_ENTERING"); + int oldy = TITLEY * scaleFactorY; + + if (!ispatch) + { + let asc = entering.mFont.GetMaxAscender("$WI_ENTERING"); + if (asc > TITLEY - 2) + { + oldy = (asc+2) * scaleFactorY; + } + } + + int y = DrawPatchOrText(oldy, entering, enteringPatch, "$WI_ENTERING"); + + // If the displayed info is made of patches we need some additional offsetting here. + + if (ispatch) + { + int h1 = BigFont.GetHeight() - BigFont.GetDisplacement(); + let size = TexMan.GetScaledSize(enteringPatch); + int h2 = int(size.Y); + let disp = min(h1, h2) / 4; + // The offset getting applied here must at least be as tall as the largest ascender in the following text to avoid overlaps. + if (!wbs.LName1.isValid()) + { + disp += mapname.mFont.GetMaxAscender(lnametexts[1]); + } + y += disp * scaleFactorY; + } + + y = DrawName(y, wbs.LName1, lnametexts[1]); + + if (wbs.LName1.isValid() && authortexts[1].length() > 0) + { + // Consdider the ascender height of the following text. + y += author.mFont.GetMaxAscender(authortexts[1]) * scaleFactorY; + } + + DrawScoreboard(DrawAuthor(y, authortexts[1])); + } } diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs b/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs index e768b9db2..93f3ab8e9 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen_dm.zs @@ -26,7 +26,6 @@ class DeathmatchStatusScreen : StatusScreen for(i = 0; i < MAXPLAYERS; i++) { - playerready[i] = false; cnt_frags[i] = cnt_deaths[i] = player_deaths[i] = 0; } total_frags = 0; @@ -124,7 +123,6 @@ class DeathmatchStatusScreen : StatusScreen } else if (ng_state == 6) { - // All players are ready; proceed. if ((acceleratestage) || doautoskip) { PlaySound("intermission/pastdmstats"); @@ -141,9 +139,9 @@ class DeathmatchStatusScreen : StatusScreen } } - override void drawStats () + protected void DrawScoreboard(int y) { - int i, pnum, x, y, ypadding, height, lineheight; + int i, pnum, x, ypadding, height, lineheight; int maxnamewidth, maxscorewidth, maxiconheight; int pwidth = IntermissionFont.GetCharWidth("%"); int icon_x, name_x, frags_x, deaths_x; @@ -151,8 +149,6 @@ class DeathmatchStatusScreen : StatusScreen String text_deaths, text_frags; TextureID readyico = TexMan.CheckForTexture("READYICO", TexMan.Type_MiscPatch); - y = drawLF(); - [maxnamewidth, maxscorewidth, maxiconheight] = GetPlayerWidths(); // Use the readyico height if it's bigger. Vector2 readysize = TexMan.GetScaledSize(readyico); @@ -199,7 +195,7 @@ class DeathmatchStatusScreen : StatusScreen screen.Dim(player.GetDisplayColor(), 0.8, x, y - ypadding, (deaths_x - x) + (8 * CleanXfac), lineheight); - //if (playerready[pnum] || player.Bot != NULL) // Bots are automatically assumed ready, to prevent confusion + if (ScreenJobRunner.IsPlayerReady(pnum)) // Bots are automatically assumed ready, to prevent confusion screen.DrawTexture(readyico, true, x - (readysize.X * CleanXfac), y, DTA_CleanNoMove, true); let thiscolor = GetRowColor(player, pnum == consoleplayer); @@ -239,4 +235,56 @@ class DeathmatchStatusScreen : StatusScreen String leveltime = Stringtable.Localize("$SCORE_LVLTIME") .. ": " .. String.Format("%02i:%02i:%02i", hours, minutes, seconds); drawTextScaled(displayFont, x, y, leveltime, FontScale, textcolor); } + + override void drawStats () + { + DrawScoreboard(drawLF()); + } + + override void drawShowNextLoc() + { + bg.drawBackground(CurState, true, snl_pointeron); + + // This has to be expanded out because drawEL() doesn't return its y offset and it's a virtual + // meaning it's too late to change :( + bool ispatch = TexMan.OkForLocalization(enteringPatch, "$WI_ENTERING"); + int oldy = TITLEY * scaleFactorY; + + if (!ispatch) + { + let asc = entering.mFont.GetMaxAscender("$WI_ENTERING"); + if (asc > TITLEY - 2) + { + oldy = (asc+2) * scaleFactorY; + } + } + + int y = DrawPatchOrText(oldy, entering, enteringPatch, "$WI_ENTERING"); + + // If the displayed info is made of patches we need some additional offsetting here. + + if (ispatch) + { + int h1 = BigFont.GetHeight() - BigFont.GetDisplacement(); + let size = TexMan.GetScaledSize(enteringPatch); + int h2 = int(size.Y); + let disp = min(h1, h2) / 4; + // The offset getting applied here must at least be as tall as the largest ascender in the following text to avoid overlaps. + if (!wbs.LName1.isValid()) + { + disp += mapname.mFont.GetMaxAscender(lnametexts[1]); + } + y += disp * scaleFactorY; + } + + y = DrawName(y, wbs.LName1, lnametexts[1]); + + if (wbs.LName1.isValid() && authortexts[1].length() > 0) + { + // Consdider the ascender height of the following text. + y += author.mFont.GetMaxAscender(authortexts[1]) * scaleFactorY; + } + + DrawScoreboard(DrawAuthor(y, authortexts[1])); + } } From 222fbfcf7c01453f97c46cbca118f6e8ac6817d7 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Sat, 28 Jun 2025 12:46:38 -0700 Subject: [PATCH 232/384] Fix missing null-terminator when writing demos --- src/g_game.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/g_game.cpp b/src/g_game.cpp index 6e4b445dd..5dc05e8ce 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -2617,7 +2617,7 @@ void G_BeginRecording (const char *startmap) WriteInt8((uint8_t)i, &demo_p); auto str = D_GetUserInfoStrings(i); memcpy(demo_p, str.GetChars(), str.Len() + 1); - demo_p += str.Len(); + demo_p += str.Len() + 1; FinishChunk(&demo_p); } } From 61faf95f762c5ebabe1f4c87d93810fc1a360573 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 28 Jun 2025 20:27:45 -0400 Subject: [PATCH 233/384] Added -coop switch Sets some sensible defaults for coop mode. --- src/d_main.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/d_main.cpp b/src/d_main.cpp index 7ebd1c40c..83ec01172 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -2079,6 +2079,7 @@ static void AddAutoloadFiles(const char *autoname, std::vector& all static void CheckCmdLine() { int flags = dmflags; + int flags3 = dmflags3; int p; const char *v; @@ -2099,8 +2100,18 @@ static void CheckCmdLine() deathmatch = 1; flags |= DF_WEAPONS_STAY | DF_ITEMS_RESPAWN; } + else if (Args->CheckParm("-coop")) + { + deathmatch = teamplay = 0; + flags |= DF_NO_COOP_WEAPON_SPAWN; + flags3 |= DF3_NO_PLAYER_CLIP | DF3_COOP_SHARE_KEYS | DF3_REMEMBER_LAST_WEAP; + // Hexen already has a bunch of custom coop items so let it handle it. + if (gameinfo.gametype != GAME_Hexen) + flags3 |= DF3_LOCAL_ITEMS; + } dmflags = flags; + dmflags3 = flags3; // get skill / episode / map from parms if (gameinfo.gametype != GAME_Hexen) @@ -3906,6 +3917,7 @@ UNSAFE_CCMD(debug_restart) Args->RemoveArgs("-file"); Args->RemoveArgs("-altdeath"); Args->RemoveArgs("-deathmatch"); + Args->RemoveArgs("-coop"); Args->RemoveArgs("-skill"); Args->RemoveArgs("-savedir"); Args->RemoveArgs("-xlat"); From d182bd9411df27e28f24b2650abd4bc258d2536a Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 28 Jun 2025 19:28:10 -0400 Subject: [PATCH 234/384] Allow disabling pausing entirely when online --- src/d_net.cpp | 8 +++++++- src/g_game.cpp | 16 ++++++++++++---- wadsrc/static/menudef.txt | 9 ++++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 0095d0f31..d1c353e89 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -159,8 +159,14 @@ CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later. CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE) -CVAR(Bool, net_disablepause, false, CVAR_SERVERINFO | CVAR_NOSAVE) CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE) +CUSTOM_CVAR(Int, net_disablepause, 0, CVAR_SERVERINFO | CVAR_NOSAVE) +{ + if (self < 0) + self = 0; + else if (self > 2) + self = 2; +} CUSTOM_CVAR(Int, net_cutscenereadytype, RT_VOTE, CVAR_SERVERINFO | CVAR_NOSAVE) { if (self < RT_VOTE) diff --git a/src/g_game.cpp b/src/g_game.cpp index 5dc05e8ce..d11a9af57 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -127,7 +127,7 @@ CVAR (Bool, cl_waitforsave, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, enablescriptscreenshot, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, cl_restartondeath, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); EXTERN_CVAR (Float, con_midtime); -EXTERN_CVAR(Bool, net_disablepause) +EXTERN_CVAR(Int, net_disablepause) //========================================================================== // @@ -351,10 +351,18 @@ CCMD (land) CCMD (pause) { - if (netgame && !players[consoleplayer].settings_controller && net_disablepause) + if (netgame) { - Printf("Only settings controllers can currently (un)pause the game\n"); - return; + if (net_disablepause == 2 && (!paused || !players[consoleplayer].settings_controller)) + { + Printf("Pausing the game is currently disabled\n"); + return; + } + else if (net_disablepause == 1 && !players[consoleplayer].settings_controller) + { + Printf("Only settings controllers can currently (un)pause the game\n"); + return; + } } sendpause = true; diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 4e7413357..7564b1262 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2440,7 +2440,7 @@ OptionMenu NetworkOptions protected StaticText " " StaticText "$NETMNU_HOSTOPTIONS", 1 Option "$NETMNU_EXTRATICS", "net_extratic", "OnOff" - Option "$NETMNU_DISABLEPAUSE", "net_disablepause", "OnOff" + Option "$NETMNU_DISABLEPAUSE", "net_disablepause", "PauseTypes" Option "$NETMNU_READYTYPE", "net_cutscenereadytype", "ReadyTypes" Slider "$NETMNU_READYPERCENT", "net_cutscenereadypercent", "0", "1", "0.05", 2 NumberField "$NETMNU_READYCOUNTDOWN", "net_cutscenecountdown", 0, 300, 5 @@ -2448,6 +2448,13 @@ OptionMenu NetworkOptions protected } +OptionValue "PauseTypes" +{ + 0, "$OPTVAL_OFF" + 1, "$OPTVAL_HOSTONLY" + 2, "$OPTVAL_ON" +} + OptionValue "ReadyTypes" { 0, "$OPTVAL_VOTE" From 1a0df14170a2d420eda4919f7fc54c9d7e4042fd Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sun, 29 Jun 2025 02:27:35 -0400 Subject: [PATCH 235/384] Count turbo as a cheat --- src/g_game.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/g_game.cpp b/src/g_game.cpp index d11a9af57..808d8ba15 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -246,7 +246,7 @@ CVAR (Bool, teamplay, false, CVAR_SERVERINFO) #endif // _M_X64 && _MSC_VER < 1910 // [RH] Allow turbo setting anytime during game -CUSTOM_CVAR (Float, turbo, 100.f, CVAR_NOINITCALL) +CUSTOM_CVAR (Float, turbo, 100.f, CVAR_NOINITCALL | CVAR_CHEAT) { if (self < 10.f) { From 3fba33204cfd0ffe2601b36aa516149ef348bed9 Mon Sep 17 00:00:00 2001 From: MajorCooke Date: Mon, 30 Jun 2025 09:15:30 -0500 Subject: [PATCH 236/384] - Fixed
Morph() being called twice for
 monsters.

---
 wadsrc/static/zscript/actors/morph.zs | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/wadsrc/static/zscript/actors/morph.zs b/wadsrc/static/zscript/actors/morph.zs
index 5ebbb59f0..a7b8c9359 100644
--- a/wadsrc/static/zscript/actors/morph.zs
+++ b/wadsrc/static/zscript/actors/morph.zs
@@ -174,9 +174,7 @@ extend class Actor
 			return false;
 		}
 
-		// [MC] Notify that we're just about to start the transfer.
-		PreMorph(morphed, false);		// False: No longer the current.
-		morphed.PreMorph(self, true);	// True: Becoming this actor.
+		// [MC] PreMorph is now called via MorphInto instead of here now.
 
 		if ((style & MRF_TRANSFERTRANSLATION) && !morphed.bDontTranslate)
 			morphed.Translation = Translation;
@@ -292,8 +290,7 @@ extend class Actor
 		if (!MorphInto(alt))
 			return false;
 
-		PreUnmorph(alt, false);
-		alt.PreUnmorph(self, true);
+		//[MC] PreUnmorph is called by MorphInto now instead of here.
 
 		// Check to see if it had a powerup that caused it to morph.
 		for (Inventory item = alt.Inv; item;)

From 311e1d09becbac07a611bc39bbae0d6c1ecf6c15 Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Tue, 1 Jul 2025 20:35:03 -0400
Subject: [PATCH 237/384] Allow limiting NPC conversations to settings
 controllers

Also cleans up the network menu a little bit.
---
 src/d_net.cpp             |  1 +
 src/playsim/p_lnspec.cpp  | 21 +++------------------
 src/playsim/p_map.cpp     | 10 +++++++++-
 wadsrc/static/menudef.txt |  6 ++++--
 4 files changed, 17 insertions(+), 21 deletions(-)

diff --git a/src/d_net.cpp b/src/d_net.cpp
index d1c353e89..7d7f08587 100644
--- a/src/d_net.cpp
+++ b/src/d_net.cpp
@@ -160,6 +160,7 @@ CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later.
 CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE)
 CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE)
+CVAR(Bool, net_limitconversations, false, CVAR_SERVERINFO | CVAR_NOSAVE)
 CUSTOM_CVAR(Int, net_disablepause, 0, CVAR_SERVERINFO | CVAR_NOSAVE)
 {
 	if (self < 0)
diff --git a/src/playsim/p_lnspec.cpp b/src/playsim/p_lnspec.cpp
index 00318dfbb..0a14c9a2a 100644
--- a/src/playsim/p_lnspec.cpp
+++ b/src/playsim/p_lnspec.cpp
@@ -134,6 +134,8 @@ FName MODtoDamageType (int mod)
 	}
 }
 
+int NativeStartConversation(AActor* self, AActor* player, bool faceTalker, bool saveAngle);
+
 FUNC(LS_NOP)
 {
 	return false;
@@ -3401,24 +3403,7 @@ FUNC(LS_StartConversation)
 		return false;
 	}
 
-	// Dead things can't talk.
-	if (target->health <= 0)
-	{
-		return false;
-	}
-	// Fighting things don't talk either.
-	if (target->flags4 & MF4_INCOMBAT)
-	{
-		return false;
-	}
-	if (target->Conversation != NULL)
-	{
-		// Give the NPC a chance to play a brief animation
-		target->ConversationAnimation (0);
-		P_StartConversation (target, it, !!arg1, true);
-		return true;
-	}
-	return false;
+	return NativeStartConversation(target, it, !!arg1, true);
 }
 
 FUNC(LS_Thing_SetConversation)
diff --git a/src/playsim/p_map.cpp b/src/playsim/p_map.cpp
index 9cc66b98d..e2bcbb24c 100644
--- a/src/playsim/p_map.cpp
+++ b/src/playsim/p_map.cpp
@@ -114,6 +114,8 @@ static FRandom pr_crunch("DoCrunch");
 TArray spechit;
 TArray portalhit;
 
+EXTERN_CVAR(Bool, net_limitconversations)
+
 //==========================================================================
 //
 // P_ShouldPassThroughPlayer
@@ -5733,11 +5735,17 @@ DEFINE_ACTION_FUNCTION_NATIVE(AActor, HasConversation, HasConversation)
 	ACTION_RETURN_BOOL(HasConversation(self));
 }
 
-static int NativeStartConversation(AActor *self, AActor *player, bool faceTalker, bool saveAngle)
+int NativeStartConversation(AActor *self, AActor *player, bool faceTalker, bool saveAngle)
 {
 	if (!CanTalk(self))
 		return false;
 
+	if (netgame && net_limitconversations && player->player != nullptr && player->player->mo == player && !player->player->settings_controller)
+	{
+		Printf("Only settings controllers can start conversations with NPCs\n");
+		return false;
+	}
+
 	self->ConversationAnimation(0);
 	P_StartConversation(self, player, faceTalker, saveAngle);
 	return true;
diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt
index 7564b1262..634c1da30 100644
--- a/wadsrc/static/menudef.txt
+++ b/wadsrc/static/menudef.txt
@@ -2440,11 +2440,13 @@ OptionMenu NetworkOptions protected
 	StaticText " "
 	StaticText "$NETMNU_HOSTOPTIONS", 1
 	Option "$NETMNU_EXTRATICS",				"net_extratic", "OnOff"
+	Option "$NETMNU_LIMITCONVO",			"net_limitconversations", "OnOff"
+	Option "$NETMNU_REPEATCOOLDOWN",		"net_repeatableactioncooldown", "OnOff"
 	Option "$NETMNU_DISABLEPAUSE",			"net_disablepause", "PauseTypes"
 	Option "$NETMNU_READYTYPE",				"net_cutscenereadytype", "ReadyTypes"
 	Slider "$NETMNU_READYPERCENT",			"net_cutscenereadypercent", "0", "1", "0.05", 2
-	NumberField "$NETMNU_READYCOUNTDOWN",	"net_cutscenecountdown", 0, 300, 5
-	NumberField "$NETMNU_CHATSLOMO",		"net_chatslowmode", 0, 300, 5
+	Slider "$NETMNU_READYCOUNTDOWN",		"net_cutscenecountdown", 0, 300, 5
+	Slider "$NETMNU_CHATSLOMO",				"net_chatslowmode", 0, 300, 5
 
 }
 

From 5ea8981472c44c2010df0c854b4ad806296fd54d Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Tue, 1 Jul 2025 20:47:05 -0400
Subject: [PATCH 238/384] Only print the message for the player trying to
 initiate a conversation

---
 src/playsim/p_map.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/playsim/p_map.cpp b/src/playsim/p_map.cpp
index e2bcbb24c..780e75e24 100644
--- a/src/playsim/p_map.cpp
+++ b/src/playsim/p_map.cpp
@@ -5742,7 +5742,8 @@ int NativeStartConversation(AActor *self, AActor *player, bool faceTalker, bool
 
 	if (netgame && net_limitconversations && player->player != nullptr && player->player->mo == player && !player->player->settings_controller)
 	{
-		Printf("Only settings controllers can start conversations with NPCs\n");
+		if (player == players[consoleplayer].mo)
+			Printf("Only settings controllers can start conversations with NPCs\n");
 		return false;
 	}
 

From cf9a9097b9b6a647990f1b1654540b9d33ee3435 Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Wed, 2 Jul 2025 10:52:51 -0400
Subject: [PATCH 239/384] Fix up saving in multiplayer

By default allow only settings controllers to save the game. Use actual file names to help prevent possible save file overriding as savexx is unreliable online. Prevent quicksave behavior from working with the rotator. Force a unique netgame subfolder for multiplayer saves to remove the ability to override singleplayer saves. Send over the host's -loadgame argument to make loading easier (will not override the guest's -loadgame in case they need a special file name).
---
 src/common/menu/savegamemanager.cpp           | 55 +++++++++++++++++--
 .../platform/posix/unix/i_specialpaths.cpp    |  7 ++-
 src/common/platform/win32/i_specialpaths.cpp  |  4 ++
 src/console/c_cmds.cpp                        |  2 +-
 src/d_net.cpp                                 | 36 +++++++++++-
 src/g_game.cpp                                | 13 +++++
 src/menu/doommenu.cpp                         |  2 +-
 7 files changed, 110 insertions(+), 9 deletions(-)

diff --git a/src/common/menu/savegamemanager.cpp b/src/common/menu/savegamemanager.cpp
index efb5a02af..f436ba8a3 100644
--- a/src/common/menu/savegamemanager.cpp
+++ b/src/common/menu/savegamemanager.cpp
@@ -53,6 +53,8 @@ CVAR(String, save_dir, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_SYSTEM_ONLY);
 FString SavegameFolder;
 CVAR(Int, save_sort_order, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
 
+extern bool netgame;
+
 //=============================================================================
 //
 // Save data maintenance 
@@ -264,12 +266,47 @@ void FSavegameManagerBase::DoSave(int Selected, const char *savegamestring)
 		FString filename;
 		int i;
 
-		for (i = 0;; ++i)
+		if (netgame)
 		{
-			filename = BuildSaveName("save", i);
-			if (!FileExists(filename))
+			// For netgames it's usually a bad idea to use the default savexx names, so instead
+			// sanitize the description to use as a name.
+			filename = savegamestring;
+			FixPathSeperator(filename);
+			bool failed = false;
+			if (filename[0] == '/')
 			{
-				break;
+				Printf("saving to an absolute path is not allowed\n");
+				failed = true;
+			}
+			else if (filename.IndexOf("..") >= 0)
+			{
+				Printf("'..' not allowed in file names\n");
+				failed = true;
+			}
+#ifdef _WIN32
+			// block all invalid characters for Windows file names
+			else if (filename.IndexOfAny(":?*<>|") >= 0)
+			{
+				Printf("file name contains invalid characters\n");
+				failed = true;
+			}
+#endif
+			if (failed)
+			{
+				M_ClearMenus();
+				return;
+			}
+			filename = G_BuildSaveName(filename.GetChars());
+		}
+		else
+		{
+			for (i = 0;; ++i)
+			{
+				filename = BuildSaveName("save", i);
+				if (!FileExists(filename))
+				{
+					break;
+				}
 			}
 		}
 		PerformSaveGame(filename.GetChars(), savegamestring);
@@ -561,7 +598,15 @@ FString G_GetSavegamesFolder()
 	FString name;
 	bool usefilter;
 
-	if (const char* const dir = Args->CheckValue("-savedir"))
+	// Always use the netgame folder for multiplayer games to prevent any singleplayer saves
+	// from being overridden by someone else. Also makes it easier for everyone to load from
+	// it.
+	if (netgame)
+	{
+		name = M_GetSavegamesPath();
+		usefilter = true;
+	}
+	else if (const char* const dir = Args->CheckValue("-savedir"))
 	{
 		name = dir;
 		usefilter = false; //-savedir specifies an absolute save directory path.
diff --git a/src/common/platform/posix/unix/i_specialpaths.cpp b/src/common/platform/posix/unix/i_specialpaths.cpp
index 112e92ee7..c06e27b67 100644
--- a/src/common/platform/posix/unix/i_specialpaths.cpp
+++ b/src/common/platform/posix/unix/i_specialpaths.cpp
@@ -42,6 +42,8 @@
 
 #include "version.h"	// for GAMENAME
 
+extern bool netgame;
+
 
 FString GetUserFile (const char *file)
 {
@@ -191,7 +193,10 @@ FString M_GetScreenshotsPath()
 
 FString M_GetSavegamesPath()
 {
-	return NicePath("$HOME/" GAME_DIR "/savegames/");
+	FString pName = "$HOME/" GAME_DIR "/savegames/";
+	if (netgame)
+		pName << "netgame/";
+	return NicePath(pName.GetChars());
 }
 
 //===========================================================================
diff --git a/src/common/platform/win32/i_specialpaths.cpp b/src/common/platform/win32/i_specialpaths.cpp
index 8862024b7..c7f53c24a 100644
--- a/src/common/platform/win32/i_specialpaths.cpp
+++ b/src/common/platform/win32/i_specialpaths.cpp
@@ -51,6 +51,8 @@
 
 static int isportable = -1;
 
+extern bool netgame;
+
 //===========================================================================
 //
 // IsProgramDirectoryWritable
@@ -377,6 +379,8 @@ FString M_GetSavegamesPath()
 		path = GetKnownFolder(-1, FOLDERID_SavedGames, true);
 		path << "/" GAMENAME "/";
 	}
+	if (netgame)
+		path << "NetGame/";
 	return path;
 }
 
diff --git a/src/console/c_cmds.cpp b/src/console/c_cmds.cpp
index 9b5e8f612..1d45a1130 100644
--- a/src/console/c_cmds.cpp
+++ b/src/console/c_cmds.cpp
@@ -713,7 +713,7 @@ UNSAFE_CCMD(save)
 		Printf("saving to an absolute path is not allowed\n");
 		return;
 	}
-	if (fname.IndexOf("..") > 0)
+	if (fname.IndexOf("..") >= 0)
 	{
 		Printf("'..' not allowed in file names\n");
 		return;
diff --git a/src/d_net.cpp b/src/d_net.cpp
index 7d7f08587..90dbd83b2 100644
--- a/src/d_net.cpp
+++ b/src/d_net.cpp
@@ -159,6 +159,7 @@ CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 
 CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later.
 CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE)
+CVAR(Bool, net_limitsaves, true, CVAR_SERVERINFO | CVAR_NOSAVE)
 CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE)
 CVAR(Bool, net_limitconversations, false, CVAR_SERVERINFO | CVAR_NOSAVE)
 CUSTOM_CVAR(Int, net_disablepause, 0, CVAR_SERVERINFO | CVAR_NOSAVE)
@@ -1818,7 +1819,21 @@ size_t Net_SetGameInfo(uint8_t*& stream)
 	WriteString(startmap.GetChars(), &stream);
 	WriteInt32(rngseed, &stream);
 	C_WriteCVars(&stream, CVAR_SERVERINFO, true);
-	return stream - start;
+
+	auto load = Args->CheckValue("-loadgame");
+	if (load != nullptr)
+	{
+		stream[0] = true;
+		const size_t len = strlen(load) + 1;
+		memcpy(&stream[1], load, len);
+		stream += len;
+	}
+	else
+	{
+		stream[0] = false;
+	}
+
+	return (stream + 1) - start;
 }
 
 size_t Net_ReadGameInfo(uint8_t*& stream)
@@ -1827,6 +1842,25 @@ size_t Net_ReadGameInfo(uint8_t*& stream)
 	startmap = ReadStringConst(&stream);
 	rngseed = ReadInt32(&stream);
 	C_ReadCVars(&stream);
+
+	if (stream[0])
+	{
+		++stream;
+		const size_t len = strlen((char*)stream) + 1;
+		// Don't override the existing argument in case they need to use
+		// a custom savefile name.
+		if (!Args->CheckParm("-loadgame"))
+		{
+			Args->AppendArg("-loadgame");
+			Args->AppendArg((char*)stream);
+		}
+		stream += len;
+	}
+	else
+	{
+		++stream;
+	}
+
 	return stream - start;
 }
 
diff --git a/src/g_game.cpp b/src/g_game.cpp
index 808d8ba15..558a592e1 100644
--- a/src/g_game.cpp
+++ b/src/g_game.cpp
@@ -128,6 +128,7 @@ CVAR (Bool, enablescriptscreenshot, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
 CVAR (Bool, cl_restartondeath, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
 EXTERN_CVAR (Float, con_midtime);
 EXTERN_CVAR(Int, net_disablepause)
+EXTERN_CVAR(Bool, net_limitsaves)
 
 //==========================================================================
 //
@@ -2161,6 +2162,10 @@ void G_SaveGame (const char *filename, const char *description)
     {
 		Printf ("%s\n", GStrings.GetString("TXT_SPPLAYERDEAD"));
     }
+	else if (netgame && net_limitsaves && !players[consoleplayer].settings_controller)
+	{
+		Printf("Only settings controllers can save the game\n");
+	}
 	else
 	{
 		savegamefile = filename;
@@ -2196,6 +2201,10 @@ CUSTOM_CVAR (Int, quicksaverotationcount, 4, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 
 void G_DoAutoSave ()
 {
+	// Never autosave in netgames since you can't load this properly anyway.
+	if (netgame)
+		return;
+
 	FString description;
 	FString file;
 	// Keep up to four autosaves at a time
@@ -2231,6 +2240,10 @@ void G_DoAutoSave ()
 
 void G_DoQuickSave ()
 {
+	// Never quicksave in netgames since you can't load this properly anyway.
+	if (netgame)
+		return;
+
 	FString description;
 	FString file;
 	// Keeps a rotating set of quicksaves
diff --git a/src/menu/doommenu.cpp b/src/menu/doommenu.cpp
index 5718bd6b0..e21f977db 100644
--- a/src/menu/doommenu.cpp
+++ b/src/menu/doommenu.cpp
@@ -477,7 +477,7 @@ CCMD (quicksave)
 		return;
 
 	// If the quick save rotation is enabled, it handles the save slot.
-	if (quicksaverotation)
+	if (!netgame && quicksaverotation)
 	{
 		G_DoQuickSave();
 		return;

From ba953b2d6dd33e9283fd9da0c682dba3ce96913a Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Wed, 2 Jul 2025 00:58:06 -0400
Subject: [PATCH 240/384] Improve default starting map

Instead of defaulting to MAP01/E1M1, select the map in the first episode after parsing. This significantly improves the autostart and netgame behavior by warping to the first defined and valid level. Also adds -episode to allow more easily specifying which episode to play for mapsets that don't have simple ExMy naming conventions.
---
 src/d_main.cpp | 38 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 36 insertions(+), 2 deletions(-)

diff --git a/src/d_main.cpp b/src/d_main.cpp
index 83ec01172..fe3812ddd 100644
--- a/src/d_main.cpp
+++ b/src/d_main.cpp
@@ -317,6 +317,7 @@ const char *D_DrawIcon;	// [RH] Patch name of icon to draw on next refresh
 int NoWipe;				// [RH] Allow wipe? (Needs to be set each time)
 bool singletics = false;	// debug flag to cancel adaptiveness
 FString startmap;
+bool setmap;
 bool autostart;
 bool advancedemo;
 FILE *debugfile;
@@ -2114,6 +2115,7 @@ static void CheckCmdLine()
 	dmflags3 = flags3;
 
 	// get skill / episode / map from parms
+	setmap = false;
 	if (gameinfo.gametype != GAME_Hexen)
 	{
 		startmap = (gameinfo.flags & GI_MAPxx) ? "MAP01" : "E1M1";
@@ -2153,7 +2155,7 @@ static void CheckCmdLine()
 		}
 
 		startmap = CalcMapName (ep, map);
-		autostart = true;
+		autostart = setmap = true;
 	}
 
 	// [RH] Hack to handle +map. The standard console command line handler
@@ -2169,7 +2171,7 @@ static void CheckCmdLine()
 		else
 		{
 			startmap = mapvalue;
-			autostart = true;
+			autostart = setmap = true;
 		}
 	}
 
@@ -2219,6 +2221,37 @@ static void CheckCmdLine()
 	}
 }
 
+// Attempt to account for wads with episodes much better when playing online. Defaulting to MAP01 is sometimes
+// a really bad idea e.g. if a hub map is the actual start area.
+static void CheckEpisodeCmd()
+{
+	bool setEpisode = false;
+	int episode = 0;
+	auto v = Args->CheckValue("-episode");
+	if (v != nullptr)
+	{
+		episode = atoi(v) - 1;
+		if (episode < 0 || episode >= AllEpisodes.Size())
+		{
+			Printf("Invalid episode %s\n", v);
+			episode = 0;
+		}
+		else
+		{
+			setEpisode = true;
+		}
+	}
+
+	// If -warp or +map were already used, keep whatever existing value they had.
+	if (!setEpisode && setmap)
+		return;
+
+	startmap = AllEpisodes[episode].mEpisodeMap;
+	setmap = true;
+	if (setEpisode)
+		autostart = true;
+}
+
 static void NewFailure ()
 {
     I_FatalError ("Failed to allocate memory from system heap");
@@ -3288,6 +3321,7 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw
 	// [RH] Parse through all loaded mapinfo lumps
 	if (!batchrun) Printf ("G_ParseMapInfo: Load map definitions.\n");
 	G_ParseMapInfo (iwad_info->MapInfo);
+	CheckEpisodeCmd();
 	MessageBoxClass = gameinfo.MessageBoxClass;
 	endoomName = gameinfo.Endoom;
 	menuBlurAmount = gameinfo.bluramount;

From 54f1f5ad9daf950258d7aec9cd476f38334f8823 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= 
Date: Mon, 30 Jun 2025 23:20:07 -0300
Subject: [PATCH 241/384] stop stupid clamp asserts

---
 src/common/textures/image.h  | 7 ++-----
 src/common/utility/basics.h  | 8 +++++++-
 src/common/utility/vectors.h | 4 +++-
 3 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/common/textures/image.h b/src/common/textures/image.h
index 72b5ca9df..5ff158fb7 100644
--- a/src/common/textures/image.h
+++ b/src/common/textures/image.h
@@ -5,6 +5,8 @@
 #include "bitmap.h"
 #include "memarena.h"
 
+#include "common/utility/basics.h"
+
 #ifndef MAKE_ID
 #ifndef __BIG_ENDIAN__
 #define MAKE_ID(a,b,c,d)	((uint32_t)((a)|((b)<<8)|((c)<<16)|((d)<<24)))
@@ -13,11 +15,6 @@
 #endif
 #endif
 
-using std::min;
-using std::max;
-using std::clamp;
-
-
 class FImageSource;
 using PrecacheInfo = TMap>;
 extern FMemArena ImageArena;
diff --git a/src/common/utility/basics.h b/src/common/utility/basics.h
index 0dbb47c67..4d9921ded 100644
--- a/src/common/utility/basics.h
+++ b/src/common/utility/basics.h
@@ -68,4 +68,10 @@ const double M_PI = 3.14159265358979323846;	// matches value in gcc v2 math.h
 
 using std::min;
 using std::max;
-using std::clamp;
+//using std::clamp;
+
+template
+T clamp(T val, T minval, T maxval)
+{
+    return std::max(std::min(val, maxval), minval);
+}
\ No newline at end of file
diff --git a/src/common/utility/vectors.h b/src/common/utility/vectors.h
index 154d42f7c..62cf9b9c9 100644
--- a/src/common/utility/vectors.h
+++ b/src/common/utility/vectors.h
@@ -46,6 +46,8 @@
 #include 
 #include 
 
+#include "common/utility/basics.h"
+
 // this is needed to properly normalize angles. We cannot do that with compiler provided conversions because they differ too much
 #include "xs_Float.h"
 
@@ -1585,7 +1587,7 @@ constexpr inline TVector2 clamp(const TVector2 &vec, const TVector2 &mi
 template
 constexpr inline TVector3 clamp(const TVector3 &vec, const TVector3 &min, const TVector3 &max)
 {
-	return TVector3(std::clamp(vec.X, min.X, max.X), std::clamp(vec.Y, min.Y, max.Y), std::clamp(vec.Z, min.Z, max.Z));
+	return TVector3(clamp(vec.X, min.X, max.X), clamp(vec.Y, min.Y, max.Y), clamp(vec.Z, min.Z, max.Z));
 }
 
 template

From 0e23b900bb32c842fcf95b6d5d3dcf81e795403a Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Mon, 30 Jun 2025 00:05:53 -0400
Subject: [PATCH 242/384] Added dynamic tic stabilization

Attempts to balance periods of rough traffic by putting in an artificial delay, smoothing playback but further increasing input delay. This can be disabled with net_ticbalance.
---
 src/common/engine/i_net.h |  1 +
 src/d_net.cpp             | 66 +++++++++++++++++++++++++++++++++++++--
 wadsrc/static/menudef.txt |  1 +
 3 files changed, 65 insertions(+), 3 deletions(-)

diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h
index 6e92e945f..510388fad 100644
--- a/src/common/engine/i_net.h
+++ b/src/common/engine/i_net.h
@@ -11,6 +11,7 @@ enum ENetConstants
 	BACKUPTICS = 35 * 5,	// Remember up to 5 seconds of data.
 	MAXTICDUP = 3,
 	MAXSENDTICS = 35 * 1,	// Only send up to 1 second of data at a time.
+	STABILITYTICS = 12,
 	LOCALCMDTICS = (BACKUPTICS * MAXTICDUP),
 	MAX_MSGLEN = 14000,
 };
diff --git a/src/d_net.cpp b/src/d_net.cpp
index 90dbd83b2..92c2e36e8 100644
--- a/src/d_net.cpp
+++ b/src/d_net.cpp
@@ -116,6 +116,14 @@ int					LastSentConsistency = 0;		// Last consistency we sent out. If < CurrentC
 int					CurrentConsistency = 0;			// Last consistency we generated.
 FClientNetState		ClientStates[MAXPLAYERS] = {};
 
+// Try and stabilize uneven connections by checking for spikes in available
+// sequences. If they're found, try and average out a buffer to prioritize
+// making the experience smoother over very stop and go heavy.
+static int			StabilityBuffer = 0;
+static int			PrevAvailableDiff = 0;
+static size_t		CurStabilityTic = 0u;
+static int			StabilityTics[STABILITYTICS] = {};
+
 // If we're sending a packet to ourselves, store it here instead. This is the simplest way to execute
 // playback as it means in the world running code itself all player commands are built the exact same way
 // instead of having to rely on pulling from the correct local buffers. It also ensures all commands are
@@ -157,7 +165,7 @@ extern	bool	 advancedemo;
 CVAR(Bool, vid_dontdowait, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 CVAR(Bool, vid_lowerinbackground, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 
-CVAR(Bool, net_ticbalance, false, CVAR_SERVERINFO | CVAR_NOSAVE) // Currently deprecated, but may be brought back later.
+CVAR(Bool, net_ticbalance, true, CVAR_SERVERINFO | CVAR_NOSAVE)
 CVAR(Bool, net_extratic, false, CVAR_SERVERINFO | CVAR_NOSAVE)
 CVAR(Bool, net_limitsaves, true, CVAR_SERVERINFO | CVAR_NOSAVE)
 CVAR(Bool, net_repeatableactioncooldown, true, CVAR_SERVERINFO | CVAR_NOSAVE)
@@ -395,6 +403,9 @@ void Net_ClearBuffers()
 	LastEnterTic = LastGameUpdate = EnterTic;
 	gametic = ClientTic = 0;
 	SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0;
+	StabilityBuffer = PrevAvailableDiff = 0;
+	CurStabilityTic = 0u;
+	memset(StabilityTics, 0, sizeof(StabilityTics));
 	NetEvents.ResetStream();
 	
 	CutsceneReady = 0u;
@@ -502,6 +513,9 @@ void Net_ResetCommands(bool midTic)
 	bCommandsReset = midTic;
 	++CurrentLobbyID;
 	SkipCommandTimer = SkipCommandAmount = CommandsAhead = 0;
+	StabilityBuffer = PrevAvailableDiff = 0;
+	CurStabilityTic = 0u;
+	memset(StabilityTics, 0, sizeof(StabilityTics));
 
 	int tic = gametic / TicDup;
 	if (midTic)
@@ -1292,6 +1306,7 @@ static bool Net_UpdateStatus()
 
 	if (updated)
 	{
+		lowestDiff -= StabilityBuffer;
 		if (lowestDiff > 0)
 		{
 			if (SkipCommandTimer++ > TICRATE / 2)
@@ -2090,6 +2105,47 @@ static bool ShouldStabilizeTick()
 			&& gameaction != ga_worlddone && gameaction != ga_completed && gameaction != ga_screenshot && gameaction != ga_fullconsole;
 }
 
+// If the connection has been unstable then let the game lag behind for a little bit
+// while we wait for it to stabilize, otherwise everything will appear to jitter around.
+static void CalculateNetStabilityBuffer(int diff)
+{
+	if (!netgame || demoplayback)
+	{
+		StabilityBuffer = 0;
+		return;
+	}
+
+	if (diff < 0)
+		diff = 0;
+
+	if (!(gametic % TicDup))
+	{
+		StabilityTics[CurStabilityTic++ % STABILITYTICS] = diff > PrevAvailableDiff ? diff : 0;
+		PrevAvailableDiff = diff;
+	}
+
+	// If we're not balancing latency, just give an extra tic for padding
+	// and nothing else.
+	if (!net_ticbalance)
+	{
+		StabilityBuffer = 1;
+		return;
+	}
+
+	double total = 0.0;
+	int unstableCount = 0;
+	for (int t : StabilityTics)
+	{
+		if (t > 0)
+		{
+			++unstableCount;
+			total += t;
+		}
+	}
+
+	StabilityBuffer = unstableCount > 0 ? static_cast(ceil(total / unstableCount)) : 0;
+}
+
 //
 // TryRunTics
 //
@@ -2148,8 +2204,12 @@ void TryRunTics()
 	// If the amount of tics to run is falling behind the amount of available tics,
 	// speed the playsim up a bit to help catch up.
 	int runTics = min(totalTics, availableTics);
-	if (totalTics > 0 && totalTics < availableTics && !singletics)
-		++runTics;
+	if (!singletics && totalTics > 0)
+	{
+		CalculateNetStabilityBuffer(availableTics - totalTics);
+		if (totalTics < availableTics - StabilityBuffer)
+			++runTics;
+	}
 
 	// Test player prediction code in singleplayer
 	// by running the gametic behind the ClientTic
diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt
index 634c1da30..046a7f1f6 100644
--- a/wadsrc/static/menudef.txt
+++ b/wadsrc/static/menudef.txt
@@ -2440,6 +2440,7 @@ OptionMenu NetworkOptions protected
 	StaticText " "
 	StaticText "$NETMNU_HOSTOPTIONS", 1
 	Option "$NETMNU_EXTRATICS",				"net_extratic", "OnOff"
+	Option "$NETMNU_BALANCETICS",			"net_ticbalance", "OnOff"
 	Option "$NETMNU_LIMITCONVO",			"net_limitconversations", "OnOff"
 	Option "$NETMNU_REPEATCOOLDOWN",		"net_repeatableactioncooldown", "OnOff"
 	Option "$NETMNU_DISABLEPAUSE",			"net_disablepause", "PauseTypes"

From 492c83cf2c32e957cd3f542693eaf63ed0bc9ff0 Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Mon, 30 Jun 2025 14:46:17 -0400
Subject: [PATCH 243/384] Improve stability in packet-server mode

Use buffers reported by clients so they can better check their own conditions when determining how to stabilize when outpacing the host.
---
 src/common/engine/i_net.h |  2 +-
 src/d_net.cpp             | 27 +++++++++++++++++++--------
 src/d_net.h               |  1 +
 3 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/src/common/engine/i_net.h b/src/common/engine/i_net.h
index 510388fad..1d09517e2 100644
--- a/src/common/engine/i_net.h
+++ b/src/common/engine/i_net.h
@@ -11,7 +11,7 @@ enum ENetConstants
 	BACKUPTICS = 35 * 5,	// Remember up to 5 seconds of data.
 	MAXTICDUP = 3,
 	MAXSENDTICS = 35 * 1,	// Only send up to 1 second of data at a time.
-	STABILITYTICS = 12,
+	STABILITYTICS = 17,
 	LOCALCMDTICS = (BACKUPTICS * MAXTICDUP),
 	MAX_MSGLEN = 14000,
 };
diff --git a/src/d_net.cpp b/src/d_net.cpp
index 92c2e36e8..1ddfde27c 100644
--- a/src/d_net.cpp
+++ b/src/d_net.cpp
@@ -378,7 +378,7 @@ void Net_ClearBuffers()
 		memset(state.RecvTime, 0, sizeof(state.RecvTime));
 		state.bNewLatency = true;
 
-		state.ResendID = 0u;
+		state.ResendID = state.StabilityBuffer = 0u;
 		state.CurrentNetConsistency = state.LastVerifiedConsistency = state.ConsistencyAck = state.ResendConsistencyFrom = -1;
 		state.CurrentSequence = state.SequenceAck = state.ResendSequenceFrom = -1;
 		state.Flags = 0;
@@ -535,6 +535,7 @@ void Net_ResetCommands(bool midTic)
 	{
 		auto& state = ClientStates[client];
 		state.Flags &= CF_QUIT;
+		state.StabilityBuffer = 0u;
 		state.CurrentSequence = min(state.CurrentSequence, tic);
 		state.SequenceAck = min(state.SequenceAck, tic);
 		if (state.ResendSequenceFrom >= tic)
@@ -589,7 +590,8 @@ static size_t GetNetBufferSize()
 	const int ranTics = NetBuffer[totalBytes++];
 	if (ranTics > 0)
 		totalBytes += 4;
-	if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator)
+	// Stability buffer/commands ahead
+	if (NetMode == NET_PacketServer)
 		++totalBytes;
 
 	// Minimum additional packet size per player:
@@ -931,12 +933,16 @@ static void GetPackets()
 		if (ranTics > 0)
 			baseConsistency = (NetBuffer[curByte++] << 24) | (NetBuffer[curByte++] << 16) | (NetBuffer[curByte++] << 8) | NetBuffer[curByte++];
 
-		if (NetMode == NET_PacketServer && clientNum == Net_Arbitrator)
+		if (NetMode == NET_PacketServer)
 		{
 			if (validID)
-				CommandsAhead = NetBuffer[curByte++];
-			else
-				++curByte;
+			{
+				if (clientNum == Net_Arbitrator)
+					CommandsAhead = NetBuffer[curByte];
+				else if (consoleplayer == Net_Arbitrator)
+					clientState.StabilityBuffer = NetBuffer[curByte];
+			}
+			++curByte;
 		}
 		
 		for (int p = 0; p < playerCount; ++p)
@@ -1696,8 +1702,13 @@ void NetUpdate(int tics)
 					NetBuffer[size++] = baseConsistency + curTicOfs;
 				}
 
-				if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator)
-					NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence - newestTic, 0);
+				if (NetMode == NET_PacketServer)
+				{
+					if (consoleplayer == Net_Arbitrator)
+						NetBuffer[size++] = client == Net_Arbitrator ? 0 : max(curState.CurrentSequence + curState.StabilityBuffer - newestTic, 0);
+					else
+						NetBuffer[size++] = max(StabilityBuffer, 0);
+				}
 
 				// Client commands.
 
diff --git a/src/d_net.h b/src/d_net.h
index b62f4be05..8938f0c42 100644
--- a/src/d_net.h
+++ b/src/d_net.h
@@ -109,6 +109,7 @@ struct FClientNetState
 
 	int				Flags = 0;				// State of this client.
 
+	uint8_t			StabilityBuffer = 0u;	// If in packet-server mode, account for if the client is trying to stabilize when measuring their performance.
 	uint8_t			ResendID = 0u;			// Make sure that if the retransmit happened on a wait barrier, it can be properly resent back over.
 	int				ResendSequenceFrom = -1; // If >= 0, send from this sequence up to the most recent one, capped to MAXSENDTICS.
 	int				SequenceAck = -1;		// The last sequence the client reported from us.

From 2de0480147e887d536d96b027413186fce02b32b Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Mon, 30 Jun 2025 20:13:19 -0400
Subject: [PATCH 244/384] Clamp max number of commands generated

Only allow half the host's buffer to be filled at any time.
---
 src/d_net.cpp | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/d_net.cpp b/src/d_net.cpp
index 1ddfde27c..fff61c3c0 100644
--- a/src/d_net.cpp
+++ b/src/d_net.cpp
@@ -1228,7 +1228,7 @@ static bool Net_UpdateStatus()
 	// Wait for the game to stabilize a bit after launch before skipping commands.
 	bool updated = false;
 	int lowestDiff = INT_MAX;
-	if (gametic > TICRATE * 2)
+	if (gametic > TICRATE * 2 && !(gametic % TicDup))
 	{
 		if (NetMode != NET_PacketServer)
 		{
@@ -1402,9 +1402,19 @@ void NetUpdate(int tics)
 		LevelStartDelay = max(LevelStartDelay - tics, 0);
 	}
 		
-	const bool netGood = Net_UpdateStatus();
+	bool netGood = Net_UpdateStatus();
 	const int startTic = ClientTic;
 	tics = min(tics, MAXSENDTICS * TicDup);
+	if ((startTic + tics - gametic) / TicDup > BACKUPTICS / 2)
+	{
+		tics = (gametic + BACKUPTICS / 2 * TicDup) - startTic;
+		if (tics <= 0)
+		{
+			tics = 1;
+			netGood = false;
+		}
+	}
+
 	for (int i = 0; i < tics; ++i)
 	{
 		I_StartTic();
@@ -2002,9 +2012,12 @@ ADD_STAT(network)
 
 	const int delay = max((ClientTic - gametic) / TicDup, 0);
 	const int msDelay = min(delay * TicDup * 1000.0 / TICRATE, 999);
-	out.AppendFormat("\nLocal\n\tIs arbitrator: %d\tDelay: %02d (%03dms)",
+	const int buffer = max(StabilityBuffer, 0);
+	const int msBuffer = min(buffer * 1000.0 / TICRATE, 999);
+	out.AppendFormat("\nLocal\n\tIs arbitrator: %d\tDelay: %02d (%03dms)\tStability Buffer: %02d (%03dms)",
 		consoleplayer == Net_Arbitrator,
-		delay, msDelay);
+		delay, msDelay,
+		buffer, msBuffer);
 
 	if (NetMode == NET_PacketServer && consoleplayer != Net_Arbitrator)
 		out.AppendFormat("\tAvg latency: %03ums", min(ClientStates[consoleplayer].AverageLatency, 999u));

From 1e1ab58ee258da1d664d71494f64424a6a115b97 Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Fri, 4 Jul 2025 01:07:18 -0400
Subject: [PATCH 245/384] Fixed startup music being cancelled

This seems to happen from one of the music cvar callbacks wiping it, but I'm ensure of which (possibly music volume?). Move it to after cvar initializing until a proper fix can be found.
---
 src/d_main.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/d_main.cpp b/src/d_main.cpp
index fe3812ddd..52d3746b5 100644
--- a/src/d_main.cpp
+++ b/src/d_main.cpp
@@ -3237,8 +3237,6 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw
 		else if (gameinfo.gametype == GAME_Strife)
 			GameStartupInfo.Type = FStartupInfo::StrifeStartup;
 	}
-
-	StartScreen = nostartscreen? nullptr : GetGameStartScreen(per_shader_progress > 0 ? max_progress * 10 / 9 : max_progress + 3);
 	
 	GameConfig->DoKeySetup(gameinfo.ConfigName.GetChars());
 
@@ -3281,7 +3279,6 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw
 	if (!restart)
 	{
 		screen->CompileNextShader();
-		if (StartScreen != nullptr) StartScreen->Render();
 	}
 	else
 	{
@@ -3291,6 +3288,9 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw
 
 	// Base systems have been inited; enable cvar callbacks
 	FBaseCVar::EnableCallbacks ();
+
+	StartScreen = nostartscreen? nullptr : GetGameStartScreen(per_shader_progress > 0 ? max_progress * 10 / 9 : max_progress + 3);
+	if (StartScreen != nullptr) StartScreen->Render();
 	
 	// +compatmode cannot be used on the command line, so use this as a substitute
 	auto compatmodeval = Args->CheckValue("-compatmode");

From a1a3be10464dfde5341de139c6ab20db58b8f5af Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Thu, 3 Jul 2025 15:16:22 -0400
Subject: [PATCH 246/384] Clean up console commands

Improve functionality of network console commands and make displaying of who is who clearer.
---
 src/d_net.cpp     | 322 ++++++++++++++++++++++++++--------------------
 src/d_netinfo.cpp |  72 +++++++----
 2 files changed, 233 insertions(+), 161 deletions(-)

diff --git a/src/d_net.cpp b/src/d_net.cpp
index fff61c3c0..084943e19 100644
--- a/src/d_net.cpp
+++ b/src/d_net.cpp
@@ -2476,9 +2476,9 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player)
 				if (deathmatch && teamplay)
 					Printf(PRINT_CHAT, "(All) ");
 				if ((who & MSG_BOLD) && !cl_noboldchat)
-					Printf(PRINT_CHAT, TEXTCOLOR_BOLD "* %s" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, s);
+					Printf(PRINT_CHAT, TEXTCOLOR_BOLD "* %s [%d]" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, player, s);
 				else
-					Printf(PRINT_CHAT, "%s" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, s);
+					Printf(PRINT_CHAT, "%s [%d]" TEXTCOLOR_CHAT ": %s" TEXTCOLOR_CHAT "\n", name, player, s);
 
 				if (!cl_nochatsound)
 					S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE);
@@ -2492,9 +2492,9 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player)
 				if (deathmatch && teamplay)
 					Printf(PRINT_TEAMCHAT, "(Team) ");
 				if ((who & MSG_BOLD) && !cl_noboldchat)
-					Printf(PRINT_TEAMCHAT, TEXTCOLOR_BOLD "* %s" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, s);
+					Printf(PRINT_TEAMCHAT, TEXTCOLOR_BOLD "* %s [%d]" TEXTCOLOR_BOLD "%s" TEXTCOLOR_BOLD "\n", name, player, s);
 				else
-					Printf(PRINT_TEAMCHAT, "%s" TEXTCOLOR_TEAMCHAT ": %s" TEXTCOLOR_TEAMCHAT "\n", name, s);
+					Printf(PRINT_TEAMCHAT, "%s [%d]" TEXTCOLOR_TEAMCHAT ": %s" TEXTCOLOR_TEAMCHAT "\n", name, player, s);
 
 				if (!cl_nochatsound)
 					S_Sound(CHAN_VOICE, CHANF_UI, gameinfo.chatSound, 1.0f, ATTN_NONE);
@@ -2859,8 +2859,10 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player)
 		{
 			uint8_t playernum = ReadInt8(stream);
 			players[playernum].settings_controller = true;
-			if (consoleplayer == playernum || consoleplayer == Net_Arbitrator)
-				Printf("%s has been added to the controller list.\n", players[playernum].userinfo.GetName());
+			if (consoleplayer == playernum)
+				Printf("You can now control game settings\n");
+			else if (consoleplayer == Net_Arbitrator)
+				Printf("%s [%d] is now a settings controller\n", players[playernum].userinfo.GetName(), playernum);
 		}
 		break;
 
@@ -2868,8 +2870,10 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player)
 		{
 			uint8_t playernum = ReadInt8(stream);
 			players[playernum].settings_controller = false;
-			if (consoleplayer == playernum || consoleplayer == Net_Arbitrator)
-				Printf("%s has been removed from the controller list.\n", players[playernum].userinfo.GetName());
+			if (consoleplayer == playernum)
+				Printf("You can no longer control game settings\n");
+			else if (consoleplayer == Net_Arbitrator)
+				Printf("%s [%d] is no longer a settings controller\n", players[playernum].userinfo.GetName(), playernum);
 		}
 		break;
 
@@ -3021,11 +3025,10 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player)
 			{
 				I_Error("You have been kicked from the game");
 			}
-			else
+			else if (NetworkClients.InGame(pNum))
 			{
-				Printf("%s has been kicked from the game\n", players[pNum].userinfo.GetName());
-				if (NetworkClients.InGame(pNum))
-					DisconnectClient(pNum);
+				Printf("%s [%d] has been kicked from the game\n", players[pNum].userinfo.GetName(), pNum);
+				DisconnectClient(pNum);
 			}
 		}
 		break;
@@ -3287,7 +3290,7 @@ CCMD(pings)
 {
 	if (!netgame)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in a net game\n");
 		return;
 	}
 
@@ -3298,37 +3301,21 @@ CCMD(pings)
 	for (auto client : NetworkClients)
 	{
 		if ((NetMode == NET_PeerToPeer && client != consoleplayer) || (NetMode == NET_PacketServer && client != Net_Arbitrator))
-			Printf("%ums %s\n", ClientStates[client].AverageLatency, players[client].userinfo.GetName());
-	}
-}
-
-CCMD(listplayers)
-{
-	if (!netgame)
-	{
-		Printf("Not currently in a net game\n");
-		return;
-	}
-
-	for (auto client : NetworkClients)
-	{
-		if (client == consoleplayer)
-			Printf("* ");
-		Printf("%s - %d\n", players[client].userinfo.GetName(), client);
+			Printf("%ums %s [%d]\n", ClientStates[client].AverageLatency, players[client].userinfo.GetName(), client);
 	}
 }
 
 CCMD(kick)
 {
-	if (argv.argc() == 1)
+	if (argv.argc() < 2)
 	{
-		Printf("Usage: kick \n");
+		Printf("Usage: kick \nRemove these clients from the game\n");
 		return;
 	}
 
 	if (!netgame)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in a net game\n");
 		return;
 	}
 
@@ -3336,99 +3323,102 @@ CCMD(kick)
 	// the host can grant.
 	if (consoleplayer != Net_Arbitrator)
 	{
-		Printf("Only the host is allowed to kick other players\n");
+		Printf("This command is only accessible to the host\n");
 		return;
 	}
 
-	int pNum = -1;
-	if (!C_IsValidInt(argv[1], pNum))
+	TArray cNums = {};
+	for (size_t i = 1u; i < argv.argc(); ++i)
 	{
-		Printf("A player number must be provided. Use listplayers for more information\n");
-		return;
+		int cNum = -1;
+		if (!C_IsValidInt(argv[i], cNum) || cNum < 0 || cNum >= MAXPLAYERS)
+			Printf("Bad client number %s\n", argv[i]);
+		else if (cNum != consoleplayer && cNums.Find(cNum) >= cNums.Size())
+			cNums.Push(cNum);
 	}
 
-	if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS)
+	for (auto cNum : cNums)
 	{
-		Printf("Invalid player number provided\n");
-		return;
+		if (!NetworkClients.InGame(cNum))
+		{
+			Printf("Client %d is not in game\n", cNum);
+		}
+		else
+		{
+			Net_WriteInt8(DEM_KICK);
+			Net_WriteInt8(cNum);
+		}
 	}
-
-	if (!NetworkClients.InGame(pNum))
-	{
-		Printf("Player is not currently in the game\n");
-		return;
-	}
-
-	Net_WriteInt8(DEM_KICK);
-	Net_WriteInt8(pNum);
 }
 
 CCMD(mute)
 {
-	if (argv.argc() == 1)
+	if (argv.argc() < 2)
 	{
-		Printf("Usage: mute  - Don't receive messages from this player\n");
+		Printf("Usage: mute \nDisable messages from these players\n");
 		return;
 	}
 
-	if (!netgame)
+	if (!multiplayer)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in multiplayer\n");
 		return;
 	}
 
-	int pNum = -1;
-	if (!C_IsValidInt(argv[1], pNum))
+	TArray pNums = {};
+	for (size_t i = 1u; i < argv.argc(); ++i)
 	{
-		Printf("A player number must be provided. Use listplayers for more information\n");
-		return;
+		int pNum = -1;
+		if (!C_IsValidInt(argv[i], pNum) || pNum < 0 || pNum >= MAXPLAYERS)
+			Printf("Bad player number %s\n", argv[i]);
+		else if (pNum != consoleplayer && pNums.Find(pNum) >= pNums.Size())
+			pNums.Push(pNum);
 	}
 
-	if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS)
+	for (auto pNum : pNums)
 	{
-		Printf("Invalid player number provided\n");
-		return;
+		if (!playeringame[pNum])
+		{
+			Printf("Player %d is not in game\n", pNum);
+		}
+		else
+		{
+			MutedClients |= (uint64_t)1u << pNum;
+			Printf("Muted player %s [%d]\n", players[pNum].userinfo.GetName(), pNum);
+		}
 	}
-
-	if (!NetworkClients.InGame(pNum))
-	{
-		Printf("Player is not currently in the game\n");
-		return;
-	}
-
-	MutedClients |= (uint64_t)1u << pNum;
 }
 
 CCMD(muteall)
 {
-	if (!netgame)
+	if (!multiplayer)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in multiplayer\n");
 		return;
 	}
 
-	for (auto client : NetworkClients)
+	for (int i = 0; i < MAXPLAYERS; ++i)
 	{
-		if (client != consoleplayer)
-			MutedClients |= (uint64_t)1u << client;
+		if (playeringame[i] && i != consoleplayer)
+			MutedClients |= (uint64_t)1u << i;
 	}
 }
 
 CCMD(listmuted)
 {
-	if (!netgame)
+	if (!multiplayer)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in multiplayer\n");
 		return;
 	}
 
 	bool found = false;
-	for (auto client : NetworkClients)
+	for (int i = 0; i < MAXPLAYERS; ++i)
 	{
-		if (MutedClients & ((uint64_t)1u << client))
+		if (MutedClients & ((uint64_t)1u << i))
 		{
 			found = true;
-			Printf("%s - %d\n", players[client].userinfo.GetName(), client);
+			Printf("%d. %s\n", i, players[i].userinfo.GetName());
 		}
 	}
 
@@ -3438,39 +3428,47 @@ CCMD(listmuted)
 
 CCMD(unmute)
 {
-	if (argv.argc() == 1)
+	if (argv.argc() < 2)
 	{
-		Printf("Usage: unmute  - Allow messages from this player again\n");
+		Printf("Usage: unmute \nAllow messages from these players again\n");
 		return;
 	}
 
-	if (!netgame)
+	if (!multiplayer)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in multiplayer\n");
 		return;
 	}
 
-	int pNum = -1;
-	if (!C_IsValidInt(argv[1], pNum))
+	TArray pNums = {};
+	for (size_t i = 1u; i < argv.argc(); ++i)
 	{
-		Printf("A player number must be provided. Use listplayers for more information\n");
-		return;
+		int pNum = -1;
+		if (!C_IsValidInt(argv[i], pNum) || pNum < 0 || pNum >= MAXPLAYERS)
+			Printf("Bad player number %s\n", argv[i]);
+		else if (pNum != consoleplayer && pNums.Find(pNum) >= pNums.Size())
+			pNums.Push(pNum);
 	}
 
-	if (pNum == consoleplayer || pNum < 0 || pNum >= MAXPLAYERS)
+	for (auto pNum : pNums)
 	{
-		Printf("Invalid player number provided\n");
-		return;
+		if (!playeringame[pNum])
+		{
+			Printf("Player %d is not in game\n", pNum);
+		}
+		else
+		{
+			MutedClients &= ~((uint64_t)1u << pNum);
+			Printf("Unmuted player %s [%d]\n", players[pNum].userinfo.GetName(), pNum);
+		}
 	}
-
-	MutedClients &= ~((uint64_t)1u << pNum);
 }
 
 CCMD(unmuteall)
 {
-	if (!netgame)
+	if (!multiplayer)
 	{
-		Printf("Not currently in a net game\n");
+		Printf("This command can only be used when playing in multiplayer\n");
 		return;
 	}
 
@@ -3479,108 +3477,158 @@ CCMD(unmuteall)
 
 //==========================================================================
 //
-// Network_Controller
+// Net_ChangeSettingsControllers
 //
 // Implement players who have the ability to change settings in a network
 // game.
 //
 //==========================================================================
 
-static void Network_Controller(int pNum, bool add)
+static void Net_ChangeSettingsControllers(const TArray& cNums, bool add)
 {
 	if (!netgame)
 	{
-		Printf("This command can only be used when playing a net game.\n");
+		Printf("This command can only be used when playing in a net game\n");
 		return;
 	}
 
 	if (consoleplayer != Net_Arbitrator)
 	{
-		Printf("This command is only accessible to the host.\n");
+		Printf("This command is only accessible to the host\n");
 		return;
 	}
 
-	if (pNum == Net_Arbitrator)
+	for (auto cNum : cNums)
 	{
-		Printf("The host cannot change their own settings controller status.\n");
-		return;
+		if (cNum == Net_Arbitrator)
+		{
+			Printf("The host cannot change their own settings controller status\n");
+		}
+		else if (!NetworkClients.InGame(cNum))
+		{
+			Printf("Client %d is not in game\n", cNum);
+		}
+		else if (players[cNum].settings_controller && add)
+		{
+			Printf("Client %d is already a settings controller\n", cNum);
+		}
+		else if (!players[cNum].settings_controller && !add)
+		{
+			Printf("Client %d is already not a settings controller\n", cNum);
+		}
+		else
+		{
+			Net_WriteInt8(add ? DEM_ADDCONTROLLER : DEM_DELCONTROLLER);
+			Net_WriteInt8(cNum);
+		}
 	}
-
-	if (!NetworkClients.InGame(pNum))
-	{
-		Printf("Player %d is not a valid client\n", pNum);
-		return;
-	}
-
-	if (players[pNum].settings_controller && add)
-	{
-		Printf("%s is already on the setting controller list.\n", players[pNum].userinfo.GetName());
-		return;
-	}
-
-	if (!players[pNum].settings_controller && !add)
-	{
-		Printf("%s is not on the setting controller list.\n", players[pNum].userinfo.GetName());
-		return;
-	}
-
-	Net_WriteInt8(add ? DEM_ADDCONTROLLER : DEM_DELCONTROLLER);
-	Net_WriteInt8(pNum);
 }
 
 //==========================================================================
 //
-// CCMD net_addcontroller
+// CCMD addsettingscontrollers
 //
 //==========================================================================
 
-CCMD(net_addcontroller)
+CCMD(addsettingscontrollers)
 {
 	if (argv.argc() < 2)
 	{
-		Printf("Usage: net_addcontroller \n");
+		Printf("Usage: addsettingscontrollers \nAllow these clients to control game settings\n");
 		return;
 	}
 
-	Network_Controller(atoi (argv[1]), true);
+	TArray cNums = {};
+	for (size_t i = 1u; i < argv.argc(); ++i)
+	{
+		int cNum = -1;
+		if (!C_IsValidInt(argv[i], cNum) || cNum < 0 || cNum >= MAXPLAYERS)
+			Printf("Bad client number %s\n", argv[i]);
+		else if (cNum != Net_Arbitrator && cNums.Find(cNum) >= cNums.Size())
+			cNums.Push(cNum);
+	}
+
+	Net_ChangeSettingsControllers(cNums, true);
 }
 
 //==========================================================================
 //
-// CCMD net_removecontroller
+// CCMD removesettingscontrollers
 //
 //==========================================================================
 
-CCMD(net_removecontroller)
+CCMD(removesettingscontrollers)
 {
 	if (argv.argc() < 2)
 	{
-		Printf("Usage: net_removecontroller \n");
+		Printf("Usage: removesettingscontrollers \nRemove the ability for these clients to control game settings\n");
 		return;
 	}
 
-	Network_Controller(atoi(argv[1]), false);
+	TArray cNums = {};
+	for (size_t i = 1u; i < argv.argc(); ++i)
+	{
+		int cNum = -1;
+		if (!C_IsValidInt(argv[i], cNum) || cNum < 0 || cNum >= MAXPLAYERS)
+			Printf("Bad player number %s\n", argv[i]);
+		else if (cNum != Net_Arbitrator && cNums.Find(cNum) >= cNums.Size())
+			cNums.Push(cNum);
+	}
+
+	Net_ChangeSettingsControllers(cNums, false);
 }
 
 //==========================================================================
 //
-// CCMD net_listcontrollers
+// CCMD removeallsettingscontrollers
 //
 //==========================================================================
 
-CCMD(net_listcontrollers)
+CCMD(removeallsettingscontrollers)
+{
+	TArray cNums = {};
+	for (auto client : NetworkClients)
+	{
+		if (client != Net_Arbitrator && players[client].settings_controller)
+			cNums.Push(client);
+	}
+
+	Net_ChangeSettingsControllers(cNums, false);
+}
+
+//==========================================================================
+//
+// CCMD listsettingscontrollers
+//
+//==========================================================================
+
+CCMD(listsettingscontrollers)
 {
 	if (!netgame)
 	{
-		Printf ("This command can only be used when playing a net game.\n");
+		Printf("This command can only be used when playing in a net game\n");
+		return;
+	}
+
+	TArray cNums = {};
+	for (auto client : NetworkClients)
+	{
+		if (client != Net_Arbitrator && players[client].settings_controller)
+			cNums.Push(client);
+	}
+
+	if (!cNums.Size())
+	{
+		Printf("No other settings controllers\n");
 		return;
 	}
 
 	Printf("The following players can change the game settings:\n");
-
-	for (auto client : NetworkClients)
+	for (auto cNum : cNums)
 	{
-		if (players[client].settings_controller)
-			Printf("- %s\n", players[client].userinfo.GetName());
+		Printf("%d. %s", cNum, players[cNum].userinfo.GetName());
+		if (cNum == consoleplayer)
+			Printf(" [*]");
+		Printf("\n");
 	}
 }
diff --git a/src/d_netinfo.cpp b/src/d_netinfo.cpp
index c528aa5d1..e319f9234 100644
--- a/src/d_netinfo.cpp
+++ b/src/d_netinfo.cpp
@@ -1016,50 +1016,75 @@ void ReadUserInfo(FSerializer &arc, userinfo_t &info, FString &skin)
 	}
 }
 
-CCMD (playerinfo)
+CCMD(playerinfo)
 {
-	if (argv.argc() < 2)
+	TArray inGame = {};
+	for (int i = 0; i < MAXPLAYERS; ++i)
 	{
-		int i;
+		if (playeringame[i])
+			inGame.Push(i);
+	}
 
-		for (i = 0; i < MAXPLAYERS; i++)
+	const bool isMulti = inGame.Size() > 1;
+	if (isMulti && argv.argc() < 2)
+	{
+		for (auto i : inGame)
 		{
-			if (playeringame[i])
+			Printf("%d. %s", i, players[i].userinfo.GetName());
+			if (i == consoleplayer || i == Net_Arbitrator || players[i].Bot != nullptr || players[i].settings_controller)
 			{
-				Printf("%d. %s\n", i, players[i].userinfo.GetName());
+				Printf(" [");
+				if (i == consoleplayer)
+					Printf("*");
+				if (i == Net_Arbitrator)
+					Printf("H");
+				if (players[i].Bot != nullptr)
+					Printf("B");
+				else if (players[i].settings_controller && i != Net_Arbitrator)
+					Printf("C");
+				Printf("]");
 			}
+			Printf("\n");
 		}
+		Printf("Pass a player number for more info\n");
 	}
 	else
 	{
-		int i = atoi(argv[1]);
-
-		if (i < 0 || i >= MAXPLAYERS)
+		int i = -1;
+		if (!isMulti)
 		{
-			Printf("Bad player number\n");
+			i = consoleplayer;
+		}
+		else if (!C_IsValidInt(argv[1], i) || i < 0 || i >= MAXPLAYERS)
+		{
+			Printf("Bad player number %s\n", argv[1]);
 			return;
 		}
-		userinfo_t *ui = &players[i].userinfo;
 
 		if (!playeringame[i])
 		{
-			Printf(TEXTCOLOR_ORANGE "Player %d is not in the game\n", i);
+			Printf("Player %d is not in game\n", i);
 			return;
 		}
 
+		const userinfo_t& info = players[i].userinfo;
+
 		// Print special info
-		Printf("%20s: %s\n",      "Name", ui->GetName());
-		Printf("%20s: %s (%d)\n", "Team", ui->GetTeam() == TEAM_NONE ? "None" : Teams[ui->GetTeam()].GetName(), ui->GetTeam());
-		Printf("%20s: %s (%d)\n", "Skin", Skins[ui->GetSkin()].Name.GetChars(), ui->GetSkin());
-		Printf("%20s: %s (%d)\n", "Gender", GenderNames[ui->GetGender()], ui->GetGender());
+		Printf("%20s: %s\n",	  "Host", i == Net_Arbitrator ? "Yes" : "No");
+		Printf("%20s: %s\n",	  "Console Player", i == consoleplayer ? "Yes" : "No");
+		Printf("%20s: %s\n",	  "Bot", players[i].Bot != nullptr ? "Yes" : "No");
+		Printf("%20s: %s\n",	  "Settings Controller", players[i].settings_controller && players[i].Bot == nullptr ? "Yes" : "No");
+		Printf("%20s: %s\n",      "Name", info.GetName());
+		Printf("%20s: %s (%d)\n", "Team", info.GetTeam() == TEAM_NONE ? "None" : Teams[info.GetTeam()].GetName(), info.GetTeam());
+		Printf("%20s: %s (%d)\n", "Skin", Skins[info.GetSkin()].Name.GetChars(), info.GetSkin());
+		Printf("%20s: %s (%d)\n", "Gender", GenderNames[info.GetGender()], info.GetGender());
 		Printf("%20s: %s (%d)\n", "PlayerClass",
-			ui->GetPlayerClassNum() == -1 ? "Random" : ui->GetPlayerClassType()->GetDisplayName().GetChars(),
-			ui->GetPlayerClassNum());
+			info.GetPlayerClassNum() == -1 ? "Random" : info.GetPlayerClassType()->GetDisplayName().GetChars(),
+			info.GetPlayerClassNum());
 
 		// Print generic info
-		TMapIterator it(*ui);
-		TMap::Pair *pair;
-
+		TMap::ConstIterator it = { info };
+		TMap::ConstPair* pair = nullptr;
 		while (it.NextPair(pair))
 		{
 			if (pair->Key != NAME_Name && pair->Key != NAME_Team && pair->Key != NAME_Skin &&
@@ -1068,10 +1093,9 @@ CCMD (playerinfo)
 				Printf("%20s: %s\n", pair->Key.GetChars(), pair->Value->GetHumanString());
 			}
 		}
-		if (argv.argc() > 2)
-		{
+
+		if (argv.argc() > 2 || (!isMulti && argv.argc() > 1))
 			PrintMiscActorInfo(players[i].mo);
-		}
 	}
 }
 

From 9a111931cf6effac0b6e12c6d5a551c24d2eb6d2 Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Thu, 3 Jul 2025 12:08:30 -0400
Subject: [PATCH 247/384] Fix for WorldPaused

Also consider the console rising to be unpaused.
---
 src/scripting/vmthunks.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp
index 099a98196..5b5f1183e 100644
--- a/src/scripting/vmthunks.cpp
+++ b/src/scripting/vmthunks.cpp
@@ -510,7 +510,7 @@ int WorldPaused()
 	if (netgame || gamestate != GS_LEVEL)
 		return false;
 
-	return pauseext || menuactive == MENU_On || ConsoleState != c_up;
+	return pauseext || menuactive == MENU_On || ConsoleState == c_down || ConsoleState == c_falling;
 }
 
 DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, WorldPaused, WorldPaused)

From a2450e119524f95837c0263c69185a4cd6513717 Mon Sep 17 00:00:00 2001
From: Cacodemon345 
Date: Sun, 29 Jun 2025 01:01:42 +0600
Subject: [PATCH 248/384] Add mechanism for Prosperity Rune-like powerups

Also add a flag to use Zandronum/Skulltag semantics for BasicArmorPickup
---
 src/namedef_custom.h                          |  1 +
 src/playsim/p_mobj.cpp                        | 10 ++++++
 .../static/zscript/actors/inventory/armor.zs  | 32 ++++++++++++++++---
 wadsrc/static/zscript/actors/player/player.zs |  1 +
 4 files changed, 40 insertions(+), 4 deletions(-)

diff --git a/src/namedef_custom.h b/src/namedef_custom.h
index bfa97008c..212fdd6f5 100644
--- a/src/namedef_custom.h
+++ b/src/namedef_custom.h
@@ -307,6 +307,7 @@ xx(GruntSpeed)
 xx(JumpZ)
 xx(MugShotMaxHealth)
 xx(BonusHealth)
+xx(MaxPickupHealth)
 xx(PlayerFlags)
 xx(InvSel)
 xx(FullHeight)
diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp
index 24aec14e9..0b01f2cfe 100644
--- a/src/playsim/p_mobj.cpp
+++ b/src/playsim/p_mobj.cpp
@@ -1197,6 +1197,9 @@ int P_GetRealMaxHealth(AActor *actor, int max)
 	// Max is 0 by default, preserving default behavior for P_GiveBody()
 	// calls while supporting health pickups.
 	auto player = actor->player;
+	auto orig_max = max;
+	int maxPickupHealth = actor->IntVar(NAME_MaxPickupHealth);
+
 	if (max <= 0)
 	{
 		max = actor->GetMaxHealth(true);
@@ -1228,6 +1231,13 @@ int P_GetRealMaxHealth(AActor *actor, int max)
 			max += actor->IntVar(NAME_BonusHealth);
 		}
 	}
+	if (maxPickupHealth)
+	{
+		max = maxPickupHealth;
+		// Allow health items with greater MaxAmount values to work properly.
+		if (orig_max > 0 && orig_max > max)
+			max = orig_max;
+	}
 	return max;
 }
 
diff --git a/wadsrc/static/zscript/actors/inventory/armor.zs b/wadsrc/static/zscript/actors/inventory/armor.zs
index 29c82a1fc..b0a93b11f 100644
--- a/wadsrc/static/zscript/actors/inventory/armor.zs
+++ b/wadsrc/static/zscript/actors/inventory/armor.zs
@@ -62,8 +62,12 @@ class BasicArmor : Armor
 	int MaxAbsorb;
 	int MaxFullAbsorb;
 	int BonusCount;
+	int MaxAllowedAmount;
 	Name ArmorType;
 	int ActualSaveAmount;
+	private uint ArmorFlags;
+
+	flagdef AltSemantics: ArmorFlags, 0; // Zandronum behaviour.
 	
 	Default
 	{
@@ -114,6 +118,8 @@ class BasicArmor : Armor
 		copy.BonusCount = BonusCount;
 		copy.ArmorType = ArmorType;
 		copy.ActualSaveAmount = ActualSaveAmount;
+		copy.bAltSemantics = bAltSemantics;
+		copy.MaxAllowedAmount = MaxAllowedAmount;
 		GoAwayAndDie ();
 		return copy;
 	}
@@ -297,9 +303,15 @@ class BasicArmorBonus : Armor
 			return BonusCount > 0 ? result : true;
 		}
 
+		let maximumAllowedAmount = MaxSaveAmount + armor.BonusCount;
+		if (armor.MaxAllowedAmount)
+		{
+			maximumAllowedAmount = armor.MaxAllowedAmount;
+		}
+
 		// If you already have more armor than this item can give you, you can't
 		// use it.
-		if (armor.Amount >= MaxSaveAmount + armor.BonusCount)
+		if (armor.Amount >= maximumAllowedAmount)
 		{
 			return result;
 		}
@@ -315,7 +327,7 @@ class BasicArmorBonus : Armor
 			armor.ActualSaveAmount = MaxSaveAmount;
 		}
 
-		armor.Amount = min(armor.Amount + saveAmount, MaxSaveAmount + armor.BonusCount);
+		armor.Amount = min(armor.Amount + saveAmount, maximumAllowedAmount);
 		armor.MaxAmount = max(armor.MaxAmount, MaxSaveAmount);
 		return true;
 	}
@@ -392,6 +404,8 @@ class BasicArmorPickup : Armor
 	{
 		int SaveAmount = GetSaveAmount();
 		let armor = BasicArmor(Owner.FindInventory("BasicArmor", true));
+		let lBonusCount = (armor == null) ? 0 : armor.BonusCount;
+		let lMaxAmount = SaveAmount + (armor == null ? 0 : lBonusCount);
 
 		// This should really never happen but let's be prepared for a broken inventory.
 		if (armor == null)
@@ -402,9 +416,19 @@ class BasicArmorPickup : Armor
 		}
 		else
 		{
+			if (armor.MaxAllowedAmount)
+			{
+				lMaxAmount = armor.MaxAllowedAmount;
+			}
 			// If you already have more armor than this item gives you, you can't
 			// use it.
-			if (armor.Amount >= SaveAmount + armor.BonusCount)
+			if (armor.bAltSemantics ? (armor.Amount > lMaxAmount) : (armor.Amount >= lMaxAmount))
+			{
+				return false;
+			}
+			// If we have the same amount of the armor we're trying to use, but our armor offers
+			// better protection, don't pick it up.
+			if (armor.bAltSemantics && armor.Amount == lMaxAmount && armor.SavePercent >= (clamp(SavePercent, 0, 100) / 100))
 			{
 				return false;
 			}
@@ -416,7 +440,7 @@ class BasicArmorPickup : Armor
 		}
 		
 		armor.SavePercent = clamp(SavePercent, 0, 100) / 100;
-		armor.Amount = SaveAmount + armor.BonusCount;
+		armor.Amount = armor.bAltSemantics ? (min(SaveAmount + armor.Amount, lMaxAmount)) : (SaveAmount + armor.BonusCount);
 		armor.MaxAmount = SaveAmount;
 		armor.Icon = Icon;
 		armor.MaxAbsorb = MaxAbsorb;
diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs
index a64fca8f9..dde0fa117 100644
--- a/wadsrc/static/zscript/actors/player/player.zs
+++ b/wadsrc/static/zscript/actors/player/player.zs
@@ -22,6 +22,7 @@ class PlayerPawn : Actor
 	int			MaxHealth;
 	int			BonusHealth;
 	int			MugShotMaxHealth;
+	int			MaxPickupHealth; // overrides MaxAmount of pickups and BonusHealth.
 	int			RunHealth;
 	private int	PlayerFlags;
 	clearscope Inventory	InvFirst;		// first inventory item displayed on inventory bar

From 672a21f533f6598647573b0c213f48c76dc97ea0 Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Sat, 5 Jul 2025 04:08:19 -0400
Subject: [PATCH 249/384] Fixed bad VisualThinker nodes getting into the render
 list

Relink on loading instead of serializing since order doesn't matter here.
---
 src/p_saveg.cpp          |  1 -
 src/playsim/p_effect.cpp | 11 ++++++++---
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/p_saveg.cpp b/src/p_saveg.cpp
index a4d82fea6..c814fe8d2 100644
--- a/src/p_saveg.cpp
+++ b/src/p_saveg.cpp
@@ -995,7 +995,6 @@ void FLevelLocals::Serialize(FSerializer &arc, bool hubload)
 		("automap", automap)
 		("interpolator", interpolator)
 		("frozenstate", frozenstate)
-		("visualthinkerhead", VisualThinkerHead)
 		("actorbehaviors", ActorBehaviors);
 
 
diff --git a/src/playsim/p_effect.cpp b/src/playsim/p_effect.cpp
index 0cbc86a64..62db58742 100644
--- a/src/playsim/p_effect.cpp
+++ b/src/playsim/p_effect.cpp
@@ -1364,13 +1364,18 @@ void DVisualThinker::Serialize(FSerializer& arc)
 		("lightlevel", LightLevel)
 		("animData", PT.animData)
 		("flags", PT.flags)
-		("visualThinkerFlags", flags)
-		("next", _next)
-		("prev", _prev);
+		("visualThinkerFlags", flags);
     
     if(arc.isReading())
     {
         UpdateSector();
+		_prev = _next = nullptr;
+		if (Level->VisualThinkerHead != nullptr)
+		{
+			Level->VisualThinkerHead->_prev = this;
+			_next = Level->VisualThinkerHead;
+		}
+		Level->VisualThinkerHead = this;
     }
 }
 

From 2a5cce543bfd265f327b796b28904584a8db1e95 Mon Sep 17 00:00:00 2001
From: Marcus Minhorst <136136617+the-phinet@users.noreply.github.com>
Date: Sat, 5 Jul 2025 16:41:40 -0400
Subject: [PATCH 250/384] Gamepad Improvements

* Cleanup: Alignment, long lines, Replace 0 with SDLK_UNKNOWN

* Gamecontroller api analogue input

* Added some button handling

* Added mapping for other buttons

* Added trigger events

* Added force_joystick flag

* Removed force_joystick flag

Rationale:
1. It was actually broken lol
2. I cannot think of a case where enabling this would be a useful thing for
gzdoom. If the user is using a gamecontroller, it is pointless. If they are
not using a gamecontroller, it will just default to using the joystick api.
If they are not using a gamecontroller, but SDL thinks they are, it is an
SDL bug, and will be reported and fixed

* Modified default mapping

* Added analogue to digital threshold

* Added analogue response curve

* Per axis settings

* Fixed controller reconnect

* Added threshold and curve to IJoystickConfig
Enabled saving of settings

* Added stubs

* Cleanup

Constants are no longer defines.
Constants are mostly shared between backends.
Moved some logic to m_joy

* Implemented xinput stubs

* Implemented dinput stubs

* Implemented ps2 stubs (untested)

* Fixed inclusive check

* Implemented osx stubs (untested)

* Fixed curve implementation

No longer savable, I screwed the curve function up.
I though it needed 2 control points, but it needs 4.
Need to re-do controller settings :(

* Now using CubicBezier struct

* Fixed SetDefaultConfig to match xinput behavior

* Expanded gamepad CCMD

* Rename enum JoyResponseCurve to EJoyCurve

* Initial menu implementation

* Fixed SDL controller setting saving

* SDL gamepads can now actually be disabled

* Fixed initial controller connect of some versions of SDL

* Spelling error

* Enable gamepad by default

* Fixed segfault on some versions of SDL

* Only block keydown
---
 src/common/console/c_bind.cpp                 |   4 +-
 src/common/console/keydef.h                   |   9 +-
 src/common/engine/m_joy.cpp                   | 330 ++++++++++++++-
 src/common/engine/m_joy.h                     |  45 +-
 src/common/menu/joystickmenu.cpp              |  50 +++
 src/common/menu/menu.cpp                      |   2 +-
 .../platform/posix/cocoa/i_joystick.cpp       | 112 ++++-
 src/common/platform/posix/sdl/i_input.cpp     | 250 ++++++++----
 src/common/platform/posix/sdl/i_input.h       |  13 +
 src/common/platform/posix/sdl/i_joystick.cpp  | 386 ++++++++++++++----
 src/common/platform/win32/i_dijoy.cpp         | 168 +++++++-
 src/common/platform/win32/i_rawps2.cpp        | 160 +++++++-
 src/common/platform/win32/i_xinput.cpp        | 167 +++++++-
 wadsrc/static/menudef.txt                     |   9 +
 .../zscript/engine/ui/menu/joystickmenu.zs    | 106 ++++-
 wadsrc/static/zscript/engine/ui/menu/menu.zs  |  19 +
 16 files changed, 1614 insertions(+), 216 deletions(-)
 create mode 100644 src/common/platform/posix/sdl/i_input.h

diff --git a/src/common/console/c_bind.cpp b/src/common/console/c_bind.cpp
index fe794c736..078a9a2b1 100644
--- a/src/common/console/c_bind.cpp
+++ b/src/common/console/c_bind.cpp
@@ -147,7 +147,9 @@ const char *KeyNames[NUM_KEYS] =
 	"DPadUp","DPadDown","DPadLeft","DPadRight",	// Gamepad buttons
 	"Pad_Start","Pad_Back","LThumb","RThumb",
 	"LShoulder","RShoulder","LTrigger","RTrigger",
-	"Pad_A", "Pad_B", "Pad_X", "Pad_Y"
+	"Pad_A", "Pad_B", "Pad_X", "Pad_Y",
+	"Paddle_1", "Paddle_2", "Paddle_3", "Paddle_4",
+	"Guide", "Pad_Misc", "Pad_Touchpad"
 };
 
 FKeyBindings Bindings;
diff --git a/src/common/console/keydef.h b/src/common/console/keydef.h
index 108806774..620b5f5e7 100644
--- a/src/common/console/keydef.h
+++ b/src/common/console/keydef.h
@@ -134,8 +134,15 @@ enum EKeyCodes
 	KEY_PAD_B				= 0x1C1,
 	KEY_PAD_X				= 0x1C2,
 	KEY_PAD_Y				= 0x1C3,
+	KEY_PAD_PADDLE1			= 0x1C4,
+	KEY_PAD_PADDLE2			= 0x1C5,
+	KEY_PAD_PADDLE3			= 0x1C6,
+	KEY_PAD_PADDLE4			= 0x1C7,
+	KEY_PAD_GUIDE			= 0x1C8,
+	KEY_PAD_MISC1			= 0x1C9,
+	KEY_PAD_TOUCHPAD		= 0x1CA,
 
-	NUM_KEYS				= 0x1C4,
+	NUM_KEYS				= 0x1CB,
 
 	NUM_JOYAXISBUTTONS		= 8,
 };
diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp
index 1e4b2f3e0..9df33efe9 100644
--- a/src/common/engine/m_joy.cpp
+++ b/src/common/engine/m_joy.cpp
@@ -33,6 +33,11 @@
 // HEADER FILES ------------------------------------------------------------
 
 #include 
+#include "c_dispatch.h"
+#include "gain_analysis.h"
+#include "keydef.h"
+#include "name.h"
+#include "tarray.h"
 #include "vectors.h"
 #include "m_joy.h"
 #include "configfile.h"
@@ -40,6 +45,7 @@
 #include "d_eventbase.h"
 #include "cmdlib.h"
 #include "printf.h"
+#include "zstring.h"
 
 // MACROS ------------------------------------------------------------------
 
@@ -57,9 +63,26 @@ EXTERN_CVAR(Bool, joy_ps2raw)
 EXTERN_CVAR(Bool, joy_dinput)
 EXTERN_CVAR(Bool, joy_xinput)
 
+extern const float JOYDEADZONE_DEFAULT = 0.1; // reduced from 0.25
+
+extern const float JOYSENSITIVITY_DEFAULT = 1.0;
+
+extern const float JOYTHRESH_DEFAULT = 0.05;
+extern const float JOYTHRESH_TRIGGER = 0.05;
+extern const float JOYTHRESH_STICK_X = 0.65;
+extern const float JOYTHRESH_STICK_Y = 0.35;
+
+extern const CubicBezier JOYCURVE[NUM_JOYCURVE] = {
+	{{0.3, 0.0, 0.7, 0.4}}, // DEFAULT -> QUADRATIC
+
+	{{0.0, 0.0, 1.0, 1.0}}, // LINEAR
+	{{0.3, 0.0, 0.7, 0.4}}, // QUADRATIC
+	{{0.5, 0.0, 0.7, 0.2}}, // CUBIC
+};
+
 // PUBLIC DATA DEFINITIONS -------------------------------------------------
 
-CUSTOM_CVARD(Bool, use_joystick, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present") 
+CUSTOM_CVARD(Bool, use_joystick, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present")
 {
 #ifdef _WIN32
 	joy_ps2raw->Callback();
@@ -163,6 +186,48 @@ bool M_LoadJoystickConfig(IJoystickConfig *joy)
 			joy->SetAxisScale(i, (float)atof(value));
 		}
 
+		mysnprintf(key + axislen, countof(key) - axislen, "threshold");
+		value = GameConfig->GetValueForKey(key);
+		if (value)
+		{
+			joy->SetAxisDigitalThreshold(i, (float)atof(value));
+		}
+
+		mysnprintf(key + axislen, countof(key) - axislen, "curve");
+		value = GameConfig->GetValueForKey(key);
+		if (value)
+		{
+			joy->SetAxisResponseCurve(i, (EJoyCurve)clamp(atoi(value), (int)JOYCURVE_CUSTOM, (int)NUM_JOYCURVE-1));
+		}
+
+		mysnprintf(key + axislen, countof(key) - axislen, "curve-x1");
+		value = GameConfig->GetValueForKey(key);
+		if (value)
+		{
+			joy->SetAxisResponseCurvePoint(i, 0, (float)atof(value));
+		}
+
+		mysnprintf(key + axislen, countof(key) - axislen, "curve-y1");
+		value = GameConfig->GetValueForKey(key);
+		if (value)
+		{
+			joy->SetAxisResponseCurvePoint(i, 1, (float)atof(value));
+		}
+
+		mysnprintf(key + axislen, countof(key) - axislen, "curve-x2");
+		value = GameConfig->GetValueForKey(key);
+		if (value)
+		{
+			joy->SetAxisResponseCurvePoint(i, 2, (float)atof(value));
+		}
+
+		mysnprintf(key + axislen, countof(key) - axislen, "curve-y2");
+		value = GameConfig->GetValueForKey(key);
+		if (value)
+		{
+			joy->SetAxisResponseCurvePoint(i, 3, (float)atof(value));
+		}
+
 		mysnprintf(key + axislen, countof(key) - axislen, "map");
 		value = GameConfig->GetValueForKey(key);
 		if (value)
@@ -227,6 +292,33 @@ void M_SaveJoystickConfig(IJoystickConfig *joy)
 				mysnprintf(value, countof(value), "%g", joy->GetAxisScale(i));
 				GameConfig->SetValueForKey(key, value);
 			}
+			if (!joy->IsAxisDigitalThresholdDefault(i))
+			{
+				mysnprintf(key + axislen, countof(key) - axislen, "threshold");
+				mysnprintf(value, countof(value), "%g", joy->GetAxisDigitalThreshold(i));
+				GameConfig->SetValueForKey(key, value);
+			}
+			if (!joy->IsAxisResponseCurveDefault(i))
+			{
+				mysnprintf(key + axislen, countof(key) - axislen, "curve");
+				mysnprintf(value, countof(value), "%d", joy->GetAxisResponseCurve(i));
+				GameConfig->SetValueForKey(key, value);
+			}
+			if (joy->GetAxisResponseCurve(i) == JOYCURVE_CUSTOM)
+			{
+				mysnprintf(key + axislen, countof(key) - axislen, "curve-x1");
+				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 0));
+				GameConfig->SetValueForKey(key, value);
+				mysnprintf(key + axislen, countof(key) - axislen, "curve-y1");
+				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 1));
+				GameConfig->SetValueForKey(key, value);
+				mysnprintf(key + axislen, countof(key) - axislen, "curve-x2");
+				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 2));
+				GameConfig->SetValueForKey(key, value);
+				mysnprintf(key + axislen, countof(key) - axislen, "curve-y2");
+				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 3));
+				GameConfig->SetValueForKey(key, value);
+			}
 			if (!joy->IsAxisMapDefault(i))
 			{
 				mysnprintf(key + axislen, countof(key) - axislen, "map");
@@ -243,6 +335,174 @@ void M_SaveJoystickConfig(IJoystickConfig *joy)
 	}
 }
 
+CCMD (gamepad)
+{
+	int COMMAND = 1, IDENTIFIER = 2, VALUE = 3;
+	int argc = argv.argc()-1;
+
+	TArray sticks;
+	I_GetJoysticks(sticks);
+
+	auto usage = []()
+	{
+		Printf(
+			"usage:"
+			"\n\tgamepad list"
+			"\n\tgamepad reset       pad"
+			"\n\tgamepad enabled     pad      [0|1]"
+			"\n\tgamepad background  pad      [0|1]"
+			"\n\tgamepad sensitivity pad      [float]"
+			"\n\tgamepad deadzone    pad.axis [float]"
+			"\n\tgamepad scale       pad.axis [float]"
+			"\n\tgamepad threshold   pad.axis [float]"
+			"\n\tgamepad curve       pad.axis [-1|0|1|2|3]"
+			"\n\tgamepad curve-x1    pad.axis [float]"
+			"\n\tgamepad curve-y1    pad.axis [float]"
+			"\n\tgamepad curve-x2    pad.axis [float]"
+			"\n\tgamepad curve-y2    pad.axis [float]"
+			"\n\tgamepad map         pad.axis [-1|0|1|2|3|4]"
+			"\n"
+		);
+	};
+
+	if (argc < COMMAND)
+	{
+		return usage();
+	};
+
+	FName command = argv[COMMAND];
+
+	if (argc < IDENTIFIER)
+	{
+		if (command == "list")
+		{
+			for (int i = 0; i < sticks.SSize(); i++)
+			{
+				Printf("%d: '%s'\n", i, sticks[i]->GetName().GetChars());
+				for (int j = 0; j < sticks[i]->GetNumAxes(); j++)
+				{
+					Printf("  %d.%d: '%s'\n", i, j, sticks[i]->GetAxisName(j));
+				}
+			}
+			return;
+		}
+		return usage();
+	}
+
+	const char * id = argv[IDENTIFIER];
+	const char * hasAxis = strchr(id, '.');
+
+	int pad, axis;
+
+	try {
+		pad = (int)std::stod(id);
+
+		if (pad < 0 || pad >= sticks.SSize())
+		{
+			return (void) Printf("Pad # out of range\n");
+		}
+	} catch (...) {
+		return (void) Printf("Failed to parse pad #\n");
+	}
+
+	if (hasAxis)
+	{
+		try {
+			axis = (int)std::stod(hasAxis+1);
+
+			if (axis < 0 || axis >= sticks[pad]->GetNumAxes())
+			{
+				return (void) Printf("Axis # out of range\n");
+			}
+		} catch (...) {
+			return (void) Printf("Failed to parse axis #\n");
+		}
+	}
+
+	float value = 0;
+	bool set = argc >= VALUE;
+
+	if (set)
+	{
+		try {
+			value = std::stod(argv[VALUE]);
+		} catch (...) {
+			return (void) Printf("Failed to parse args\n");
+		}
+	}
+
+	if (command == "reset")
+	{
+		if (set) return usage();
+		sticks[pad]->SetDefaultConfig();
+		sticks[pad]->SetEnabled(true);
+		sticks[pad]->SetEnabledInBackground(sticks[pad]->AllowsEnabledInBackground());
+		sticks[pad]->SetSensitivity(1);
+		return;
+	}
+	if (command == "enabled")
+	{
+		if (set) sticks[pad]->SetEnabled((int)value);
+		return (void) Printf("%d\n", sticks[pad]->GetEnabled());
+	}
+	if (command == "background")
+	{
+		if (set) sticks[pad]->SetEnabledInBackground((int)value);
+		return (void) Printf("%d\n", sticks[pad]->GetEnabledInBackground());
+	}
+	if (command == "sensitivity")
+	{
+		if (set) sticks[pad]->SetSensitivity(value);
+		return (void) Printf("%g\n", sticks[pad]->GetSensitivity());
+	}
+	if (command == "deadzone")
+	{
+		if (set) sticks[pad]->SetAxisDeadZone(axis, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisDeadZone(axis));
+	}
+	if (command == "scale")
+	{
+		if (set) sticks[pad]->SetAxisScale(axis, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisScale(axis));
+	}
+	if (command == "threshold")
+	{
+		if (set) sticks[pad]->SetAxisDigitalThreshold(axis, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisDigitalThreshold(axis));
+	}
+	if (command == "curve")
+	{
+		if (set) sticks[pad]->SetAxisResponseCurve(axis, (EJoyCurve)value);
+		return (void) Printf("%d\n", sticks[pad]->GetAxisResponseCurve(axis));
+	}
+	if (command == "curve-x1")
+	{
+		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 0, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 0));
+	}
+	if (command == "curve-y1")
+	{
+		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 1, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 1));
+	}
+	if (command == "curve-x2")
+	{
+		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 2, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 2));
+	}
+	if (command == "curve-y2")
+	{
+		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 3, value);
+		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 3));
+	}
+	if (command == "map")
+	{
+		if (set) sticks[pad]->SetAxisMap(axis, (EJoyAxis)value);
+		return (void) Printf("%d\n", sticks[pad]->GetAxisMap(axis));
+	}
+
+	return usage();
+}
 
 //===========================================================================
 //
@@ -278,6 +538,48 @@ double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons)
 	return axisval;
 }
 
+//===========================================================================
+//
+// Joy_ApplyResponseCurveBezier
+//
+// Applies cubic bezier easing function
+// Curve is defined by control points [(0,0) (x1,y1) (x2,y2) (1,1)]
+// https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/cubic-bezier
+//
+//===========================================================================
+
+double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input)
+{
+	// clamp + trivial cases
+	if (input == 0) return 0;
+	double sign = (input >= 0)? 1.0: -1.0;
+	input = abs(input);
+	input = (input > 1.0)? 1.0: input;
+	if (input == 1.0) return sign*input;
+
+	double t = input, T;
+	float x1 = curve.x1, y1 = curve.y1, x2 = curve.x2, y2 = curve.y2;
+
+	const int max_iter = 4;
+	for (auto i = 0; i < max_iter; i++)
+	{
+		T = 1-t;
+
+		double x = 3*T*T*t*x1 + 3*T*t*t*x2 + t*t*t;
+		double dx = 3*T*T*x1 + 6*T*t*(x2-x1) + 3*t*t*(1-x2);
+
+		// no div by 0
+		if (abs(dx) < 0.00001) break;
+
+		t = clamp(t - (x-input)/dx, 0.0, 1.0);
+	}
+
+	T = 1-t;
+	t = 3*T*T*t*y1 + 3*T*t*t*y2 + t*t*t;
+
+	return sign*t;
+}
+
 //===========================================================================
 //
 // Joy_XYAxesToButtons
@@ -315,6 +617,22 @@ int Joy_XYAxesToButtons(double x, double y)
 	return JoyAngleButtons[int(rad) & 7];
 }
 
+//===========================================================================
+//
+// Joy_GenerateButtonEvent
+//
+// Send either a button up or button down event for supplied key code
+//
+//===========================================================================
+
+void Joy_GenerateButtonEvent(bool down, EKeyCodes which)
+{
+	event_t event = { 0,0,0,0,0,0,0 };
+	event.type = down ? EV_KeyDown : EV_KeyUp;
+	event.data1 = which;
+	D_PostEvent(&event);
+}
+
 //===========================================================================
 //
 // Joy_GenerateButtonEvents
@@ -330,15 +648,12 @@ void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, in
 	int changed = oldbuttons ^ newbuttons;
 	if (changed != 0)
 	{
-		event_t ev = { 0, 0, 0, 0, 0, 0, 0 };
 		int mask = 1;
 		for (int j = 0; j < numbuttons; mask <<= 1, ++j)
 		{
 			if (changed & mask)
 			{
-				ev.data1 = base + j;
-				ev.type = (newbuttons & mask) ? EV_KeyDown : EV_KeyUp;
-				D_PostEvent(&ev);
+				Joy_GenerateButtonEvent(newbuttons & mask, static_cast(base + j));
 			}
 		}
 	}
@@ -349,15 +664,12 @@ void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, co
 	int changed = oldbuttons ^ newbuttons;
 	if (changed != 0)
 	{
-		event_t ev = { 0, 0, 0, 0, 0, 0, 0 };
 		int mask = 1;
 		for (int j = 0; j < numbuttons; mask <<= 1, ++j)
 		{
 			if (changed & mask)
 			{
-				ev.data1 = keys[j];
-				ev.type = (newbuttons & mask) ? EV_KeyDown : EV_KeyUp;
-				D_PostEvent(&ev);
+				Joy_GenerateButtonEvent(newbuttons & mask, static_cast(keys[j]));
 			}
 		}
 	}
diff --git a/src/common/engine/m_joy.h b/src/common/engine/m_joy.h
index e8d9d3b13..64be2e164 100644
--- a/src/common/engine/m_joy.h
+++ b/src/common/engine/m_joy.h
@@ -2,9 +2,30 @@
 #define M_JOY_H
 
 #include "basics.h"
+#include "keydef.h"
 #include "tarray.h"
 #include "c_cvars.h"
 
+union CubicBezier {
+	struct {
+		float x1;
+		float y1;
+		float x2;
+		float y2;
+	};
+	float pts[4];
+};
+
+enum EJoyCurve {
+	JOYCURVE_CUSTOM = -1,
+	JOYCURVE_DEFAULT,
+	JOYCURVE_LINEAR,
+	JOYCURVE_QUADRATIC,
+	JOYCURVE_CUBIC,
+
+	NUM_JOYCURVE
+};
+
 enum EJoyAxis
 {
 	JOYAXIS_None = -1,
@@ -13,10 +34,22 @@ enum EJoyAxis
 	JOYAXIS_Forward,
 	JOYAXIS_Side,
 	JOYAXIS_Up,
-//	JOYAXIS_Roll,		// Ha ha. No roll for you.
+	//	JOYAXIS_Roll,		// Ha ha. No roll for you.
 	NUM_JOYAXIS,
 };
 
+extern const float JOYDEADZONE_DEFAULT;
+
+extern const float JOYSENSITIVITY_DEFAULT;
+
+extern const float JOYTHRESH_DEFAULT;
+
+extern const float JOYTHRESH_TRIGGER;
+extern const float JOYTHRESH_STICK_X;
+extern const float JOYTHRESH_STICK_Y;
+
+extern const CubicBezier JOYCURVE[NUM_JOYCURVE];
+
 // Generic configuration interface for a controller.
 struct IJoystickConfig
 {
@@ -31,10 +64,16 @@ struct IJoystickConfig
 	virtual EJoyAxis GetAxisMap(int axis) = 0;
 	virtual const char *GetAxisName(int axis) = 0;
 	virtual float GetAxisScale(int axis) = 0;
+	virtual float GetAxisDigitalThreshold(int axis) = 0;
+	virtual EJoyCurve GetAxisResponseCurve(int axis) = 0;
+	virtual float GetAxisResponseCurvePoint(int axis, int point) = 0;
 
 	virtual void SetAxisDeadZone(int axis, float zone) = 0;
 	virtual void SetAxisMap(int axis, EJoyAxis gameaxis) = 0;
 	virtual void SetAxisScale(int axis, float scale) = 0;
+	virtual void SetAxisDigitalThreshold(int axis, float threshold) = 0;
+	virtual void SetAxisResponseCurve(int axis, EJoyCurve preset) = 0;
+	virtual void SetAxisResponseCurvePoint(int axis, int point, float value) = 0;
 
 	virtual bool GetEnabled() = 0;
 	virtual void SetEnabled(bool enabled) = 0;
@@ -48,6 +87,8 @@ struct IJoystickConfig
 	virtual bool IsAxisDeadZoneDefault(int axis) = 0;
 	virtual bool IsAxisMapDefault(int axis) = 0;
 	virtual bool IsAxisScaleDefault(int axis) = 0;
+	virtual bool IsAxisDigitalThresholdDefault(int axis) = 0;
+	virtual bool IsAxisResponseCurveDefault(int axis) = 0;
 
 	virtual void SetDefaultConfig() = 0;
 	virtual FString GetIdentifier() = 0;
@@ -58,10 +99,12 @@ EXTERN_CVAR(Bool, use_joystick);
 bool M_LoadJoystickConfig(IJoystickConfig *joy);
 void M_SaveJoystickConfig(IJoystickConfig *joy);
 
+void Joy_GenerateButtonEvent(bool down, EKeyCodes which);
 void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, int base);
 void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, const int *keys);
 int Joy_XYAxesToButtons(double x, double y);
 double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons);
+double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input);
 
 // These ought to be provided by a system-specific i_input.cpp.
 void I_GetAxes(float axes[NUM_JOYAXIS]);
diff --git a/src/common/menu/joystickmenu.cpp b/src/common/menu/joystickmenu.cpp
index 3d3e881b1..ad86dd985 100644
--- a/src/common/menu/joystickmenu.cpp
+++ b/src/common/menu/joystickmenu.cpp
@@ -84,6 +84,56 @@ DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisDeadZone)
 	return 0;
 }
 
+DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisDigitalThreshold)
+{
+	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
+	PARAM_INT(axis);
+	ACTION_RETURN_FLOAT(self->GetAxisDigitalThreshold(axis));
+}
+
+DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisDigitalThreshold)
+{
+	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
+	PARAM_INT(axis);
+	PARAM_FLOAT(dt);
+	self->SetAxisDigitalThreshold(axis, (float)dt);
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisResponseCurve)
+{
+	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
+	PARAM_INT(axis);
+	ACTION_RETURN_INT(self->GetAxisResponseCurve(axis));
+}
+
+DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisResponseCurve)
+{
+	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
+	PARAM_INT(axis);
+	PARAM_INT(curve);
+	self->SetAxisResponseCurve(axis, (EJoyCurve)curve);
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisResponseCurvePoint)
+{
+	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
+	PARAM_INT(axis);
+	PARAM_INT(point);
+	ACTION_RETURN_INT(self->GetAxisResponseCurvePoint(axis, point));
+}
+
+DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisResponseCurvePoint)
+{
+	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
+	PARAM_INT(axis);
+	PARAM_INT(point);
+	PARAM_FLOAT(value);
+	self->SetAxisResponseCurvePoint(axis, point, value);
+	return 0;
+}
+
 DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisMap)
 {
 	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
diff --git a/src/common/menu/menu.cpp b/src/common/menu/menu.cpp
index a07dfd36f..6fbe1dd1b 100644
--- a/src/common/menu/menu.cpp
+++ b/src/common/menu/menu.cpp
@@ -680,7 +680,7 @@ bool M_Responder (event_t *ev)
 		else if (menuactive != MENU_WaitKey && (ev->type == EV_KeyDown || ev->type == EV_KeyUp))
 		{
 			// eat blocked controller events without dispatching them.
-			if (ev->data1 >= KEY_FIRSTJOYBUTTON && m_blockcontrollers) return true;
+			if (ev->data1 >= KEY_FIRSTJOYBUTTON && m_blockcontrollers && ev->type == EV_KeyDown) return true;
 
 			keyup = ev->type == EV_KeyUp;
 
diff --git a/src/common/platform/posix/cocoa/i_joystick.cpp b/src/common/platform/posix/cocoa/i_joystick.cpp
index 44d1fb961..ffb9f90a0 100644
--- a/src/common/platform/posix/cocoa/i_joystick.cpp
+++ b/src/common/platform/posix/cocoa/i_joystick.cpp
@@ -94,15 +94,23 @@ public:
 	virtual EJoyAxis GetAxisMap(int axis);
 	virtual const char* GetAxisName(int axis);
 	virtual float GetAxisScale(int axis);
+	float GetAxisDigitalThreshold(int axis);
+	EJoyCurve GetAxisResponseCurve(int axis);
+	float GetAxisResponseCurvePoint(int axis, int point);
 
 	virtual void SetAxisDeadZone(int axis, float deadZone);
 	virtual void SetAxisMap(int axis, EJoyAxis gameAxis);
 	virtual void SetAxisScale(int axis, float scale);
+	void SetAxisDigitalThreshold(int axis, float threshold);
+	void SetAxisResponseCurve(int axis, EJoyCurve preset);
+	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	virtual bool IsSensitivityDefault();
 	virtual bool IsAxisDeadZoneDefault(int axis);
 	virtual bool IsAxisMapDefault(int axis);
 	virtual bool IsAxisScaleDefault(int axis);
+	bool IsAxisDigitalThresholdDefault(int axis);
+	bool IsAxisResponseCurveDefault(int axis);
 
 	virtual bool GetEnabled();
 	virtual void SetEnabled(bool enabled);
@@ -146,6 +154,11 @@ private:
 		float defaultDeadZone;
 		float sensitivity;
 		float defaultSensitivity;
+		float digitalThreshold;
+		float defaultDigitalThreshold;
+		EJoyCurve responseCurvePreset;
+		EJoyCurve defaultResponseCurvePreset;
+		CubicBezier responseCurve;
 
 		EJoyAxis gameAxis;
 		EJoyAxis defaultGameAxis;
@@ -177,10 +190,6 @@ private:
 
 	io_object_t m_notification;
 
-
-	static const float DEFAULT_DEADZONE;
-	static const float DEFAULT_SENSITIVITY;
-
 	void ProcessAxes();
 	bool ProcessAxis  (const IOHIDEventStruct& event);
 	bool ProcessButton(const IOHIDEventStruct& event);
@@ -200,10 +209,6 @@ private:
 };
 
 
-const float IOKitJoystick::DEFAULT_DEADZONE    = 0.25f;
-const float IOKitJoystick::DEFAULT_SENSITIVITY = 1.0f;
-
-
 IOHIDDeviceInterface** CreateDeviceInterface(const io_object_t device)
 {
 	IOCFPlugInInterface** plugInInterface = NULL;
@@ -286,7 +291,7 @@ IOHIDQueueInterface** CreateDeviceQueue(IOHIDDeviceInterface** const interface)
 IOKitJoystick::IOKitJoystick(const io_object_t device)
 : m_interface(CreateDeviceInterface(device))
 , m_queue(CreateDeviceQueue(m_interface))
-, m_sensitivity(DEFAULT_SENSITIVITY)
+, m_sensitivity(JOYSENSITIVITY_DEFAULT)
 , m_enabled(true)
 , m_useAxesPolling(true)
 , m_notification(0)
@@ -386,6 +391,21 @@ float IOKitJoystick::GetAxisScale(int axis)
 	return IS_AXIS_VALID ? m_axes[axis].sensitivity : 0.0f;
 }
 
+float IOKitJoystick::GetAxisDigitalThreshold(int axis)
+{
+	return IS_AXIS_VALID ? m_axes[axis].digitalThreshold : JOYTHRESH_DEFAULT;
+}
+
+EJoyCurve IOKitJoystick::GetAxisResponseCurve(int axis)
+{
+	return IS_AXIS_VALID ? m_axes[axis].responseCurvePreset : JOYCURVE_DEFAULT;
+}
+
+float IOKitJoystick::GetAxisResponseCurvePoint(int axis, int point)
+{
+	return ( IS_AXIS_VALID && unsigned(point) < 4 )? m_axes[axis].responseCurve.pts[point] : 0;
+}
+
 void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone)
 {
 	if (IS_AXIS_VALID)
@@ -412,10 +432,37 @@ void IOKitJoystick::SetAxisScale(int axis, float scale)
 	}
 }
 
+void IOKitJoystick::SetAxisDigitalThreshold(int axis, float threshold)
+{
+	if (IS_AXIS_VALID)
+	{
+		m_axes[axis].digitalThreshold = threshold;
+	}
+}
+
+void IOKitJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset)
+{
+	if (IS_AXIS_VALID)
+	{
+		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
+		m_axes[axis].responseCurvePreset = preset;
+		if (preset == JOYCURVE_CUSTOM) return;
+		m_axes[axis].responseCurve = JOYCURVE[preset];
+	}
+}
+
+void IOKitJoystick::SetAxisResponseCurvePoint(int axis, int point, float value)
+{
+	if (IS_AXIS_VALID && unsigned(point) < 4)
+	{
+		m_axes[axis].responseCurvePreset = JOYCURVE_CUSTOM;
+		m_axes[axis].responseCurve.pts[point] = value;
+	}
+}
 
 bool IOKitJoystick::IsSensitivityDefault()
 {
-	return DEFAULT_SENSITIVITY == m_sensitivity;
+	return JOYSENSITIVITY_DEFAULT == m_sensitivity;
 }
 
 bool IOKitJoystick::IsAxisDeadZoneDefault(int axis)
@@ -439,7 +486,19 @@ bool IOKitJoystick::IsAxisScaleDefault(int axis)
 		: true;
 }
 
+bool IOKitJoystick::IsAxisDigitalThresholdDefault(int axis)
+{
+	return IS_AXIS_VALID
+		? (m_axes[axis].digitalThreshold == m_axes[axis].defaultDigitalThreshold)
+		: true;
+}
 
+bool IOKitJoystick::IsAxisResponseCurveDefault(int axis)
+{
+	return IS_AXIS_VALID
+		? m_axes[axis].responseCurvePreset == m_axes[axis].defaultResponseCurvePreset
+		: true;
+}
 
 bool IOKitJoystick::GetEnabled()
 {
@@ -454,15 +513,18 @@ void IOKitJoystick::SetEnabled(bool enabled)
 
 void IOKitJoystick::SetDefaultConfig()
 {
-	m_sensitivity = DEFAULT_SENSITIVITY;
+	m_sensitivity = JOYSENSITIVITY_DEFAULT;
 
 	const size_t axisCount = m_axes.Size();
 
 	for (size_t i = 0; i < axisCount; ++i)
 	{
-		m_axes[i].deadZone    = DEFAULT_DEADZONE;
-		m_axes[i].sensitivity = DEFAULT_SENSITIVITY;
+		m_axes[i].deadZone    = JOYDEADZONE_DEFAULT;
+		m_axes[i].sensitivity = JOYSENSITIVITY_DEFAULT;
 		m_axes[i].gameAxis    = JOYAXIS_None;
+		m_axes[i].digitalThreshold = JOYTHRESH_DEFAULT;
+		m_axes[i].responseCurvePreset = JOYCURVE_DEFAULT;
+		m_axes[i].responseCurve = JOYCURVE[JOYCURVE_DEFAULT];
 	}
 
 	// Two axes? Horizontal is yaw and vertical is forward.
@@ -470,7 +532,10 @@ void IOKitJoystick::SetDefaultConfig()
 	if (2 == axisCount)
 	{
 		m_axes[0].gameAxis = JOYAXIS_Yaw;
+		m_axes[0].digitalThreshold = JOYTHRESH_STICK_X;
+
 		m_axes[1].gameAxis = JOYAXIS_Forward;
+		m_axes[1].digitalThreshold = JOYTHRESH_STICK_Y;
 	}
 
 	// Three axes? First two are movement, third is yaw.
@@ -478,8 +543,13 @@ void IOKitJoystick::SetDefaultConfig()
 	else if (axisCount >= 3)
 	{
 		m_axes[0].gameAxis = JOYAXIS_Side;
+		m_axes[0].digitalThreshold = JOYTHRESH_STICK_X;
+
 		m_axes[1].gameAxis = JOYAXIS_Forward;
+		m_axes[1].digitalThreshold = JOYTHRESH_STICK_Y;
+
 		m_axes[2].gameAxis = JOYAXIS_Yaw;
+		m_axes[2].digitalThreshold = JOYTHRESH_STICK_X;
 
 		// Four axes? First two are movement, last two are looking around.
 
@@ -487,12 +557,14 @@ void IOKitJoystick::SetDefaultConfig()
 		{
 			m_axes[3].gameAxis = JOYAXIS_Pitch;
 //	???		m_axes[3].sensitivity = 0.75f;
+			m_axes[3].digitalThreshold = JOYTHRESH_STICK_Y;
 
 			// Five axes? Use the fifth one for moving up and down.
 
 			if (axisCount >= 5)
 			{
 				m_axes[4].gameAxis = JOYAXIS_Up;
+				m_axes[4].digitalThreshold = JOYTHRESH_STICK_Y;
 			}
 		}
 	}
@@ -504,9 +576,11 @@ void IOKitJoystick::SetDefaultConfig()
 
 	for (size_t i = 0; i < axisCount; ++i)
 	{
-		m_axes[i].defaultDeadZone    = m_axes[i].deadZone;
-		m_axes[i].defaultSensitivity = m_axes[i].sensitivity;
-		m_axes[i].defaultGameAxis    = m_axes[i].gameAxis;
+		m_axes[i].defaultDeadZone            = m_axes[i].deadZone;
+		m_axes[i].defaultSensitivity         = m_axes[i].sensitivity;
+		m_axes[i].defaultGameAxis            = m_axes[i].gameAxis;
+		m_axes[i].defaultDigitalThreshold    = m_axes[i].digitalThreshold;
+		m_axes[i].defaultResponseCurvePreset = m_axes[i].responseCurvePreset;
 	}
 }
 
@@ -602,8 +676,9 @@ void IOKitJoystick::ProcessAxes()
 			const double scaledValue = scaledMin +
 				(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
 			const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);
+			const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue);
 
-			axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity);
+			axis.value = static_cast(smoothedValue * m_sensitivity * axis.sensitivity);
 		}
 		else
 		{
@@ -635,8 +710,9 @@ bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event)
 		const double scaledValue = scaledMin +
 			(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
 		const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);
+		const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue);
 
-		axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity);
+		axis.value = static_cast(smoothedValue * m_sensitivity * axis.sensitivity);
 
 		return true;
 	}
diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp
index 0148eaad2..da7ac59c2 100644
--- a/src/common/platform/posix/sdl/i_input.cpp
+++ b/src/common/platform/posix/sdl/i_input.cpp
@@ -31,11 +31,14 @@
 **
 */
 #include 
+#include 
+#include "i_input.h"
+#include "c_cvars.h"
+#include "dobject.h"
 #include "m_argv.h"
 #include "m_joy.h"
 #include "v_video.h"
 
-#include "d_eventbase.h"
 #include "d_gui.h"
 #include "c_buttons.h"
 #include "c_console.h"
@@ -47,88 +50,147 @@
 #include "engineerrors.h"
 #include "i_interface.h"
 
-
-static void I_CheckGUICapture ();
-static void I_CheckNativeMouse ();
-
 bool GUICapture;
 static bool NativeMouse = true;
 
 CVAR (Bool,  use_mouse,				true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 
-
 extern int WaitingForKey;
 
 static const SDL_Keycode DIKToKeySym[256] =
 {
-	0, SDLK_ESCAPE, SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6,
-	SDLK_7, SDLK_8, SDLK_9, SDLK_0,SDLK_MINUS, SDLK_EQUALS, SDLK_BACKSPACE, SDLK_TAB,
-	SDLK_q, SDLK_w, SDLK_e, SDLK_r, SDLK_t, SDLK_y, SDLK_u, SDLK_i,
-	SDLK_o, SDLK_p, SDLK_LEFTBRACKET, SDLK_RIGHTBRACKET, SDLK_RETURN, SDLK_LCTRL, SDLK_a, SDLK_s,
-	SDLK_d, SDLK_f, SDLK_g, SDLK_h, SDLK_j, SDLK_k, SDLK_l, SDLK_SEMICOLON,
-	SDLK_QUOTE, SDLK_BACKQUOTE, SDLK_LSHIFT, SDLK_BACKSLASH, SDLK_z, SDLK_x, SDLK_c, SDLK_v,
-	SDLK_b, SDLK_n, SDLK_m, SDLK_COMMA, SDLK_PERIOD, SDLK_SLASH, SDLK_RSHIFT, SDLK_KP_MULTIPLY,
-	SDLK_LALT, SDLK_SPACE, SDLK_CAPSLOCK, SDLK_F1, SDLK_F2, SDLK_F3, SDLK_F4, SDLK_F5,
-	SDLK_F6, SDLK_F7, SDLK_F8, SDLK_F9, SDLK_F10, SDLK_NUMLOCKCLEAR, SDLK_SCROLLLOCK, SDLK_KP_7,
-	SDLK_KP_8, SDLK_KP_9, SDLK_KP_MINUS, SDLK_KP_4, SDLK_KP_5, SDLK_KP_6, SDLK_KP_PLUS, SDLK_KP_1,
-	SDLK_KP_2, SDLK_KP_3, SDLK_KP_0, SDLK_KP_PERIOD, 0, 0, 0, SDLK_F11,
-	SDLK_F12, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, SDLK_F13, SDLK_F14, SDLK_F15, SDLK_F16,
-	SDLK_F17, SDLK_F18, SDLK_F19, SDLK_F20, SDLK_F21, SDLK_F22, SDLK_F23, SDLK_F24,
-	0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, SDLK_KP_EQUALS, 0, 0,
-	0, SDLK_AT, SDLK_COLON, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, SDLK_KP_ENTER, SDLK_RCTRL, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, SDLK_KP_COMMA, 0, SDLK_KP_DIVIDE, 0, SDLK_SYSREQ,
-	SDLK_RALT, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, SDLK_PAUSE, 0, SDLK_HOME,
-	SDLK_UP, SDLK_PAGEUP, 0, SDLK_LEFT, 0, SDLK_RIGHT, 0, SDLK_END,
-	SDLK_DOWN, SDLK_PAGEDOWN, SDLK_INSERT, SDLK_DELETE, 0, 0, 0, 0,
-	0, 0, 0, SDLK_LGUI, SDLK_RGUI, SDLK_MENU, SDLK_POWER, SDLK_SLEEP,
-	0, 0, 0, 0, 0, SDLK_AC_SEARCH, SDLK_AC_BOOKMARKS, SDLK_AC_REFRESH,
-	SDLK_AC_STOP, SDLK_AC_FORWARD, SDLK_AC_BACK, SDLK_COMPUTER, SDLK_MAIL, SDLK_MEDIASELECT, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0
+	SDLK_UNKNOWN,  SDLK_ESCAPE,       SDLK_1,            SDLK_2,
+	SDLK_3,        SDLK_4,            SDLK_5,            SDLK_6,
+	SDLK_7,        SDLK_8,            SDLK_9,            SDLK_0,
+	SDLK_MINUS,    SDLK_EQUALS,       SDLK_BACKSPACE,    SDLK_TAB,
+	SDLK_q,        SDLK_w,            SDLK_e,            SDLK_r,
+	SDLK_t,        SDLK_y,            SDLK_u,            SDLK_i,
+	SDLK_o,        SDLK_p,            SDLK_LEFTBRACKET,  SDLK_RIGHTBRACKET,
+	SDLK_RETURN,   SDLK_LCTRL,        SDLK_a,            SDLK_s,
+	SDLK_d,        SDLK_f,            SDLK_g,            SDLK_h,
+	SDLK_j,        SDLK_k,            SDLK_l,            SDLK_SEMICOLON,
+	SDLK_QUOTE,    SDLK_BACKQUOTE,    SDLK_LSHIFT,       SDLK_BACKSLASH,
+	SDLK_z,        SDLK_x,            SDLK_c,            SDLK_v,
+	SDLK_b,        SDLK_n,            SDLK_m,            SDLK_COMMA,
+	SDLK_PERIOD,   SDLK_SLASH,        SDLK_RSHIFT,       SDLK_KP_MULTIPLY,
+	SDLK_LALT,     SDLK_SPACE,        SDLK_CAPSLOCK,     SDLK_F1,
+	SDLK_F2,       SDLK_F3,           SDLK_F4,           SDLK_F5,
+	SDLK_F6,       SDLK_F7,           SDLK_F8,           SDLK_F9,
+	SDLK_F10,      SDLK_NUMLOCKCLEAR, SDLK_SCROLLLOCK,   SDLK_KP_7,
+	SDLK_KP_8,     SDLK_KP_9,         SDLK_KP_MINUS,     SDLK_KP_4,
+	SDLK_KP_5,     SDLK_KP_6,         SDLK_KP_PLUS,      SDLK_KP_1,
+	SDLK_KP_2,     SDLK_KP_3,         SDLK_KP_0,         SDLK_KP_PERIOD,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_F11,
+	SDLK_F12,      SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_F13,      SDLK_F14,          SDLK_F15,          SDLK_F16,
+	SDLK_F17,      SDLK_F18,          SDLK_F19,          SDLK_F20,
+	SDLK_F21,      SDLK_F22,          SDLK_F23,          SDLK_F24,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_KP_EQUALS,    SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_AT,           SDLK_COLON,        SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_KP_ENTER, SDLK_RCTRL,        SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_KP_COMMA,
+	SDLK_UNKNOWN,  SDLK_KP_DIVIDE,    SDLK_UNKNOWN,      SDLK_SYSREQ,
+	SDLK_RALT,     SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_PAUSE,        SDLK_UNKNOWN,      SDLK_HOME,
+	SDLK_UP,       SDLK_PAGEUP,       SDLK_UNKNOWN,      SDLK_LEFT,
+	SDLK_UNKNOWN,  SDLK_RIGHT,        SDLK_UNKNOWN,      SDLK_END,
+	SDLK_DOWN,     SDLK_PAGEDOWN,     SDLK_INSERT,       SDLK_DELETE,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_LGUI,
+	SDLK_RGUI,     SDLK_MENU,         SDLK_POWER,        SDLK_SLEEP,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_AC_SEARCH,    SDLK_AC_BOOKMARKS, SDLK_AC_REFRESH,
+	SDLK_AC_STOP,  SDLK_AC_FORWARD,   SDLK_AC_BACK,      SDLK_COMPUTER,
+	SDLK_MAIL,     SDLK_MEDIASELECT,  SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
+	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN
 };
 
 static const SDL_Scancode DIKToKeyScan[256] =
 {
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_ESCAPE, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, SDL_SCANCODE_5, SDL_SCANCODE_6,
-	SDL_SCANCODE_7, SDL_SCANCODE_8, SDL_SCANCODE_9, SDL_SCANCODE_0 ,SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS, SDL_SCANCODE_BACKSPACE, SDL_SCANCODE_TAB,
-	SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_E, SDL_SCANCODE_R, SDL_SCANCODE_T, SDL_SCANCODE_Y, SDL_SCANCODE_U, SDL_SCANCODE_I,
-	SDL_SCANCODE_O, SDL_SCANCODE_P, SDL_SCANCODE_LEFTBRACKET, SDL_SCANCODE_RIGHTBRACKET, SDL_SCANCODE_RETURN, SDL_SCANCODE_LCTRL, SDL_SCANCODE_A, SDL_SCANCODE_S,
-	SDL_SCANCODE_D, SDL_SCANCODE_F, SDL_SCANCODE_G, SDL_SCANCODE_H, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L, SDL_SCANCODE_SEMICOLON,
-	SDL_SCANCODE_APOSTROPHE, SDL_SCANCODE_GRAVE, SDL_SCANCODE_LSHIFT, SDL_SCANCODE_BACKSLASH, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_C, SDL_SCANCODE_V,
-	SDL_SCANCODE_B, SDL_SCANCODE_N, SDL_SCANCODE_M, SDL_SCANCODE_COMMA, SDL_SCANCODE_PERIOD, SDL_SCANCODE_SLASH, SDL_SCANCODE_RSHIFT, SDL_SCANCODE_KP_MULTIPLY,
-	SDL_SCANCODE_LALT, SDL_SCANCODE_SPACE, SDL_SCANCODE_CAPSLOCK, SDL_SCANCODE_F1, SDL_SCANCODE_F2, SDL_SCANCODE_F3, SDL_SCANCODE_F4, SDL_SCANCODE_F5,
-	SDL_SCANCODE_F6, SDL_SCANCODE_F7, SDL_SCANCODE_F8, SDL_SCANCODE_F9, SDL_SCANCODE_F10, SDL_SCANCODE_NUMLOCKCLEAR, SDL_SCANCODE_SCROLLLOCK, SDL_SCANCODE_KP_7,
-	SDL_SCANCODE_KP_8, SDL_SCANCODE_KP_9, SDL_SCANCODE_KP_MINUS, SDL_SCANCODE_KP_4, SDL_SCANCODE_KP_5, SDL_SCANCODE_KP_6, SDL_SCANCODE_KP_PLUS, SDL_SCANCODE_KP_1,
-	SDL_SCANCODE_KP_2, SDL_SCANCODE_KP_3, SDL_SCANCODE_KP_0, SDL_SCANCODE_KP_PERIOD, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F11,
-	SDL_SCANCODE_F12, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F13, SDL_SCANCODE_F14, SDL_SCANCODE_F15, SDL_SCANCODE_F16,
-	SDL_SCANCODE_F17, SDL_SCANCODE_F18, SDL_SCANCODE_F19, SDL_SCANCODE_F20, SDL_SCANCODE_F21, SDL_SCANCODE_F22, SDL_SCANCODE_F23, SDL_SCANCODE_F24,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_EQUALS, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_ENTER, SDL_SCANCODE_RCTRL, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_COMMA, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_DIVIDE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_SYSREQ,
-	SDL_SCANCODE_RALT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_PAUSE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_HOME,
-	SDL_SCANCODE_UP, SDL_SCANCODE_PAGEUP, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_END,
-	SDL_SCANCODE_DOWN, SDL_SCANCODE_PAGEDOWN, SDL_SCANCODE_INSERT, SDL_SCANCODE_DELETE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LGUI, SDL_SCANCODE_RGUI, SDL_SCANCODE_MENU, SDL_SCANCODE_POWER, SDL_SCANCODE_SLEEP,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_AC_SEARCH, SDL_SCANCODE_AC_BOOKMARKS, SDL_SCANCODE_AC_REFRESH,
-	SDL_SCANCODE_AC_STOP, SDL_SCANCODE_AC_FORWARD, SDL_SCANCODE_AC_BACK, SDL_SCANCODE_COMPUTER, SDL_SCANCODE_MAIL, SDL_SCANCODE_MEDIASELECT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_ESCAPE,       SDL_SCANCODE_1,            SDL_SCANCODE_2,
+	SDL_SCANCODE_3,          SDL_SCANCODE_4,            SDL_SCANCODE_5,            SDL_SCANCODE_6,
+	SDL_SCANCODE_7,          SDL_SCANCODE_8,            SDL_SCANCODE_9,            SDL_SCANCODE_0,
+	SDL_SCANCODE_MINUS,      SDL_SCANCODE_EQUALS,       SDL_SCANCODE_BACKSPACE,    SDL_SCANCODE_TAB,
+	SDL_SCANCODE_Q,          SDL_SCANCODE_W,            SDL_SCANCODE_E,            SDL_SCANCODE_R,
+	SDL_SCANCODE_T,          SDL_SCANCODE_Y,            SDL_SCANCODE_U,            SDL_SCANCODE_I,
+	SDL_SCANCODE_O,          SDL_SCANCODE_P,            SDL_SCANCODE_LEFTBRACKET,  SDL_SCANCODE_RIGHTBRACKET,
+	SDL_SCANCODE_RETURN,     SDL_SCANCODE_LCTRL,        SDL_SCANCODE_A,            SDL_SCANCODE_S,
+	SDL_SCANCODE_D,          SDL_SCANCODE_F,            SDL_SCANCODE_G,            SDL_SCANCODE_H,
+	SDL_SCANCODE_J,          SDL_SCANCODE_K,            SDL_SCANCODE_L,            SDL_SCANCODE_SEMICOLON,
+	SDL_SCANCODE_APOSTROPHE, SDL_SCANCODE_GRAVE,        SDL_SCANCODE_LSHIFT,       SDL_SCANCODE_BACKSLASH,
+	SDL_SCANCODE_Z,          SDL_SCANCODE_X,            SDL_SCANCODE_C,            SDL_SCANCODE_V,
+	SDL_SCANCODE_B,          SDL_SCANCODE_N,            SDL_SCANCODE_M,            SDL_SCANCODE_COMMA,
+	SDL_SCANCODE_PERIOD,     SDL_SCANCODE_SLASH,        SDL_SCANCODE_RSHIFT,       SDL_SCANCODE_KP_MULTIPLY,
+	SDL_SCANCODE_LALT,       SDL_SCANCODE_SPACE,        SDL_SCANCODE_CAPSLOCK,     SDL_SCANCODE_F1,
+	SDL_SCANCODE_F2,         SDL_SCANCODE_F3,           SDL_SCANCODE_F4,           SDL_SCANCODE_F5,
+	SDL_SCANCODE_F6,         SDL_SCANCODE_F7,           SDL_SCANCODE_F8,           SDL_SCANCODE_F9,
+	SDL_SCANCODE_F10,        SDL_SCANCODE_NUMLOCKCLEAR, SDL_SCANCODE_SCROLLLOCK,   SDL_SCANCODE_KP_7,
+	SDL_SCANCODE_KP_8,       SDL_SCANCODE_KP_9,         SDL_SCANCODE_KP_MINUS,     SDL_SCANCODE_KP_4,
+	SDL_SCANCODE_KP_5,       SDL_SCANCODE_KP_6,         SDL_SCANCODE_KP_PLUS,      SDL_SCANCODE_KP_1,
+	SDL_SCANCODE_KP_2,       SDL_SCANCODE_KP_3,         SDL_SCANCODE_KP_0,         SDL_SCANCODE_KP_PERIOD,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_F11,
+	SDL_SCANCODE_F12,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_F13,        SDL_SCANCODE_F14,          SDL_SCANCODE_F15,          SDL_SCANCODE_F16,
+	SDL_SCANCODE_F17,        SDL_SCANCODE_F18,          SDL_SCANCODE_F19,          SDL_SCANCODE_F20,
+	SDL_SCANCODE_F21,        SDL_SCANCODE_F22,          SDL_SCANCODE_F23,          SDL_SCANCODE_F24,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_KP_EQUALS,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_KP_ENTER,   SDL_SCANCODE_RCTRL,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_KP_COMMA,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_KP_DIVIDE,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_SYSREQ,
+	SDL_SCANCODE_RALT,       SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_PAUSE,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_HOME,
+	SDL_SCANCODE_UP,         SDL_SCANCODE_PAGEUP,       SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_LEFT,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_RIGHT,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_END,
+	SDL_SCANCODE_DOWN,       SDL_SCANCODE_PAGEDOWN,     SDL_SCANCODE_INSERT,       SDL_SCANCODE_DELETE,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_LGUI,
+	SDL_SCANCODE_RGUI,       SDL_SCANCODE_MENU,         SDL_SCANCODE_POWER,        SDL_SCANCODE_SLEEP,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_AC_SEARCH,    SDL_SCANCODE_AC_BOOKMARKS, SDL_SCANCODE_AC_REFRESH,
+	SDL_SCANCODE_AC_STOP,    SDL_SCANCODE_AC_FORWARD,   SDL_SCANCODE_AC_BACK,      SDL_SCANCODE_COMPUTER,
+	SDL_SCANCODE_MAIL,       SDL_SCANCODE_MEDIASELECT,  SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN
 };
 
 static TMap InitKeySymMap ()
@@ -463,14 +525,60 @@ void MessagePump (const SDL_Event &sev)
 
 	case SDL_JOYBUTTONDOWN:
 	case SDL_JOYBUTTONUP:
+		if (SDL_GameControllerFromInstanceID(sev.jdevice.which))
+			break; // let SDL_CONTROLLERBUTTON* handle this
 		event.type = sev.type == SDL_JOYBUTTONDOWN ? EV_KeyDown : EV_KeyUp;
 		event.data1 = KEY_FIRSTJOYBUTTON + sev.jbutton.button;
 		if(event.data1 != 0)
-			D_PostEvent(&event);
+			I_JoyConsumeEvent(sev.jdevice.which, &event);
+		break;
+
+	case SDL_CONTROLLERBUTTONDOWN:
+	case SDL_CONTROLLERBUTTONUP:
+		event.type = sev.type == SDL_CONTROLLERBUTTONDOWN ? EV_KeyDown : EV_KeyUp;
+		switch (sev.cbutton.button)
+		{
+			case SDL_CONTROLLER_BUTTON_A:             event.data1 = KEY_PAD_A;          break;
+			case SDL_CONTROLLER_BUTTON_B:             event.data1 = KEY_PAD_B;          break;
+			case SDL_CONTROLLER_BUTTON_X:             event.data1 = KEY_PAD_X;          break;
+			case SDL_CONTROLLER_BUTTON_Y:             event.data1 = KEY_PAD_Y;          break;
+			case SDL_CONTROLLER_BUTTON_BACK:          event.data1 = KEY_PAD_BACK;       break;
+			case SDL_CONTROLLER_BUTTON_GUIDE:         event.data1 = KEY_PAD_GUIDE;      break;
+			case SDL_CONTROLLER_BUTTON_START:         event.data1 = KEY_PAD_START;      break;
+			case SDL_CONTROLLER_BUTTON_LEFTSTICK:     event.data1 = KEY_PAD_LTHUMB;     break;
+			case SDL_CONTROLLER_BUTTON_RIGHTSTICK:    event.data1 = KEY_PAD_RTHUMB;     break;
+			case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:  event.data1 = KEY_PAD_LSHOULDER;  break;
+			case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: event.data1 = KEY_PAD_RSHOULDER;  break;
+			case SDL_CONTROLLER_BUTTON_DPAD_UP:       event.data1 = KEY_PAD_DPAD_UP;    break;
+			case SDL_CONTROLLER_BUTTON_DPAD_DOWN:     event.data1 = KEY_PAD_DPAD_DOWN;  break;
+			case SDL_CONTROLLER_BUTTON_DPAD_LEFT:     event.data1 = KEY_PAD_DPAD_LEFT;  break;
+			case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:    event.data1 = KEY_PAD_DPAD_RIGHT; break;
+			case SDL_CONTROLLER_BUTTON_MISC1:         event.data1 = KEY_PAD_MISC1;      break;
+			case SDL_CONTROLLER_BUTTON_PADDLE1:       event.data1 = KEY_PAD_PADDLE1;    break;
+			case SDL_CONTROLLER_BUTTON_PADDLE2:       event.data1 = KEY_PAD_PADDLE2;    break;
+			case SDL_CONTROLLER_BUTTON_PADDLE3:       event.data1 = KEY_PAD_PADDLE3;    break;
+			case SDL_CONTROLLER_BUTTON_PADDLE4:       event.data1 = KEY_PAD_PADDLE4;    break;
+			case SDL_CONTROLLER_BUTTON_TOUCHPAD:      event.data1 = KEY_PAD_TOUCHPAD;   break;
+			default:                                  event.data1 = 0;
+		}
+		if(event.data1 != 0)
+			I_JoyConsumeEvent(sev.cbutton.which, &event);
 		break;
 
 	case SDL_JOYDEVICEADDED:
+	case SDL_CONTROLLERDEVICEADDED:
+		if (sev.type == SDL_JOYDEVICEADDED && SDL_IsGameController(sev.jdevice.which)) // DeviceIndex Here
+			break; // skip double event
+		I_UpdateDeviceList();
+		event.type = EV_DeviceChange;
+		D_PostEvent (&event);
+		break;
+
 	case SDL_JOYDEVICEREMOVED:
+	case SDL_CONTROLLERDEVICEREMOVED:
+	case SDL_CONTROLLERDEVICEREMAPPED:
+		if (sev.type == SDL_JOYDEVICEREMOVED && SDL_GameControllerFromInstanceID(sev.jdevice.which))
+			break; // skip double event
 		I_UpdateDeviceList();
 		event.type = EV_DeviceChange;
 		D_PostEvent (&event);
diff --git a/src/common/platform/posix/sdl/i_input.h b/src/common/platform/posix/sdl/i_input.h
new file mode 100644
index 000000000..6ca2e05aa
--- /dev/null
+++ b/src/common/platform/posix/sdl/i_input.h
@@ -0,0 +1,13 @@
+#ifndef I_INPUT_H
+#define I_INPUT_H
+
+#include "d_eventbase.h"
+
+extern int WaitingForKey;
+
+static void I_CheckGUICapture ();
+static void I_CheckNativeMouse ();
+
+void I_JoyConsumeEvent(int instanceID, event_t * event);
+
+#endif
diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp
index 8dc9fd8ac..d37b4f3a6 100644
--- a/src/common/platform/posix/sdl/i_joystick.cpp
+++ b/src/common/platform/posix/sdl/i_joystick.cpp
@@ -31,47 +31,80 @@
 **
 */
 #include 
+#include 
+#include 
 
 #include "basics.h"
 #include "cmdlib.h"
 
+#include "d_eventbase.h"
+#include "i_input.h"
 #include "m_joy.h"
-#include "keydef.h"
-
-#define DEFAULT_DEADZONE 0.25f;
-
-// Very small deadzone so that floating point magic doesn't happen
-#define MIN_DEADZONE 0.000001f
 
 class SDLInputJoystick: public IJoystickConfig
 {
 public:
-	SDLInputJoystick(int DeviceIndex) : DeviceIndex(DeviceIndex), Multiplier(1.0f) , Enabled(true)
+	SDLInputJoystick(int DeviceIndex) :
+	DeviceIndex(DeviceIndex),
+	InstanceID(SDL_JoystickGetDeviceInstanceID(DeviceIndex)),
+	Multiplier(JOYSENSITIVITY_DEFAULT),
+	Enabled(true),
+	SettingsChanged(false)
 	{
-		Device = SDL_JoystickOpen(DeviceIndex);
-		if(Device != NULL)
+		if (SDL_IsGameController(DeviceIndex))
 		{
-			NumAxes = SDL_JoystickNumAxes(Device);
-			NumHats = SDL_JoystickNumHats(Device);
+			Mapping = SDL_GameControllerOpen(DeviceIndex);
+			Device = NULL;
 
-			SetDefaultConfig();
+			DefaultAxes = DefaultControllerAxes;
+			DefaultAxesCount = sizeof(DefaultControllerAxes) / sizeof(DefaultAxisConfig);
+
+			if(Mapping != NULL)
+			{
+				NumAxes = SDL_CONTROLLER_AXIS_MAX;
+				NumHats = 0;
+
+				SetDefaultConfig();
+			}
 		}
+		else
+		{
+			Device = SDL_JoystickOpen(DeviceIndex);
+			Mapping = NULL;
+
+			DefaultAxes = DefaultJoystickAxes;
+			DefaultAxesCount = sizeof(DefaultJoystickAxes) / sizeof(DefaultAxisConfig);
+
+			if(Device != NULL)
+			{
+				NumAxes = SDL_JoystickNumAxes(Device);
+				NumHats = SDL_JoystickNumHats(Device);
+
+				SetDefaultConfig();
+			}
+		}
+		M_LoadJoystickConfig(this);
 	}
 	~SDLInputJoystick()
 	{
-		if(Device != NULL)
+		if(IsValid() && SettingsChanged)
 			M_SaveJoystickConfig(this);
-		SDL_JoystickClose(Device);
+		if (Mapping)
+			SDL_GameControllerClose(Mapping);
+		if (Device)
+			SDL_JoystickClose(Device);
 	}
 
 	bool IsValid() const
 	{
-		return Device != NULL;
+		return Device != NULL || Mapping != NULL;
 	}
 
 	FString GetName()
 	{
-		return SDL_JoystickName(Device);
+		return (Mapping)
+			? SDL_GameControllerName(Mapping)
+			: SDL_JoystickName(Device);
 	}
 	float GetSensitivity()
 	{
@@ -79,6 +112,7 @@ public:
 	}
 	void SetSensitivity(float scale)
 	{
+		SettingsChanged = true;
 		Multiplier = scale;
 	}
 
@@ -102,58 +136,145 @@ public:
 	{
 		return Axes[axis].Multiplier;
 	}
+	float GetAxisDigitalThreshold(int axis)
+	{
+		return Axes[axis].DigitalThreshold;
+	}
+	EJoyCurve GetAxisResponseCurve(int axis)
+	{
+		return Axes[axis].ResponseCurvePreset;
+	}
+	float GetAxisResponseCurvePoint(int axis, int point)
+	{
+		return unsigned(point) < 4
+			? Axes[axis].ResponseCurve.pts[point]
+			: 0;
+	};
 
 	void SetAxisDeadZone(int axis, float zone)
 	{
-		Axes[axis].DeadZone = clamp(zone, MIN_DEADZONE, 1.f);
+		SettingsChanged = true;
+		Axes[axis].DeadZone = clamp(zone, 0.f, 1.f);
 	}
 	void SetAxisMap(int axis, EJoyAxis gameaxis)
 	{
+		SettingsChanged = true;
 		Axes[axis].GameAxis = gameaxis;
 	}
 	void SetAxisScale(int axis, float scale)
 	{
+		SettingsChanged = true;
 		Axes[axis].Multiplier = scale;
 	}
+	void SetAxisDigitalThreshold(int axis, float threshold)
+	{
+		SettingsChanged = true;
+		Axes[axis].DigitalThreshold = threshold;
+	}
+	void SetAxisResponseCurve(int axis, EJoyCurve preset)
+	{
+		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
+		SettingsChanged = true;
+		Axes[axis].ResponseCurvePreset = preset;
+		if (preset == JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurve = JOYCURVE[preset];
+	}
+	void SetAxisResponseCurvePoint(int axis, int point, float value)
+	{
+		if (unsigned(point) < 4)
+		{
+			SettingsChanged = true;
+			Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
+			Axes[axis].ResponseCurve.pts[point] = value;
+		}
+	}
 
 	// Used by the saver to not save properties that are at their defaults.
 	bool IsSensitivityDefault()
 	{
-		return Multiplier == 1.0f;
+		return Multiplier == JOYSENSITIVITY_DEFAULT;
 	}
 	bool IsAxisDeadZoneDefault(int axis)
 	{
-		return Axes[axis].DeadZone <= MIN_DEADZONE;
+		if(axis >= DefaultAxesCount)
+			return Axes[axis].DeadZone == JOYDEADZONE_DEFAULT;
+		return Axes[axis].DeadZone == DefaultAxes[axis].DeadZone;
 	}
 	bool IsAxisMapDefault(int axis)
 	{
-		if(axis >= 5)
+		if(axis >= DefaultAxesCount)
 			return Axes[axis].GameAxis == JOYAXIS_None;
-		return Axes[axis].GameAxis == DefaultAxes[axis];
+		return Axes[axis].GameAxis == DefaultAxes[axis].GameAxis;
 	}
 	bool IsAxisScaleDefault(int axis)
 	{
-		return Axes[axis].Multiplier == 1.0f;
+		if(axis >= DefaultAxesCount)
+			return Axes[axis].Multiplier == JOYSENSITIVITY_DEFAULT;
+		return Axes[axis].Multiplier == DefaultAxes[axis].Multiplier;
+	}
+	bool IsAxisDigitalThresholdDefault(int axis)
+	{
+		if(axis >= DefaultAxesCount)
+			return Axes[axis].DigitalThreshold == JOYTHRESH_DEFAULT;
+		return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold;
+	}
+	bool IsAxisResponseCurveDefault(int axis)
+	{
+		if(axis >= DefaultAxesCount)
+			return Axes[axis].ResponseCurvePreset == JOYCURVE_DEFAULT;
+		return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset;
 	}
 
 	void SetDefaultConfig()
 	{
+		if (Axes.size() == 0)
+		{
+			for(int i = 0;i < GetNumAxes();i++)
+			{
+				Axes.Push({});
+			}
+		}
+
 		for(int i = 0;i < GetNumAxes();i++)
 		{
-			AxisInfo info;
-			if(i < NumAxes)
-				info.Name.Format("Axis %d", i+1);
+			if (Mapping) {
+				switch(i) {
+					case SDL_CONTROLLER_AXIS_LEFTX: Axes[i].Name = "Left Stick X"; break;
+					case SDL_CONTROLLER_AXIS_LEFTY: Axes[i].Name = "Left Stick Y"; break;
+					case SDL_CONTROLLER_AXIS_RIGHTX: Axes[i].Name = "Right Stick X"; break;
+					case SDL_CONTROLLER_AXIS_RIGHTY: Axes[i].Name = "Right Stick Y"; break;
+					case SDL_CONTROLLER_AXIS_TRIGGERLEFT: Axes[i].Name = "Left Trigger"; break;
+					case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: Axes[i].Name = "Right Trigger"; break;
+					default: Axes[i].Name.Format("Axis %d", i+1); break;
+				}
+			} else {
+				if(i < NumAxes)
+					Axes[i].Name.Format("Axis %d", i+1);
+				else
+					Axes[i].Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y');
+			}
+
+			Axes[i].Value = 0.0;
+			Axes[i].ButtonValue = 0;
+
+			if (i < DefaultAxesCount)
+			{
+				Axes[i].GameAxis = DefaultAxes[i].GameAxis;
+				Axes[i].DeadZone = DefaultAxes[i].DeadZone;
+				Axes[i].Multiplier = DefaultAxes[i].Multiplier;
+				Axes[i].DigitalThreshold = DefaultAxes[i].DigitalThreshold;
+				Axes[i].ResponseCurvePreset = DefaultAxes[i].ResponseCurvePreset;
+				Axes[i].ResponseCurve = JOYCURVE[DefaultAxes[i].ResponseCurvePreset];
+			}
 			else
-				info.Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y');
-			info.DeadZone = DEFAULT_DEADZONE;
-			info.Multiplier = 1.0f;
-			info.Value = 0.0;
-			info.ButtonValue = 0;
-			if(i >= 5)
-				info.GameAxis = JOYAXIS_None;
-			else
-				info.GameAxis = DefaultAxes[i];
-			Axes.Push(info);
+			{
+				Axes[i].GameAxis = JOYAXIS_None;
+				Axes[i].DeadZone = JOYDEADZONE_DEFAULT;
+				Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT;
+				Axes[i].DigitalThreshold = JOYTHRESH_DEFAULT;
+				Axes[i].ResponseCurvePreset = JOYCURVE_DEFAULT;
+				Axes[i].ResponseCurve = JOYCURVE[JOYCURVE_DEFAULT];
+			}
 		}
 	}
 
@@ -161,9 +282,10 @@ public:
 	{
 		return Enabled;
 	}
-	
+
 	void SetEnabled(bool enabled)
 	{
+		SettingsChanged = true;
 		Enabled = enabled;
 	}
 
@@ -188,63 +310,101 @@ public:
 		}
 	}
 
-	void ProcessInput()
-	{
+	void ProcessInput() {
 		uint8_t buttonstate;
 
-		for (int i = 0; i < NumAxes; ++i)
+		if (Mapping)
 		{
-			buttonstate = 0;
+			// GameController API available
 
-			Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0;
-			Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
+			auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold;
+			auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold;
 
-			// Map button to axis
-			// X and Y are handled differently so if we have 2 or more axes then we'll use that code instead.
-			if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS))
+			for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i)
 			{
-				Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
-				Axes[i].ButtonValue = buttonstate;
-			}
-		}
+				buttonstate = 0;
 
-		if(NumAxes > 1)
-		{
-			buttonstate = Joy_XYAxesToButtons(Axes[0].Value, Axes[1].Value);
+				Axes[i].Value = SDL_GameControllerGetAxis(Mapping, static_cast(i))/32767.0;
+				Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
+				Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value);
+			}
+
+			auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold;
+			auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold;
+
+			if (lastTriggerL != currTriggerL) Joy_GenerateButtonEvent(currTriggerL, KEY_PAD_LTRIGGER);
+			if (lastTriggerR != currTriggerR) Joy_GenerateButtonEvent(currTriggerR, KEY_PAD_RTRIGGER);
+
+			// todo: right stick
+			buttonstate = Joy_XYAxesToButtons(
+				abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value,
+				abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value
+			);
 			Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
 			Axes[0].ButtonValue = buttonstate;
 		}
-
-		// Map POV hats to buttons and axes.  Why axes?  Well apparently I have
-		// a gamepad where the left control stick is a POV hat (instead of the
-		// d-pad like you would expect, no that's pressure sensitive).  Also
-		// KDE's joystick dialog maps them to axes as well.
-		for (int i = 0; i < NumHats; ++i)
+		else
 		{
-			AxisInfo &x = Axes[NumAxes + i*2];
-			AxisInfo &y = Axes[NumAxes + i*2 + 1];
+			// Joystick API fallback
 
-			buttonstate = SDL_JoystickGetHat(Device, i);
-
-			// If we're going to assume that we can pass SDL's value into
-			// Joy_GenerateButtonEvents then we might as well assume the format here.
-			if(buttonstate & 0x1) // Up
-				y.Value = -1.0;
-			else if(buttonstate & 0x4) // Down
-				y.Value = 1.0;
-			else
-				y.Value = 0.0;
-			if(buttonstate & 0x2) // Left
-				x.Value = 1.0;
-			else if(buttonstate & 0x8) // Right
-				x.Value = -1.0;
-			else
-				x.Value = 0.0;
-
-			if(i < 4)
+			for (int i = 0; i < NumAxes; ++i)
 			{
-				Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4);
-				x.ButtonValue = buttonstate;
+				buttonstate = 0;
+
+				Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0;
+				Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
+				Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value);
+
+				// Map button to axis
+				// X and Y are handled differently so if we have 2 or more axes then we'll use that code instead.
+				if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS))
+				{
+					Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
+					Axes[i].ButtonValue = buttonstate;
+				}
+			}
+
+			if(NumAxes > 1)
+			{
+				buttonstate = Joy_XYAxesToButtons(
+					abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value,
+					abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value
+				);
+				Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
+				Axes[0].ButtonValue = buttonstate;
+			}
+
+			// Map POV hats to buttons and axes.  Why axes?  Well apparently I have
+			// a gamepad where the left control stick is a POV hat (instead of the
+			// d-pad like you would expect, no that's pressure sensitive).  Also
+			// KDE's joystick dialog maps them to axes as well.
+			for (int i = 0; i < NumHats; ++i)
+			{
+				AxisInfo &x = Axes[NumAxes + i*2];
+				AxisInfo &y = Axes[NumAxes + i*2 + 1];
+
+				buttonstate = SDL_JoystickGetHat(Device, i);
+
+				// If we're going to assume that we can pass SDL's value into
+				// Joy_GenerateButtonEvents then we might as well assume the format here.
+				if(buttonstate & 0x1) // Up
+					y.Value = -1.0;
+				else if(buttonstate & 0x4) // Down
+					y.Value = 1.0;
+				else
+					y.Value = 0.0;
+				if(buttonstate & 0x2) // Left
+					x.Value = 1.0;
+				else if(buttonstate & 0x8) // Right
+					x.Value = -1.0;
+				else
+					x.Value = 0.0;
+
+				if(i < 4)
+				{
+					Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4);
+					x.ButtonValue = buttonstate;
+				}
 			}
 		}
 	}
@@ -255,33 +415,66 @@ protected:
 		FString Name;
 		float DeadZone;
 		float Multiplier;
+		float DigitalThreshold;
+		EJoyCurve ResponseCurvePreset;
+		CubicBezier ResponseCurve;
 		EJoyAxis GameAxis;
 		double Value;
 		uint8_t ButtonValue;
 	};
-	static const EJoyAxis DefaultAxes[5];
+	struct DefaultAxisConfig
+	{
+		float DeadZone;
+		EJoyAxis GameAxis;
+		float Multiplier;
+		float DigitalThreshold;
+		EJoyCurve ResponseCurvePreset;
+	};
+	static const DefaultAxisConfig DefaultJoystickAxes[5];
+	static const DefaultAxisConfig DefaultControllerAxes[6];
+	const DefaultAxisConfig * DefaultAxes;
+	int DefaultAxesCount;
 
 	int					DeviceIndex;
+	int					InstanceID;
 	SDL_Joystick		*Device;
+	SDL_GameController	*Mapping;
 
 	float				Multiplier;
 	bool				Enabled;
 	TArray	Axes;
 	int					NumAxes;
 	int					NumHats;
+	bool 				SettingsChanged;
 
 	friend class SDLInputJoystickManager;
 };
 
 // [Nash 4 Feb 2024] seems like on Linux, the third axis is actually the Left Trigger, resulting in the player uncontrollably looking upwards.
-const EJoyAxis SDLInputJoystick::DefaultAxes[5] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_None, JOYAXIS_Yaw, JOYAXIS_Pitch};
+const SDLInputJoystick::DefaultAxisConfig SDLInputJoystick::DefaultJoystickAxes[5] = {
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_DEFAULT, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT}
+};
+
+// Defaults if we have access to the GameController API for this device
+const SDLInputJoystick::DefaultAxisConfig SDLInputJoystick::DefaultControllerAxes[6] = {
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT},
+	{JOYDEADZONE_DEFAULT, JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT},
+};
 
 class SDLInputJoystickManager
 {
 public:
 	SDLInputJoystickManager()
 	{
-		this->UpdateDeviceList();
+		UpdateDeviceList();
 	}
 
 	void UpdateDeviceList()
@@ -318,6 +511,19 @@ public:
 			if(Joysticks[i]->Enabled) Joysticks[i]->ProcessInput();
 	}
 
+	bool IsJoystickEnabled(int instanceID)
+	{
+		for(unsigned int i = 0; i < Joysticks.Size(); i++)
+		{
+			if (Joysticks[i]->InstanceID != instanceID)
+			{
+				continue;
+			}
+			return Joysticks[i]->Enabled;
+		}
+		return false;
+	}
+
 protected:
 	TDeletingArray Joysticks;
 };
@@ -326,7 +532,7 @@ static SDLInputJoystickManager *JoystickManager;
 void I_StartupJoysticks()
 {
 #ifndef NO_SDL_JOYSTICK
-	if(SDL_InitSubSystem(SDL_INIT_JOYSTICK) >= 0)
+	if(SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) >= 0)
 		JoystickManager = new SDLInputJoystickManager();
 #endif
 }
@@ -335,7 +541,7 @@ void I_ShutdownInput()
 	if(JoystickManager)
 	{
 		delete JoystickManager;
-		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
+		SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
 	}
 }
 
@@ -365,6 +571,16 @@ void I_ProcessJoysticks()
 		JoystickManager->ProcessInput();
 }
 
+void I_JoyConsumeEvent(int instanceID, event_t * event)
+{
+	if (event->type == EV_KeyDown)
+	{
+		bool okay = use_joystick && JoystickManager && JoystickManager->IsJoystickEnabled(instanceID);
+		if (!okay) return;
+	}
+	D_PostEvent(event);
+}
+
 IJoystickConfig *I_UpdateDeviceList()
 {
 	JoystickManager->UpdateDeviceList();
diff --git a/src/common/platform/win32/i_dijoy.cpp b/src/common/platform/win32/i_dijoy.cpp
index 33c4574c9..2340bfc84 100644
--- a/src/common/platform/win32/i_dijoy.cpp
+++ b/src/common/platform/win32/i_dijoy.cpp
@@ -146,7 +146,6 @@ public:
 
 // MACROS ------------------------------------------------------------------
 
-#define DEFAULT_DEADZONE			0.25f
 
 // TYPES -------------------------------------------------------------------
 
@@ -170,15 +169,23 @@ public:
 	EJoyAxis GetAxisMap(int axis);
 	const char *GetAxisName(int axis);
 	float GetAxisScale(int axis);
+	float GetAxisDigitalThreshold(int axis);
+	EJoyCurve GetAxisResponseCurve(int axis);
+	float GetAxisResponseCurvePoint(int axis, int point);
 
 	void SetAxisDeadZone(int axis, float deadzone);
 	void SetAxisMap(int axis, EJoyAxis gameaxis);
 	void SetAxisScale(int axis, float scale);
+	void SetAxisDigitalThreshold(int axis, float threshold);
+	void SetAxisResponseCurve(int axis, EJoyCurve preset);
+	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	bool IsSensitivityDefault();
 	bool IsAxisDeadZoneDefault(int axis);
 	bool IsAxisMapDefault(int axis);
 	bool IsAxisScaleDefault(int axis);
+	bool IsAxisDigitalThresholdDefault(int axis);
+	bool IsAxisResponseCurveDefault(int axis);
 
 	bool GetEnabled();
 	void SetEnabled(bool enabled);
@@ -201,6 +208,9 @@ protected:
 		float Value;
 		float DeadZone, DefaultDeadZone;
 		float Multiplier, DefaultMultiplier;
+		float DigitalThreshold, DefaultDigitalThreshold;
+		EJoyCurve ResponseCurvePreset, DefaultResponseCurvePreset;
+		CubicBezier ResponseCurve;
 		EJoyAxis GameAxis, DefaultGameAxis;
 		uint8_t ButtonValue;
 	};
@@ -454,16 +464,21 @@ void FDInputJoystick::ProcessInput()
 		axisval = (value - info->Min) * 2.0 / (info->Max - info->Min) - 1.0;
 		// Cancel out dead zone
 		axisval = Joy_RemoveDeadZone(axisval, info->DeadZone, &buttonstate);
+		axisval = Joy_ApplyResponseCurveBezier(info->ResponseCurve, axisval);
 		info->Value = float(axisval);
 		if (i < NUM_JOYAXISBUTTONS && (i > 2 || Axes.Size() == 1))
 		{
+			if (abs(axisval) < info->DigitalThreshold) buttonstate = 0;
 			Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
 		}
 		else if (i == 1)
 		{
 			// Since we sorted the axes, we know that the first two are definitely X and Y.
 			// They are probably a single stick, so use angular position to determine buttons.
-			buttonstate = Joy_XYAxesToButtons(Axes[0].Value, axisval);
+			buttonstate = Joy_XYAxesToButtons(
+				(abs(Axes[0].Value) < Axes[0].DigitalThreshold) ? 0 : Axes[0].Value,
+				(abs(axisval)       < info->DigitalThreshold)   ? 0 : axisval
+			);
 			Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
 		}
 		info->ButtonValue = buttonstate;
@@ -761,38 +776,54 @@ void FDInputJoystick::SetDefaultConfig()
 {
 	unsigned i;
 
-	Multiplier = 1;
+	Multiplier = JOYSENSITIVITY_DEFAULT;
 	for (i = 0; i < Axes.Size(); ++i)
 	{
-		Axes[i].DeadZone = DEFAULT_DEADZONE;
-		Axes[i].Multiplier = 1;
+		Axes[i].DeadZone = JOYDEADZONE_DEFAULT;
+		Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT;
 		Axes[i].GameAxis = JOYAXIS_None;
+		Axes[i].DigitalThreshold = JOYTHRESH_DEFAULT;
+		Axes[i].ResponseCurvePreset = JOYCURVE_DEFAULT;
+		Axes[i].ResponseCurve = JOYCURVE[JOYCURVE_DEFAULT];
 	}
 	// Triggers on a 360 controller have a much smaller deadzone.
 	if (Axes.Size() == 5 && Axes[4].Guid == GUID_ZAxis)
 	{
 		Axes[4].DeadZone = 30 / 256.f;
+		Axes[4].DigitalThreshold = JOYTHRESH_TRIGGER;
 	}
 	// Two axes? Horizontal is yaw and vertical is forward.
 	if (Axes.Size() == 2)
 	{
 		Axes[0].GameAxis = JOYAXIS_Yaw;
+		Axes[0].DigitalThreshold = JOYTHRESH_STICK_X;
+
 		Axes[1].GameAxis = JOYAXIS_Forward;
+		Axes[1].DigitalThreshold = JOYTHRESH_STICK_Y;
 	}
 	// Three axes? First two are movement, third is yaw.
 	else if (Axes.Size() >= 3)
 	{
 		Axes[0].GameAxis = JOYAXIS_Side;
+		Axes[0].DigitalThreshold = JOYTHRESH_STICK_X;
+
 		Axes[1].GameAxis = JOYAXIS_Forward;
+		Axes[1].DigitalThreshold = JOYTHRESH_STICK_Y;
+
 		Axes[2].GameAxis = JOYAXIS_Yaw;
+		Axes[2].DigitalThreshold = JOYTHRESH_STICK_X;
+
 		// Four axes? First two are movement, last two are looking around.
 		if (Axes.Size() >= 4)
 		{
-			Axes[3].GameAxis = JOYAXIS_Pitch;	Axes[3].Multiplier = 0.75f;
+			Axes[3].GameAxis = JOYAXIS_Pitch;
+			// Axes[3].Multiplier = 0.75f;
+			Axes[3].DigitalThreshold = JOYTHRESH_STICK_Y;
 			// Five axes? Use the fifth one for moving up and down.
 			if (Axes.Size() >= 5)
 			{
 				Axes[4].GameAxis = JOYAXIS_Up;
+				Axes[4].DigitalThreshold = JOYTHRESH_STICK_Y;
 			}
 		}
 	}
@@ -805,6 +836,8 @@ void FDInputJoystick::SetDefaultConfig()
 		Axes[i].DefaultDeadZone = Axes[i].DeadZone;
 		Axes[i].DefaultMultiplier = Axes[i].Multiplier;
 		Axes[i].DefaultGameAxis = Axes[i].GameAxis;
+		Axes[i].DefaultDigitalThreshold = Axes[i].DigitalThreshold;
+		Axes[i].DefaultResponseCurvePreset = Axes[i].ResponseCurvePreset;
 	}
 }
 
@@ -849,7 +882,7 @@ void FDInputJoystick::SetSensitivity(float scale)
 
 bool FDInputJoystick::IsSensitivityDefault()
 {
-	return Multiplier == 1;
+	return Multiplier == JOYSENSITIVITY_DEFAULT;
 }
 
 //===========================================================================
@@ -923,6 +956,51 @@ float FDInputJoystick::GetAxisScale(int axis)
 	return Axes[axis].Multiplier;
 }
 
+//===========================================================================
+//
+// FDInputJoystick :: GetAxisDigitalThreshold
+//
+//===========================================================================
+
+float FDInputJoystick::GetAxisDigitalThreshold(int axis)
+{
+	if (unsigned(axis) >= Axes.Size())
+	{
+		return JOYTHRESH_DEFAULT;
+	}
+	return Axes[axis].DigitalThreshold;
+}
+
+//===========================================================================
+//
+// FDInputJoystick :: GetAxisResponseCurve
+//
+//===========================================================================
+
+EJoyCurve FDInputJoystick::GetAxisResponseCurve(int axis)
+{
+	if (unsigned(axis) >= Axes.Size())
+	{
+		return JOYCURVE_DEFAULT;
+	}
+	return Axes[axis].ResponseCurvePreset;
+}
+
+//===========================================================================
+//
+// FDInputJoystick :: GetAxisResponseCurvePoint
+//
+//===========================================================================
+
+float FDInputJoystick::GetAxisResponseCurvePoint(int axis, int point)
+{
+	if (unsigned(axis) >= Axes.Size() || unsigned(point) >= 4)
+	{
+		return 0;
+	}
+	return Axes[axis].ResponseCurve.pts[point];
+}
+
 //===========================================================================
 //
 // FDInputJoystick :: SetAxisDeadZone
@@ -965,6 +1043,52 @@ void FDInputJoystick::SetAxisScale(int axis, float scale)
 	}
 }
 
+//===========================================================================
+//
+// FDInputJoystick :: SetAxisDigitalThreshold
+//
+//===========================================================================
+
+void FDInputJoystick::SetAxisDigitalThreshold(int axis, float threshold)
+{
+	if (unsigned(axis) < Axes.Size())
+	{
+		Axes[axis].DigitalThreshold = threshold;
+	}
+}
+
+//===========================================================================
+//
+// FDInputJoystick :: SetAxisResponseCurve
+//
+//===========================================================================
+
+void FDInputJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset)
+{
+	if (unsigned(axis) < Axes.Size())
+	{
+		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurvePreset = preset;
+		if (preset == JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurve = JOYCURVE[preset];
+	}
+}
+
+//===========================================================================
+//
+// FDInputJoystick :: SetAxisResponseCurvePoint
+//
+//===========================================================================
+
+void FDInputJoystick::SetAxisResponseCurvePoint(int axis, int point, float value)
+{
+	if (unsigned(axis) < Axes.Size() && unsigned(point) < 4)
+	{
+		Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
+		Axes[axis].ResponseCurve.pts[point] = value;
+	}
+}
+
 //===========================================================================
 //
 // FDInputJoystick :: IsAxisDeadZoneDefault
@@ -995,6 +1119,36 @@ bool FDInputJoystick::IsAxisScaleDefault(int axis)
 	return true;
 }
 
+//===========================================================================
+//
+// FDInputJoystick :: IsAxisDigitalThresholdDefault
+//
+//===========================================================================
+
+bool FDInputJoystick::IsAxisDigitalThresholdDefault(int axis)
+{
+	if (unsigned(axis) < Axes.Size())
+	{
+		return Axes[axis].DigitalThreshold == Axes[axis].DefaultDigitalThreshold;
+	}
+	return true;
+}
+
+//===========================================================================
+//
+// FDInputJoystick :: IsAxisResponseCurveDefault
+//
+//===========================================================================
+
+bool FDInputJoystick::IsAxisResponseCurveDefault(int axis)
+{
+	if (unsigned(axis) < Axes.Size())
+	{
+		return Axes[axis].ResponseCurvePreset == Axes[axis].DefaultResponseCurvePreset;
+	}
+	return true;
+}
+
 //===========================================================================
 //
 // FDInputJoystick :: GetEnabled
diff --git a/src/common/platform/win32/i_rawps2.cpp b/src/common/platform/win32/i_rawps2.cpp
index 2f89739bd..09bb314b0 100644
--- a/src/common/platform/win32/i_rawps2.cpp
+++ b/src/common/platform/win32/i_rawps2.cpp
@@ -48,7 +48,6 @@
 
 // MACROS ------------------------------------------------------------------
 
-#define DEFAULT_DEADZONE			0.25f
 #define STATUS_SWITCH_TIME			3
 
 #define VID_PLAY_COM							0x0b43
@@ -104,15 +103,23 @@ public:
 	EJoyAxis GetAxisMap(int axis);
 	const char *GetAxisName(int axis);
 	float GetAxisScale(int axis);
+	float GetAxisDigitalThreshold(int axis);
+	EJoyCurve GetAxisResponseCurve(int axis);
+	float GetAxisResponseCurvePoint(int axis, int point);
 
 	void SetAxisDeadZone(int axis, float deadzone);
 	void SetAxisMap(int axis, EJoyAxis gameaxis);
 	void SetAxisScale(int axis, float scale);
+	void SetAxisDigitalThreshold(int axis, float threshold);
+	void SetAxisResponseCurve(int axis, EJoyCurve preset);
+	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	bool IsSensitivityDefault();
 	bool IsAxisDeadZoneDefault(int axis);
 	bool IsAxisMapDefault(int axis);
 	bool IsAxisScaleDefault(int axis);
+	bool IsAxisDigitalThresholdDefault(int axis);
+	bool IsAxisResponseCurveDefault(int axis);
 
 	bool GetEnabled();
 	void SetEnabled(bool enabled);
@@ -130,6 +137,9 @@ protected:
 		float Value;
 		float DeadZone;
 		float Multiplier;
+		float DigitalThreshold;
+		EJoyCurve ResponseCurvePreset;
+		CubicBezier ResponseCurve;
 		EJoyAxis GameAxis;
 		uint8_t ButtonValue;
 	};
@@ -137,6 +147,8 @@ protected:
 	{
 		EJoyAxis GameAxis;
 		float Multiplier;
+		float DigitalThreshold;
+		EJoyCurve ResponseCurvePreset;
 	};
 	enum
 	{
@@ -357,11 +369,11 @@ static const char *AxisNames[] =
 
 FRawPS2Controller::DefaultAxisConfig FRawPS2Controller::DefaultAxes[NUM_AXES] =
 {
-	// Game axis, multiplier
-	{ JOYAXIS_Side, 1 },		// ThumbLX
-	{ JOYAXIS_Forward, 1 },		// ThumbLY
-	{ JOYAXIS_Yaw, 1 },			// ThumbRX
-	{ JOYAXIS_Pitch, 0.75 },	// ThumbRY
+	// Game axis, multiplier, digital threshold, response curve A, response curve B
+	{ JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbLX
+	{ JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbLY
+	{ JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbRX
+	{ JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbRY
 };
 
 // CODE --------------------------------------------------------------------
@@ -552,11 +564,16 @@ void FRawPS2Controller::ProcessThumbstick(int value1, AxisInfo *axis1, int value
 	axisval2 = value2 * (2.0 / 255) - 1.0;
 	axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL);
 	axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL);
+	axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1);
+	axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2);
 	axis1->Value = float(axisval1);
 	axis2->Value = float(axisval2);
 
 	// We store all four buttons in the first axis and ignore the second.
-	buttonstate = Joy_XYAxesToButtons(axisval1, axisval2);
+	buttonstate = Joy_XYAxesToButtons(
+		(abs(axisval1) < axis1->DigitalThreshold) ? 0 : axisval1,
+		(abs(axisval2) < axis2->DigitalThreshold) ? 0 : axisval2
+	);
 	Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base);
 	axis1->ButtonValue = buttonstate;
 }
@@ -644,10 +661,10 @@ void FRawPS2Controller::AddAxes(float axes[NUM_JOYAXIS])
 
 void FRawPS2Controller::SetDefaultConfig()
 {
-	Multiplier = 1;
+	Multiplier = JOYSENSITIVITY_DEFAULT;
 	for (int i = 0; i < NUM_AXES; ++i)
 	{
-		Axes[i].DeadZone = DEFAULT_DEADZONE;
+		Axes[i].DeadZone = JOYDEADZONE_DEFAULT;
 		Axes[i].GameAxis = DefaultAxes[i].GameAxis;
 		Axes[i].Multiplier = DefaultAxes[i].Multiplier;
 	}
@@ -712,7 +729,7 @@ void FRawPS2Controller::SetSensitivity(float scale)
 
 bool FRawPS2Controller::IsSensitivityDefault()
 {
-	return Multiplier == 1;
+	return Multiplier == JOYSENSITIVITY_DEFAULT;
 }
 
 //==========================================================================
@@ -786,6 +803,51 @@ float FRawPS2Controller::GetAxisScale(int axis)
 	return 0;
 }
 
+//==========================================================================
+//
+// FRawPS2Controller :: GetAxisDigitalThreshold
+//
+//==========================================================================
+
+float FRawPS2Controller::GetAxisDigitalThreshold(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].DigitalThreshold;
+	}
+	return JOYTHRESH_DEFAULT;
+}
+
+//==========================================================================
+//
+// FRawPS2Controller :: GetAxisResponseCurve
+//
+//==========================================================================
+
+EJoyCurve FRawPS2Controller::GetAxisResponseCurve(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].ResponseCurvePreset;
+	}
+	return JOYCURVE_DEFAULT;
+}
+
+//==========================================================================
+//
+// FRawPS2Controller :: GetAxisResponseCurvePoint
+//
+//==========================================================================
+
+float FRawPS2Controller::GetAxisResponseCurvePoint(int axis, int point)
+{
+	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
+	{
+		return Axes[axis].ResponseCurve.pts[point];
+	}
+	return 0;
+}
+
 //==========================================================================
 //
 // FRawPS2Controller :: SetAxisDeadZone
@@ -828,6 +890,52 @@ void FRawPS2Controller::SetAxisScale(int axis, float scale)
 	}
 }
 
+//==========================================================================
+//
+// FRawPS2Controller :: SetAxisDigitalThreshold
+//
+//==========================================================================
+
+void FRawPS2Controller::SetAxisDigitalThreshold(int axis, float threshold)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		Axes[axis].DigitalThreshold = threshold;
+	}
+}
+
+//==========================================================================
+//
+// FRawPS2Controller :: SetAxisResponseCurve
+//
+//==========================================================================
+
+void FRawPS2Controller::SetAxisResponseCurve(int axis, EJoyCurve preset)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurvePreset = preset;
+		if (preset == JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurve = JOYCURVE[preset];
+	}
+}
+
+//==========================================================================
+//
+// FRawPS2Controller :: SetAxisResponseCurvePoint
+//
+//==========================================================================
+
+void FRawPS2Controller::SetAxisResponseCurvePoint(int axis, int point, float value)
+{
+	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
+	{
+		Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
+		Axes[axis].ResponseCurve.pts[point] = value;
+	}
+}
+
 //===========================================================================
 //
 // FRawPS2Controller :: IsAxisDeadZoneDefault
@@ -838,7 +946,7 @@ bool FRawPS2Controller::IsAxisDeadZoneDefault(int axis)
 {
 	if (unsigned(axis) < NUM_AXES)
 	{
-		return Axes[axis].DeadZone == DEFAULT_DEADZONE;
+		return Axes[axis].DeadZone == JOYDEADZONE_DEFAULT;
 	}
 	return true;
 }
@@ -858,6 +966,36 @@ bool FRawPS2Controller::IsAxisScaleDefault(int axis)
 	return true;
 }
 
+//===========================================================================
+//
+// FRawPS2Controller :: IsAxisDigitalThresholdDefault
+//
+//===========================================================================
+
+bool FRawPS2Controller::IsAxisDigitalThresholdDefault(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold;
+	}
+	return true;
+}
+
+//===========================================================================
+//
+// FRawPS2Controller :: IsAxisResponseCurveDefault
+//
+//===========================================================================
+
+bool FRawPS2Controller::IsAxisResponseCurveDefault(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset;
+	}
+	return true;
+}
+
 //===========================================================================
 //
 // FRawPS2Controller :: GetEnabled
diff --git a/src/common/platform/win32/i_xinput.cpp b/src/common/platform/win32/i_xinput.cpp
index 0f3c19237..890888a4b 100644
--- a/src/common/platform/win32/i_xinput.cpp
+++ b/src/common/platform/win32/i_xinput.cpp
@@ -94,15 +94,23 @@ public:
 	EJoyAxis GetAxisMap(int axis);
 	const char *GetAxisName(int axis);
 	float GetAxisScale(int axis);
+	float GetAxisDigitalThreshold(int axis);
+	EJoyCurve GetAxisResponseCurve(int axis);
+	float GetAxisResponseCurvePoint(int axis, int point);
 
 	void SetAxisDeadZone(int axis, float deadzone);
 	void SetAxisMap(int axis, EJoyAxis gameaxis);
 	void SetAxisScale(int axis, float scale);
+	void SetAxisDigitalThreshold(int axis, float threshold);
+	void SetAxisResponseCurve(int axis, EJoyCurve preset);
+	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	bool IsSensitivityDefault();
 	bool IsAxisDeadZoneDefault(int axis);
 	bool IsAxisMapDefault(int axis);
 	bool IsAxisScaleDefault(int axis);
+	bool IsAxisDigitalThresholdDefault(int axis);
+	bool IsAxisResponseCurveDefault(int axis);
 
 	bool GetEnabled();
 	void SetEnabled(bool enabled);
@@ -122,12 +130,17 @@ protected:
 		float Multiplier;
 		EJoyAxis GameAxis;
 		uint8_t ButtonValue;
+		float DigitalThreshold;
+		EJoyCurve ResponseCurvePreset;
+		CubicBezier ResponseCurve;
 	};
 	struct DefaultAxisConfig
 	{
 		float DeadZone;
 		EJoyAxis GameAxis;
 		float Multiplier;
+		float DigitalThreshold;
+		EJoyCurve ResponseCurvePreset;
 	};
 	enum
 	{
@@ -211,13 +224,13 @@ static const char *AxisNames[] =
 
 FXInputController::DefaultAxisConfig FXInputController::DefaultAxes[NUM_AXES] =
 {
-	// Dead zone, game axis, multiplier
-	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, 1 },		// ThumbLX
-	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, 1 },	// ThumbLY
-	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, 1 },		// ThumbRX
-	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, 0.75 },	// ThumbRY
-	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 },			// LeftTrigger
-	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 }			// RightTrigger
+	// Dead zone, game axis, multiplier, digitalthreshold, curveA, curveB
+	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f,  JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbLX
+	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f,  JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbLY
+	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbRX
+	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbRY
+	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f,      JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT }, // LeftTrigger
+	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f,      JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT }  // RightTrigger
 };
 
 // CODE --------------------------------------------------------------------
@@ -327,11 +340,16 @@ void FXInputController::ProcessThumbstick(int value1, AxisInfo *axis1,
 	axisval2 = (value2 - SHRT_MIN) * 2.0 / 65536 - 1.0;
 	axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL);
 	axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL);
+	axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1);
+	axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2);
 	axis1->Value = float(axisval1);
 	axis2->Value = float(axisval2);
 
 	// We store all four buttons in the first axis and ignore the second.
-	buttonstate = Joy_XYAxesToButtons(axisval1, axisval2);
+	buttonstate = Joy_XYAxesToButtons(
+		abs(axisval1) < axis1->DigitalThreshold ? 0: axisval1,
+		abs(axisval2) < axis2->DigitalThreshold ? 0: axisval2
+	);
 	Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base);
 	axis1->ButtonValue = buttonstate;
 }
@@ -351,6 +369,11 @@ void FXInputController::ProcessTrigger(int value, AxisInfo *axis, int base)
 	double axisval;
 
 	axisval = Joy_RemoveDeadZone(value / 256.0, axis->DeadZone, &buttonstate);
+	axisval = Joy_ApplyResponseCurveBezier(axis->ResponseCurve, axisval);
+
+	// TODO: probably just put this into Joy_RemoveDeadZone
+	if (abs(axisval) < axis->DigitalThreshold) buttonstate = 0;
+
 	Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 1, base);
 	axis->ButtonValue = buttonstate;
 	axis->Value = float(axisval);
@@ -431,12 +454,15 @@ void FXInputController::AddAxes(float axes[NUM_JOYAXIS])
 
 void FXInputController::SetDefaultConfig()
 {
-	Multiplier = 1;
+	Multiplier = JOYSENSITIVITY_DEFAULT;
 	for (int i = 0; i < NUM_AXES; ++i)
 	{
 		Axes[i].DeadZone = DefaultAxes[i].DeadZone;
 		Axes[i].GameAxis = DefaultAxes[i].GameAxis;
 		Axes[i].Multiplier = DefaultAxes[i].Multiplier;
+		Axes[i].DigitalThreshold = DefaultAxes[i].DigitalThreshold;
+		Axes[i].ResponseCurve = JOYCURVE[DefaultAxes[i].ResponseCurvePreset];
+		Axes[i].ResponseCurvePreset = DefaultAxes[i].ResponseCurvePreset;
 	}
 }
 
@@ -494,7 +520,7 @@ void FXInputController::SetSensitivity(float scale)
 
 bool FXInputController::IsSensitivityDefault()
 {
-	return Multiplier == 1;
+	return Multiplier == JOYSENSITIVITY_DEFAULT;
 }
 
 //==========================================================================
@@ -565,6 +591,51 @@ float FXInputController::GetAxisScale(int axis)
 	{
 		return Axes[axis].Multiplier;
 	}
+	return JOYSENSITIVITY_DEFAULT;
+}
+
+//==========================================================================
+//
+// FXInputController :: GetAxisDigitalThreshold
+//
+//==========================================================================
+
+float FXInputController::GetAxisDigitalThreshold(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].DigitalThreshold;
+	}
+	return JOYTHRESH_DEFAULT;
+}
+
+//==========================================================================
+//
+// FXInputController :: GetAxisResponseCurve
+//
+//==========================================================================
+
+EJoyCurve FXInputController::GetAxisResponseCurve(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].ResponseCurvePreset;
+	}
+	return JOYCURVE_DEFAULT;
+}
+
+//==========================================================================
+//
+// FXInputController :: GetAxisResponseCurvePoint
+//
+//==========================================================================
+
+float FXInputController::GetAxisResponseCurvePoint(int axis, int point)
+{
+	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
+	{
+		return Axes[axis].ResponseCurve.pts[point];
+	}
 	return 0;
 }
 
@@ -610,6 +681,52 @@ void FXInputController::SetAxisScale(int axis, float scale)
 	}
 }
 
+//==========================================================================
+//
+// FXInputController :: SetAxisDigitalThreshold
+//
+//==========================================================================
+
+void FXInputController::SetAxisDigitalThreshold(int axis, float threshold)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		Axes[axis].DigitalThreshold = threshold;
+	}
+}
+
+//==========================================================================
+//
+// FXInputController :: SetAxisResponseCurve
+//
+//==========================================================================
+
+void FXInputController::SetAxisResponseCurve(int axis, EJoyCurve preset)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurvePreset = preset;
+		if (preset == JOYCURVE_CUSTOM) return;
+		Axes[axis].ResponseCurve = JOYCURVE[preset];
+	}
+}
+
+//==========================================================================
+//
+// FXInputController :: SetAxisResponseCurvePoint
+//
+//==========================================================================
+
+void FXInputController::SetAxisResponseCurvePoint(int axis, int point, float value)
+{
+	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
+	{
+		Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
+		Axes[axis].ResponseCurve.pts[point] = value;
+	}
+}
+
 //===========================================================================
 //
 // FXInputController :: IsAxisDeadZoneDefault
@@ -640,6 +757,36 @@ bool FXInputController::IsAxisScaleDefault(int axis)
 	return true;
 }
 
+//===========================================================================
+//
+// FXInputController :: IsAxisDigitalThresholdDefault
+//
+//===========================================================================
+
+bool FXInputController::IsAxisDigitalThresholdDefault(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold;
+	}
+	return true;
+}
+
+//===========================================================================
+//
+// FXInputController :: IsAxisResponseCurveDefault
+//
+//===========================================================================
+
+bool FXInputController::IsAxisResponseCurveDefault(int axis)
+{
+	if (unsigned(axis) < NUM_AXES)
+	{
+		return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset;
+	}
+	return true;
+}
+
 //===========================================================================
 //
 // FXInputController :: GetEnabled
diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt
index 046a7f1f6..2577b9aa7 100644
--- a/wadsrc/static/menudef.txt
+++ b/wadsrc/static/menudef.txt
@@ -837,6 +837,15 @@ OptionMenu "JoystickOptions" protected
 	Title "$JOYMNU_OPTIONS"
 }
 
+OptionValue "JoyAxisCurveNames"
+{
+	-1, "$OPTVAL_CUSTOM"
+	0, "$OPTVAL_DEFAULT"
+	1, "$OPTVAL_LINEAR"
+	2, "$OPTVAL_QUADRATIC"
+	3, "$OPTVAL_CUBIC"
+}
+
 OptionValue "JoyAxisMapNames"
 {
 	-1, "$OPTVAL_NONE"
diff --git a/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs b/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs
index 02669c1a3..1b813818f 100644
--- a/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs
+++ b/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs
@@ -130,7 +130,94 @@ class OptionMenuSliderJoyDeadZone : OptionMenuSliderBase
 
 //=============================================================================
 //
-// 
+//
+//
+//=============================================================================
+
+class OptionMenuSliderJoyDigitalThreshold : OptionMenuSliderBase
+{
+	int mAxis;
+	int mNeg;
+	JoystickConfig mJoy;
+
+	OptionMenuSliderJoyDigitalThreshold Init(String label, int axis, double min, double max, double step, int showval, JoystickConfig joy)
+	{
+		Super.Init(label, min, max, step, showval);
+		mAxis = axis;
+		mNeg = 1;
+		mJoy = joy;
+		return self;
+	}
+
+	override double GetSliderValue()
+	{
+		double d = mJoy.GetAxisDigitalThreshold(mAxis);
+		mNeg = d < 0? -1:1;
+		return d;
+	}
+
+	override void SetSliderValue(double val)
+	{
+		mJoy.SetAxisDigitalThreshold(mAxis, val * mNeg);
+	}
+}
+
+//=============================================================================
+//
+//
+//
+//=============================================================================
+
+class OptionMenuItemJoyCurve : OptionMenuItemOptionBase
+{
+	int mAxis;
+	JoystickConfig mJoy;
+
+	OptionMenuItemJoyCurve Init(String label, int axis, Name values, int center, JoystickConfig joy)
+	{
+		Super.Init(label, 'none', values, null, center);
+		mAxis = axis;
+		mJoy = joy;
+		return self;
+	}
+
+	override int GetSelection()
+	{
+		double f = mJoy.GetAxisResponseCurve(mAxis);
+		let opt = OptionValues.GetCount(mValues);
+		if (opt > 0)
+		{
+			// Map from joystick curve to menu selection.
+			for(int i = 0; i < opt; i++)
+			{
+				if (f ~== OptionValues.GetValue(mValues, i))
+				{
+					return i;
+				}
+			}
+		}
+		return JoystickConfig.JOYCURVE_CUSTOM;
+	}
+
+	override void SetSelection(int selection)
+	{
+		let opt = OptionValues.GetCount(mValues);
+		// Map from menu selection to joystick curve.
+		if (opt == 0 || selection >= opt)
+		{
+			selection = JoystickConfig.JOYCURVE_DEFAULT;
+		}
+		else
+		{
+			selection = int(OptionValues.GetValue(mValues, selection));
+		}
+		mJoy.setAxisResponseCurve(mAxis, selection);
+	}
+}
+
+//=============================================================================
+//
+//
 //
 //=============================================================================
 
@@ -340,8 +427,25 @@ class OptionMenuItemJoyConfigMenu : OptionMenuItemSubmenu
 					opt.mItems.Push(it);
 					it = new("OptionMenuItemInverter").Init("$JOYMNU_INVERT", i, false, joy);
 					opt.mItems.Push(it);
+					it = new("OptionMenuItemJoyCurve").Init("$JOYMNU_CURVE", i, "JoyAxisCurveNames", false, joy);
+					opt.mItems.Push(it);
+					// // is there a way to do something like this?
+					// // add extra options if the selected value is something specific?
+					// if (joy.GetAxisResponseCurve(i) == JoystickConfig.JOYCURVE_CUSTOM)
+					// {
+					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_X1", false);
+					// 	opt.mItems.Push(it);
+					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_Y1", false);
+					// 	opt.mItems.Push(it);
+					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_X2", false);
+					// 	opt.mItems.Push(it);
+					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_Y2", false);
+					// 	opt.mItems.Push(it);
+					// }
 					it = new("OptionMenuSliderJoyDeadZone").Init("$JOYMNU_DEADZONE", i, 0, 0.9, 0.05, 3, joy);
 					opt.mItems.Push(it);
+					it = new("OptionMenuSliderJoyDigitalThreshold").Init("$JOYMNU_THRESHOLD", i, 0, 0.9, 0.05, 3, joy);
+					opt.mItems.Push(it);
 				}
 			}
 			else
diff --git a/wadsrc/static/zscript/engine/ui/menu/menu.zs b/wadsrc/static/zscript/engine/ui/menu/menu.zs
index 7a0ef2036..ed98a97e9 100644
--- a/wadsrc/static/zscript/engine/ui/menu/menu.zs
+++ b/wadsrc/static/zscript/engine/ui/menu/menu.zs
@@ -68,6 +68,16 @@ struct JoystickConfig native version("2.4")
 		NUM_JOYAXIS,
 	};
 
+	enum EJoyCurve {
+		JOYCURVE_CUSTOM = -1,
+		JOYCURVE_DEFAULT,
+		JOYCURVE_LINEAR,
+		JOYCURVE_QUADRATIC,
+		JOYCURVE_CUBIC,
+
+		NUM_JOYCURVE
+	};
+
 	native float GetSensitivity();
 	native void SetSensitivity(float scale);
 
@@ -77,6 +87,15 @@ struct JoystickConfig native version("2.4")
 	native float GetAxisDeadZone(int axis);
 	native void SetAxisDeadZone(int axis, float zone);
 
+	native float GetAxisDigitalThreshold(int axis);
+	native void SetAxisDigitalThreshold(int axis, float thresh);
+
+	native int GetAxisResponseCurve(int axis);
+	native void SetAxisResponseCurve(int axis, int preset);
+
+	native float GetAxisResponseCurvePoint(int axis, int point);
+	native void SetAxisResponseCurvePoint(int axis, int point, float value);
+
 	native int GetAxisMap(int axis);
 	native void SetAxisMap(int axis, int gameaxis);
 

From 6756c850e76ca116571a136c53def5aeed573a0a Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Sat, 5 Jul 2025 16:54:51 -0400
Subject: [PATCH 251/384] Disable autoloading mods when playing online

Any mods when hosting and joining should have to be explicitly labeled, this way extra mods aren't accidentally making their way into the lists.
---
 src/d_main.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/d_main.cpp b/src/d_main.cpp
index 52d3746b5..3f0ee70a0 100644
--- a/src/d_main.cpp
+++ b/src/d_main.cpp
@@ -2029,7 +2029,8 @@ static void AddAutoloadFiles(const char *autoname, std::vector& all
 		}
 	}
 
-	if (!(gameinfo.flags & GI_SHAREWARE) && !Args->CheckParm("-noautoload") && !disableautoload)
+	// Disable autoloading in netgames as we don't want people who are hosting/joining loading up random files.
+	if (!(gameinfo.flags & GI_SHAREWARE) && !Args->CheckParm("-noautoload") && !disableautoload && !Args->CheckParm("-host") && !Args->CheckParm("-join"))
 	{
 		FString file;
 

From 9701bfaa5c5ceeb14dc2c51d626debb4a538a72e Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Wed, 2 Jul 2025 19:56:17 -0400
Subject: [PATCH 252/384] Added multiplayer tab for launcher + backend
 improvements

Adds a multiplayer tab to allow hosting and joining games from within the GZDoom launcher rather than needed to use the command line. This has its own set of defaults independent from the main play page which necessitated rewriting how this information is passed and stored in the backend. A startup info struct is now passed back which has its defaults set from the cvars and then propagates any changes to it back to the defaults after selection is complete, making it much simpler to interface with the engine defaults.
---
 src/CMakeLists.txt                          |   1 +
 src/common/engine/i_interface.cpp           | 143 +++++-
 src/common/engine/i_interface.h             |  46 ++
 src/common/platform/posix/cocoa/i_system.mm |  15 +-
 src/common/platform/posix/i_system.h        |   3 +-
 src/common/platform/posix/sdl/i_system.cpp  |  14 +-
 src/common/platform/win32/i_system.cpp      |   6 +-
 src/common/platform/win32/i_system.h        |   3 +-
 src/d_iwad.cpp                              |  87 +---
 src/launcher/launcherbuttonbar.cpp          |   9 +-
 src/launcher/launcherwindow.cpp             |  47 +-
 src/launcher/launcherwindow.h               |  14 +-
 src/launcher/networkpage.cpp                | 539 ++++++++++++++++++++
 src/launcher/networkpage.h                  | 112 ++++
 src/launcher/playgamepage.cpp               |  54 +-
 src/launcher/playgamepage.h                 |  11 +-
 src/launcher/settingspage.cpp               |  50 +-
 src/launcher/settingspage.h                 |  10 +-
 18 files changed, 1007 insertions(+), 157 deletions(-)
 create mode 100644 src/launcher/networkpage.cpp
 create mode 100644 src/launcher/networkpage.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ddeba6029..012c1f10e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -976,6 +976,7 @@ set (PCH_SOURCES
 	launcher/launcherbuttonbar.cpp
 	launcher/playgamepage.cpp
 	launcher/settingspage.cpp
+	launcher/networkpage.cpp
 
 	common/audio/sound/i_sound.cpp
 	common/audio/sound/oalsound.cpp
diff --git a/src/common/engine/i_interface.cpp b/src/common/engine/i_interface.cpp
index dfedbe8ee..fabf398d9 100644
--- a/src/common/engine/i_interface.cpp
+++ b/src/common/engine/i_interface.cpp
@@ -5,6 +5,7 @@
 #include "c_cvars.h"
 #include "gstrings.h"
 #include "version.h"
+#include "m_argv.h"
 
 static_assert(sizeof(void*) == 8,
 	"Only LP64/LLP64 builds are officially supported. "
@@ -30,11 +31,32 @@ bool			pauseext;
 
 FStartupInfo GameStartupInfo;
 
-CVAR(Bool, queryiwad, QUERYIWADDEFAULT, CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
-CVAR(String, defaultiwad, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
 CVAR(Bool, vid_fps, false, 0)
+CVAR(Bool, queryiwad, QUERYIWADDEFAULT, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Bool, saveargs, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Bool, savenetfile, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Bool, savenetargs, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(String, defaultiwad, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(String, defaultargs, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(String, defaultnetiwad, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(String, defaultnetargs, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetplayers, 8, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnethostport, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetticdup, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetmode, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetgamemode, -1, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Bool, defaultnetaltdm, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(String, defaultnetaddress, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetjoinport, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetpage, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnethostteam, 255, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Int, defaultnetjointeam, 255, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(Bool, defaultnetextratic, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
+CVAR(String, defaultnetsavefile, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
 
 EXTERN_CVAR(Bool, ui_generic)
+EXTERN_CVAR(Int, vid_preferbackend)
+EXTERN_CVAR(Bool, vid_fullscreen)
 
 CUSTOM_CVAR(String, language, "auto", CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG)
 {
@@ -43,3 +65,120 @@ CUSTOM_CVAR(String, language, "auto", CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOB
 	if (sysCallbacks.LanguageChanged) sysCallbacks.LanguageChanged(self);
 }
 
+// Some of this info has to be passed and managed from the front end since it's game-engine specific.
+FStartupSelectionInfo::FStartupSelectionInfo(const TArray& wads, FArgs& args, int startFlags) : Wads(&wads), Args(&args), DefaultStartFlags(startFlags)
+{
+	DefaultQueryIWAD = queryiwad;
+	DefaultLanguage = language;
+	DefaultBackend = vid_preferbackend;
+	DefaultFullscreen = vid_fullscreen;
+
+	if (defaultiwad[0] != '\0')
+	{
+		for (int i = 0; i < wads.Size(); ++i)
+		{
+			if (!wads[i].Name.CompareNoCase(defaultiwad))
+			{
+				DefaultIWAD = i;
+				break;
+			}
+		}
+	}
+	DefaultArgs = defaultargs;
+	bSaveArgs = saveargs;
+
+	if (defaultnetiwad[0] != '\0')
+	{
+		for (int i = 0; i < wads.Size(); ++i)
+		{
+			if (!wads[i].Name.CompareNoCase(defaultnetiwad))
+			{
+				DefaultNetIWAD = i;
+				break;
+			}
+		}
+	}
+	DefaultNetArgs = defaultnetargs;
+	DefaultNetPage = defaultnetpage;
+	DefaultNetSaveFile = defaultnetsavefile;
+	bSaveNetFile = savenetfile;
+	bSaveNetArgs = savenetargs;
+
+	DefaultNetPlayers = defaultnetplayers;
+	DefaultNetHostPort = defaultnethostport;
+	DefaultNetTicDup = defaultnetticdup;
+	DefaultNetMode = defaultnetmode;
+	DefaultNetGameMode = defaultnetgamemode;
+	DefaultNetAltDM = defaultnetaltdm;
+	DefaultNetHostTeam = defaultnethostteam;
+	DefaultNetExtraTic = defaultnetextratic;
+
+	DefaultNetAddress = defaultnetaddress;
+	DefaultNetJoinPort = defaultnetjoinport;
+	DefaultNetJoinTeam = defaultnetjointeam;
+}
+
+// Return whatever IWAD the user selected.
+int FStartupSelectionInfo::SaveInfo()
+{
+	DefaultLanguage.StripLeftRight();
+
+	DefaultArgs.StripLeftRight();
+
+	DefaultNetArgs.StripLeftRight();
+	AdditionalNetArgs.StripLeftRight();
+	DefaultNetAddress.StripLeftRight();
+	DefaultNetSaveFile.StripLeftRight();
+
+	queryiwad = DefaultQueryIWAD;
+	language = DefaultLanguage.GetChars();
+	vid_fullscreen = DefaultFullscreen;
+	if (DefaultBackend != vid_preferbackend)
+		vid_preferbackend = DefaultBackend;
+
+	if (bNetStart)
+	{
+		savenetfile = bSaveNetFile;
+		savenetargs = bSaveNetArgs;
+
+		defaultnetiwad = (*Wads)[DefaultNetIWAD].Name.GetChars();
+		defaultnetpage = DefaultNetPage;
+		defaultnetsavefile = savenetfile ? DefaultNetSaveFile.GetChars() : "";
+		defaultnetargs = savenetargs ? DefaultNetArgs.GetChars() : "";
+
+		if (bHosting)
+		{
+			defaultnetplayers = DefaultNetPlayers;
+			defaultnethostport = DefaultNetHostPort;
+			defaultnetticdup = DefaultNetTicDup;
+			defaultnetmode = DefaultNetMode;
+			defaultnetgamemode = DefaultNetGameMode;
+			defaultnetaltdm = DefaultNetAltDM;
+			defaultnethostteam = DefaultNetHostTeam;
+			defaultnetextratic = DefaultNetExtraTic;
+		}
+		else
+		{
+			defaultnetaddress = DefaultNetAddress.GetChars();
+			defaultnetjoinport = DefaultNetJoinPort;
+			defaultnetjointeam = DefaultNetJoinTeam;
+		}
+
+		if (!DefaultNetArgs.IsEmpty())
+			Args->AppendArgsString(DefaultNetArgs);
+		if (!AdditionalNetArgs.IsEmpty())
+			Args->AppendArgsString(AdditionalNetArgs);
+
+		return DefaultNetIWAD;
+	}
+
+	defaultiwad = (*Wads)[DefaultIWAD].Name.GetChars();
+	saveargs = bSaveArgs;
+	defaultargs = saveargs ? DefaultArgs.GetChars() : "";
+
+	if (!DefaultArgs.IsEmpty())
+		Args->AppendArgsString(DefaultArgs);
+
+	return DefaultIWAD;
+}
+
diff --git a/src/common/engine/i_interface.h b/src/common/engine/i_interface.h
index 4289bfeab..e901705f2 100644
--- a/src/common/engine/i_interface.h
+++ b/src/common/engine/i_interface.h
@@ -10,6 +10,7 @@ class FGameTexture;
 class FTextureID;
 enum EUpscaleFlags : int;
 class FConfigFile;
+class FArgs;
 struct FTranslationID;
 
 struct SystemCallbacks
@@ -58,6 +59,51 @@ struct WadStuff
 	FString Name;
 };
 
+struct FStartupSelectionInfo
+{
+	const TArray* Wads = nullptr;
+	FArgs* Args = nullptr;
+
+	// Local game info
+	int DefaultIWAD = 0;
+	FString DefaultArgs = {};
+	bool bSaveArgs = true;
+
+	// Settings
+	int DefaultStartFlags = 0;
+	bool DefaultQueryIWAD = true;
+	FString DefaultLanguage = "auto";
+	int DefaultBackend = 1;
+	bool DefaultFullscreen = true;
+
+	// Net game info
+	int DefaultNetIWAD = 0;
+	bool bNetStart = false;
+	bool bHosting = false;
+	bool bSaveNetFile = false;
+	bool bSaveNetArgs = true;
+	int DefaultNetPage = 0;
+	FString DefaultNetArgs = {};
+	FString AdditionalNetArgs = {}; // These ones shouldn't be saved.
+	FString DefaultNetSaveFile = {};
+	int DefaultNetHostTeam = 255;
+	int DefaultNetPlayers = 8;
+	int DefaultNetHostPort = 0;
+	int DefaultNetTicDup = 0;
+	bool DefaultNetExtraTic = false;
+	int DefaultNetMode = -1;
+	int DefaultNetGameMode = -1;
+	bool DefaultNetAltDM = false;
+
+	FString DefaultNetAddress = {};
+	int DefaultNetJoinPort = 0;
+	int DefaultNetJoinTeam = 255;
+
+	FStartupSelectionInfo() = delete;
+	FStartupSelectionInfo(const TArray& wads, FArgs& args, int startFlags);
+	int SaveInfo();
+};
+
 
 extern FString endoomName;
 extern bool batchrun;
diff --git a/src/common/platform/posix/cocoa/i_system.mm b/src/common/platform/posix/cocoa/i_system.mm
index 267bac31d..bafd7ab10 100644
--- a/src/common/platform/posix/cocoa/i_system.mm
+++ b/src/common/platform/posix/cocoa/i_system.mm
@@ -33,6 +33,7 @@
 
 #include "i_common.h"
 #include "c_cvars.h"
+#include "i_interface.h"
 
 #include 
 #include 
@@ -122,21 +123,27 @@ void I_ShowFatalError(const char *message)
 }
 
 
-int I_PickIWad(WadStuff* const wads, const int numwads, const bool showwin, const int defaultiwad, int&, FString&)
+bool I_PickIWad(bool showwin, FStartupSelectionInfo& info)
 {
 	if (!showwin)
 	{
-		return defaultiwad;
+		return true;
 	}
 
 	I_SetMainWindowVisible(false);
 
 	extern int I_PickIWad_Cocoa(WadStuff*, int, bool, int);
-	const int result = I_PickIWad_Cocoa(wads, numwads, showwin, defaultiwad);
+	const int result = I_PickIWad_Cocoa(&(*info.Wads)[0], (int)info.Wads->Size(), showwin, info.DefaultIWAD);
 
 	I_SetMainWindowVisible(true);
 
-	return result;
+	if (result >= 0)
+	{
+		info.DefaultIWAD = result;
+		return true;
+	}
+
+	return false;
 }
 
 
diff --git a/src/common/platform/posix/i_system.h b/src/common/platform/posix/i_system.h
index da99cf9e0..4f05ad09a 100644
--- a/src/common/platform/posix/i_system.h
+++ b/src/common/platform/posix/i_system.h
@@ -14,6 +14,7 @@
 #include "zstring.h"
 
 struct WadStuff;
+struct FStartupSelectionInfo;
 
 #ifndef SHARE_DIR
 #define SHARE_DIR "/usr/local/share/"
@@ -37,7 +38,7 @@ void I_PrintStr (const char *str);
 void I_SetIWADInfo ();
 
 // Pick from multiple IWADs to use
-int I_PickIWad (WadStuff *wads, int numwads, bool queryiwad, int defaultiwad, int&, FString &);
+bool I_PickIWad (bool showwin, FStartupSelectionInfo& info);
 
 // [RH] Checks the registry for Steam's install path, so we can scan its
 // directories for IWADs if the user purchased any through Steam.
diff --git a/src/common/platform/posix/sdl/i_system.cpp b/src/common/platform/posix/sdl/i_system.cpp
index db793f04c..059123622 100644
--- a/src/common/platform/posix/sdl/i_system.cpp
+++ b/src/common/platform/posix/sdl/i_system.cpp
@@ -302,17 +302,23 @@ void I_PrintStr(const char *cp)
 	if (StartWindow) RedrawProgressBar(ProgressBarCurPos,ProgressBarMaxPos);
 }
 
-int I_PickIWad (WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& autoloadflags, FString &extraArgs)
+bool I_PickIWad (bool showwin, FStartupSelectionInfo& info)
 {
 	if (!showwin)
 	{
-		return defaultiwad;
+		return true;
 	}
 
 #ifdef __APPLE__
-	return I_PickIWad_Cocoa (wads, numwads, showwin, defaultiwad);
+	const int ret = I_PickIWad_Cocoa(&(*info.Wads)[0], (int)info.Wads->Size(), showwin, info.DefaultIWAD);
+	if (ret >= 0)
+	{
+		info.DefaultIWAD = ret;
+		return true;
+	}
+	return false;
 #else
-	return LauncherWindow::ExecModal(wads, numwads, defaultiwad, &autoloadflags, &extraArgs);
+	return LauncherWindow::ExecModal(info);
 #endif
 }
 
diff --git a/src/common/platform/win32/i_system.cpp b/src/common/platform/win32/i_system.cpp
index c2615d51f..3ce88a38f 100644
--- a/src/common/platform/win32/i_system.cpp
+++ b/src/common/platform/win32/i_system.cpp
@@ -358,7 +358,7 @@ static void SetQueryIWad(HWND dialog)
 //
 //==========================================================================
 
-int I_PickIWad(WadStuff *wads, int numwads, bool showwin, int defaultiwad, int& autoloadflags, FString &extraArgs)
+bool I_PickIWad(bool showwin, FStartupSelectionInfo& info)
 {
 	int vkey;
 	if (stricmp(queryiwad_key, "shift") == 0)
@@ -375,9 +375,9 @@ int I_PickIWad(WadStuff *wads, int numwads, bool showwin, int defaultiwad, int&
 	}
 	if (showwin || (vkey != 0 && GetAsyncKeyState(vkey)))
 	{
-		return LauncherWindow::ExecModal(wads, numwads, defaultiwad, &autoloadflags, &extraArgs);
+		return LauncherWindow::ExecModal(info);
 	}
-	return defaultiwad;
+	return true;
 }
 
 //==========================================================================
diff --git a/src/common/platform/win32/i_system.h b/src/common/platform/win32/i_system.h
index f83b3f8c1..5bb027e05 100644
--- a/src/common/platform/win32/i_system.h
+++ b/src/common/platform/win32/i_system.h
@@ -9,6 +9,7 @@
 #include "utf8.h"
 
 struct WadStuff;
+struct FStartupSelectionInfo;
 
 // [RH] Detects the OS the game is running under.
 void I_DetectOS (void);
@@ -37,7 +38,7 @@ void I_PrintStr (const char *cp);
 void I_SetIWADInfo ();
 
 // Pick from multiple IWADs to use
-int I_PickIWad(WadStuff* wads, int numwads, bool queryiwad, int defaultiwad, int& autoloadflags, FString &extraArgs);
+bool I_PickIWad(bool showwin, FStartupSelectionInfo& info);
 
 // The ini could not be saved at exit
 bool I_WriteIniFailed (const char* filename);
diff --git a/src/d_iwad.cpp b/src/d_iwad.cpp
index 54fba56d7..755dfac3e 100644
--- a/src/d_iwad.cpp
+++ b/src/d_iwad.cpp
@@ -52,7 +52,6 @@
 #include "gstrings.h"
 
 EXTERN_CVAR(Bool, queryiwad);
-EXTERN_CVAR(String, defaultiwad);
 EXTERN_CVAR(Bool, disableautoload)
 EXTERN_CVAR(Bool, autoloadlights)
 EXTERN_CVAR(Bool, autoloadbrightmaps)
@@ -584,8 +583,6 @@ FString FIWadManager::IWADPathFileSearch(const FString &file)
 	return "";
 }
 
-CVAR(String, extra_args, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG);
-
 int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char *iwad, const char *zdoom_wad, const char *optional_wad)
 {
 	const char *iwadparm = Args->CheckValue ("-iwad");
@@ -759,69 +756,37 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char
 	// Present the IWAD selection box.
 	bool alwaysshow = (queryiwad && !Args->CheckParm("-iwad") && !foundprio);
 
-	if (alwaysshow || picks.Size() > 1)
+	if (!havepicked && (alwaysshow || picks.Size() > 1))
 	{
-		// Locate the user's prefered IWAD, if it was found.
-		if (defaultiwad[0] != '\0')
+		TArray wads;
+		for (auto & found : picks)
 		{
-			for (unsigned i = 0; i < picks.Size(); ++i)
-			{
-				FString &basename = mIWadInfos[picks[i].mInfoIndex].Name;
-				if (basename.CompareNoCase(defaultiwad) == 0)
-				{
-					pick = i;
-					break;
-				}
-			}
+			WadStuff stuff;
+			stuff.Name = mIWadInfos[found.mInfoIndex].Name;
+			stuff.Path = ExtractFileBase(found.mFullPath.GetChars());
+			wads.Push(stuff);
 		}
-		if (alwaysshow || picks.Size() > 1)
+
+		int flags = 0;
+		if (disableautoload) flags |= 1;
+		if (autoloadlights) flags |= 2;
+		if (autoloadbrightmaps) flags |= 4;
+		if (autoloadwidescreen) flags |= 8;
+
+		FStartupSelectionInfo info = FStartupSelectionInfo(wads, *Args, flags);
+		if (I_PickIWad(queryiwad, info))
 		{
-			if (!havepicked)
-			{
-				TArray wads;
-				for (auto & found : picks)
-				{
-					WadStuff stuff;
-					stuff.Name = mIWadInfos[found.mInfoIndex].Name;
-					stuff.Path = ExtractFileBase(found.mFullPath.GetChars());
-					wads.Push(stuff);
-				}
-				int flags = 0;;
-
-				if (disableautoload) flags |= 1;
-				if (autoloadlights) flags |= 2;
-				if (autoloadbrightmaps) flags |= 4;
-				if (autoloadwidescreen) flags |= 8;
-
-				FString extraArgs = *extra_args;
-
-				pick = I_PickIWad(&wads[0], (int)wads.Size(), queryiwad, pick, flags, extraArgs);
-				if (pick >= 0)
-				{
-					extraArgs.StripLeftRight();
-
-					extra_args = extraArgs.GetChars();
-					
-					if(extraArgs.Len() > 0)
-					{
-						Args->AppendArgsString(extraArgs);
-					}
-
-					disableautoload = !!(flags & 1);
-					autoloadlights = !!(flags & 2);
-					autoloadbrightmaps = !!(flags & 4);
-					autoloadwidescreen = !!(flags & 8);
-
-					// The newly selected IWAD becomes the new default
-					defaultiwad = mIWadInfos[picks[pick].mInfoIndex].Name.GetChars();
-				}
-				else
-				{
-					return -1;
-				}
-				havepicked = true;
-			}
+			pick = info.SaveInfo();
+			disableautoload = !!(info.DefaultStartFlags & 1);
+			autoloadlights = !!(info.DefaultStartFlags & 2);
+			autoloadbrightmaps = !!(info.DefaultStartFlags & 4);
+			autoloadwidescreen = !!(info.DefaultStartFlags & 8);
 		}
+		else
+		{
+			return -1;
+		}
+		havepicked = true;
 	}
 
 	// zdoom.pk3 must always be the first file loaded and the IWAD second.
diff --git a/src/launcher/launcherbuttonbar.cpp b/src/launcher/launcherbuttonbar.cpp
index 789881000..89e18070d 100644
--- a/src/launcher/launcherbuttonbar.cpp
+++ b/src/launcher/launcherbuttonbar.cpp
@@ -15,7 +15,14 @@ LauncherButtonbar::LauncherButtonbar(LauncherWindow* parent) : Widget(parent)
 
 void LauncherButtonbar::UpdateLanguage()
 {
-	PlayButton->SetText(GStrings.GetString("PICKER_PLAY"));
+	auto launcher = GetLauncher();
+	if (!launcher->IsInMultiplayer())
+		PlayButton->SetText(GStrings.GetString("PICKER_PLAY"));
+	else if (launcher->IsHosting())
+		PlayButton->SetText(GStrings.GetString("PICKER_PLAYHOST"));
+	else
+		PlayButton->SetText(GStrings.GetString("PICKER_PLAYJOIN"));
+
 	ExitButton->SetText(GStrings.GetString("PICKER_EXIT"));
 }
 
diff --git a/src/launcher/launcherwindow.cpp b/src/launcher/launcherwindow.cpp
index b60db7cf8..e6ab31839 100644
--- a/src/launcher/launcherwindow.cpp
+++ b/src/launcher/launcherwindow.cpp
@@ -3,6 +3,7 @@
 #include "launcherbuttonbar.h"
 #include "playgamepage.h"
 #include "settingspage.h"
+#include "networkpage.h"
 #include "v_video.h"
 #include "version.h"
 #include "i_interface.h"
@@ -12,25 +13,22 @@
 #include 
 #include 
 
-int LauncherWindow::ExecModal(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags, FString * extraArgs)
+bool LauncherWindow::ExecModal(FStartupSelectionInfo& info)
 {
 	Size screenSize = GetScreenSize();
 	double windowWidth = 615.0;
 	double windowHeight = 700.0;
 
-	auto launcher = std::make_unique(wads, numwads, defaultiwad, autoloadflags);
+	auto launcher = std::make_unique(info);
 	launcher->SetFrameGeometry((screenSize.width - windowWidth) * 0.5, (screenSize.height - windowHeight) * 0.5, windowWidth, windowHeight);
-	if(extraArgs) launcher->PlayGame->SetExtraArgs(extraArgs->GetChars());
 	launcher->Show();
 
 	DisplayWindow::RunLoop();
 
-	if(extraArgs) *extraArgs = launcher->PlayGame->GetExtraArgs();
-
 	return launcher->ExecResult;
 }
 
-LauncherWindow::LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags) : Widget(nullptr, WidgetType::Window)
+LauncherWindow::LauncherWindow(FStartupSelectionInfo& info) : Widget(nullptr, WidgetType::Window), Info(&info)
 {
 	SetWindowTitle(GAMENAME);
 
@@ -38,11 +36,15 @@ LauncherWindow::LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int
 	Pages = new TabWidget(this);
 	Buttonbar = new LauncherButtonbar(this);
 
-	PlayGame = new PlayGamePage(this, wads, numwads, defaultiwad);
-	Settings = new SettingsPage(this, autoloadflags);
+	PlayGame = new PlayGamePage(this, info);
+	Settings = new SettingsPage(this, info);
+	Network = new NetworkPage(this, info);
 
 	Pages->AddTab(PlayGame, "Play");
 	Pages->AddTab(Settings, "Settings");
+	Pages->AddTab(Network, "Multiplayer");
+
+	Network->InitializeTabs(info);
 
 	UpdateLanguage();
 
@@ -50,17 +52,38 @@ LauncherWindow::LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int
 	PlayGame->SetFocus();
 }
 
+void LauncherWindow::UpdatePlayButton()
+{
+	Buttonbar->UpdateLanguage();
+}
+
+bool LauncherWindow::IsInMultiplayer() const
+{
+	return Pages->GetCurrentIndex() >= 0 ? Pages->GetCurrentWidget() == Network : false;
+}
+
+bool LauncherWindow::IsHosting() const
+{
+	return IsInMultiplayer() && Network->IsInHost();
+}
+
 void LauncherWindow::Start()
 {
-	Settings->Save();
+	Info->bNetStart = IsInMultiplayer();
 
-	ExecResult = PlayGame->GetSelectedGame();
+	Settings->SetValues(*Info);
+	if (Info->bNetStart)
+		Network->SetValues(*Info);
+	else
+		PlayGame->SetValues(*Info);
+
+	ExecResult = true;
 	DisplayWindow::ExitLoop();
 }
 
 void LauncherWindow::Exit()
 {
-	ExecResult = -1;
+	ExecResult = false;
 	DisplayWindow::ExitLoop();
 }
 
@@ -68,8 +91,10 @@ void LauncherWindow::UpdateLanguage()
 {
 	Pages->SetTabText(PlayGame, GStrings.GetString("PICKER_TAB_PLAY"));
 	Pages->SetTabText(Settings, GStrings.GetString("OPTMNU_TITLE"));
+	Pages->SetTabText(Network, "Multiplayer");
 	PlayGame->UpdateLanguage();
 	Settings->UpdateLanguage();
+	Network->UpdateLanguage();
 	Buttonbar->UpdateLanguage();
 }
 
diff --git a/src/launcher/launcherwindow.h b/src/launcher/launcherwindow.h
index 8e7c8bbe6..d10623a36 100644
--- a/src/launcher/launcherwindow.h
+++ b/src/launcher/launcherwindow.h
@@ -9,18 +9,23 @@ class LauncherBanner;
 class LauncherButtonbar;
 class PlayGamePage;
 class SettingsPage;
+class NetworkPage;
 struct WadStuff;
+struct FStartupSelectionInfo;
 
 class LauncherWindow : public Widget
 {
 public:
-	static int ExecModal(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags, FString * extraArgs = nullptr);
+	static bool ExecModal(FStartupSelectionInfo& info);
 
-	LauncherWindow(WadStuff* wads, int numwads, int defaultiwad, int* autoloadflags);
+	LauncherWindow(FStartupSelectionInfo& info);
 	void UpdateLanguage();
 
 	void Start();
 	void Exit();
+	bool IsInMultiplayer() const;
+	bool IsHosting() const;
+	void UpdatePlayButton();
 
 private:
 	void OnClose() override;
@@ -32,6 +37,9 @@ private:
 
 	PlayGamePage* PlayGame = nullptr;
 	SettingsPage* Settings = nullptr;
+	NetworkPage* Network = nullptr;
 
-	int ExecResult = -1;
+	FStartupSelectionInfo* Info = nullptr;
+
+	bool ExecResult = false;
 };
diff --git a/src/launcher/networkpage.cpp b/src/launcher/networkpage.cpp
new file mode 100644
index 000000000..317042ebc
--- /dev/null
+++ b/src/launcher/networkpage.cpp
@@ -0,0 +1,539 @@
+
+#include "networkpage.h"
+#include "launcherwindow.h"
+#include "gstrings.h"
+#include "c_cvars.h"
+#include "i_net.h"
+#include "i_interface.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+constexpr double EditHeight = 24.0;
+
+NetworkPage::NetworkPage(LauncherWindow* launcher, const FStartupSelectionInfo& info) : Widget(nullptr), Launcher(launcher)
+{
+	ParametersEdit = new LineEdit(this);
+	ParametersLabel = new TextLabel(this);
+	SaveFileEdit = new LineEdit(this);
+	SaveFileLabel = new TextLabel(this);
+	SaveFileCheckbox = new CheckboxLabel(this);
+	SaveArgsCheckbox = new CheckboxLabel(this);
+	IWADsList = new ListView(this);
+
+	SaveFileCheckbox->SetChecked(info.bSaveNetFile);
+	if (!info.DefaultNetSaveFile.IsEmpty())
+		SaveFileEdit->SetText(info.DefaultNetSaveFile.GetChars());
+
+	SaveArgsCheckbox->SetChecked(info.bSaveNetArgs);
+	if (!info.DefaultNetArgs.IsEmpty())
+		ParametersEdit->SetText(info.DefaultNetArgs.GetChars());
+
+	StartPages = new TabWidget(this);
+	HostPage = new HostSubPage(this, info);
+	JoinPage = new JoinSubPage(this, info);
+
+	for (const auto& wad : *info.Wads)
+	{
+		const char* filepart = strrchr(wad.Path.GetChars(), '/');
+		if (filepart == nullptr)
+			filepart = wad.Path.GetChars();
+		else
+			++filepart;
+
+		FString work;
+		if (*filepart)
+			work.Format("%s (%s)", wad.Name.GetChars(), filepart);
+		else
+			work = wad.Name.GetChars();
+
+		IWADsList->AddItem(work.GetChars());
+	}
+
+	if (info.DefaultNetIWAD >= 0 && info.DefaultNetIWAD < info.Wads->Size())
+	{
+		IWADsList->SetSelectedItem(info.DefaultNetIWAD);
+		IWADsList->ScrollToItem(info.DefaultNetIWAD);
+	}
+
+	IWADsList->OnActivated = [=]() { OnIWADsListActivated(); };
+}
+
+// This has to be done after the main page is parented, otherwise it won't have the correct
+// info to pull from.
+void NetworkPage::InitializeTabs(const FStartupSelectionInfo& info)
+{
+	StartPages->AddTab(HostPage, "Host");
+	StartPages->AddTab(JoinPage, "Join");
+
+	switch (info.DefaultNetPage)
+	{
+	case 1:
+		StartPages->SetCurrentWidget(JoinPage);
+		break;
+	default:
+		StartPages->SetCurrentWidget(HostPage);
+		break;
+	}
+}
+
+void NetworkPage::OnIWADsListActivated()
+{
+	Launcher->Start();
+}
+
+void NetworkPage::OnSetFocus()
+{
+	IWADsList->SetFocus();
+}
+
+void NetworkPage::SetValues(FStartupSelectionInfo& info) const
+{
+	info.DefaultNetIWAD = IWADsList->GetSelectedItem();
+	info.DefaultNetArgs = ParametersEdit->GetText();
+
+	info.bHosting = IsInHost();
+	if (info.bHosting)
+	{
+		info.DefaultNetPage = 0;
+		HostPage->SetValues(info);
+	}
+	else
+	{
+		info.DefaultNetPage = 1;
+		JoinPage->SetValues(info);
+	}
+
+	info.bSaveNetFile = SaveFileCheckbox->GetChecked();
+	info.bSaveNetArgs = SaveArgsCheckbox->GetChecked();
+	const auto save = SaveFileEdit->GetText();
+	if (!save.empty())
+		info.AdditionalNetArgs.AppendFormat(" -loadgame %s", save.c_str());
+	info.DefaultNetSaveFile = save;
+}
+
+void NetworkPage::UpdatePlayButton()
+{
+	Launcher->UpdatePlayButton();
+}
+
+bool NetworkPage::IsInHost() const
+{
+	return StartPages->GetCurrentIndex() >= 0 ? StartPages->GetCurrentWidget() == HostPage : false;
+}
+
+void NetworkPage::OnGeometryChanged()
+{
+	const double w = GetWidth();
+	const double h = GetHeight();
+
+	const double wSize = w * 0.45 - 2.5;
+
+	double y = h - SaveArgsCheckbox->GetPreferredHeight();
+	SaveArgsCheckbox->SetFrameGeometry(0.0, y, wSize, SaveArgsCheckbox->GetPreferredHeight());
+
+	y -= SaveFileCheckbox->GetPreferredHeight();
+	SaveFileCheckbox->SetFrameGeometry(0.0, y, wSize, SaveFileCheckbox->GetPreferredHeight());
+
+	y -= EditHeight + 2.0;
+	ParametersEdit->SetFrameGeometry(0.0, y, wSize, EditHeight);
+	y -= ParametersLabel->GetPreferredHeight();
+	ParametersLabel->SetFrameGeometry(0.0, y, wSize, ParametersLabel->GetPreferredHeight());
+
+	y -= EditHeight + 2.0;
+	SaveFileEdit->SetFrameGeometry(0.0, y, wSize, EditHeight);
+	y -= SaveFileLabel->GetPreferredHeight();
+	SaveFileLabel->SetFrameGeometry(0.0, y, wSize, SaveFileLabel->GetPreferredHeight());
+	y -= 5.0;
+
+	IWADsList->SetFrameGeometry(0.0, 0.0, wSize, y);
+
+	const double xOfs = w * 0.45 + 2.5;
+	StartPages->SetFrameGeometry(xOfs, 0.0, w - xOfs, h);
+}
+
+void NetworkPage::UpdateLanguage()
+{
+	ParametersLabel->SetText(GStrings.GetString("PICKER_ADDPARM"));
+	SaveFileLabel->SetText(GStrings.GetString("PICKER_LOADSAVE"));
+	SaveFileCheckbox->SetText(GStrings.GetString("PICKER_REMSAVE"));
+	SaveArgsCheckbox->SetText(GStrings.GetString("PICKER_REMPARM"));
+
+	StartPages->SetTabText(HostPage, GStrings.GetString("PICKER_HOST"));
+	StartPages->SetTabText(JoinPage, GStrings.GetString("PICKER_JOIN"));
+	HostPage->UpdateLanguage();
+	JoinPage->UpdateLanguage();
+}
+
+HostSubPage::HostSubPage(NetworkPage* main, const FStartupSelectionInfo& info) : Widget(nullptr), MainTab(main)
+{
+	NetModesLabel = new TextLabel(this);
+	AutoNetmodeCheckbox = new CheckboxLabel(this);
+	PacketServerCheckbox = new CheckboxLabel(this);
+	PeerToPeerCheckbox = new CheckboxLabel(this);
+
+	AutoNetmodeCheckbox->SetRadioStyle(true);
+	PacketServerCheckbox->SetRadioStyle(true);
+	PeerToPeerCheckbox->SetRadioStyle(true);
+	AutoNetmodeCheckbox->FuncChanged = [this](bool on) { if (on) { PacketServerCheckbox->SetChecked(false); PeerToPeerCheckbox->SetChecked(false); }};
+	PacketServerCheckbox->FuncChanged = [this](bool on) { if (on) { AutoNetmodeCheckbox->SetChecked(false); PeerToPeerCheckbox->SetChecked(false); }};
+	PeerToPeerCheckbox->FuncChanged = [this](bool on) { if (on) { PacketServerCheckbox->SetChecked(false); AutoNetmodeCheckbox->SetChecked(false); }};
+	
+	switch (info.DefaultNetMode)
+	{
+	case 0:
+		PeerToPeerCheckbox->SetChecked(true);
+		break;
+	case 1:
+		PacketServerCheckbox->SetChecked(true);
+		break;
+	default:
+		AutoNetmodeCheckbox->SetChecked(true);
+		break;
+	}
+
+	TicDupList = new ListView(this);
+	TicDupLabel = new TextLabel(this);
+	ExtraTicCheckbox = new CheckboxLabel(this);
+
+	std::vector widths = { 30.0, 30.0 };
+	TicDupList->SetColumnWidths(widths);
+	TicDupList->AddItem("35.0");
+	TicDupList->UpdateItem("Hz", 0, 1);
+	TicDupList->AddItem("17.5");
+	TicDupList->UpdateItem("Hz", 1, 1);
+	TicDupList->AddItem("11.6");
+	TicDupList->UpdateItem("Hz", 2, 1);
+	TicDupList->SetSelectedItem(info.DefaultNetTicDup);
+	ExtraTicCheckbox->SetChecked(info.DefaultNetExtraTic);
+
+	GameModesLabel = new TextLabel(this);
+	CoopCheckbox = new CheckboxLabel(this);
+	DeathmatchCheckbox = new CheckboxLabel(this);
+	AltDeathmatchCheckbox = new CheckboxLabel(this);
+	TeamDeathmatchCheckbox = new CheckboxLabel(this);
+	TeamLabel = new TextLabel(this);
+	TeamEdit = new LineEdit(this);
+
+	// These are intentionally not radio buttons, they just act similarly for clarity in the UI.
+	CoopCheckbox->FuncChanged = [this](bool on) { if (on) { DeathmatchCheckbox->SetChecked(false); TeamDeathmatchCheckbox->SetChecked(false); }};
+	DeathmatchCheckbox->FuncChanged = [this](bool on) { if (on) { CoopCheckbox->SetChecked(false); TeamDeathmatchCheckbox->SetChecked(false); }};
+	TeamDeathmatchCheckbox->FuncChanged = [this](bool on) { if (on) { CoopCheckbox->SetChecked(false); DeathmatchCheckbox->SetChecked(false); }};
+
+	switch (info.DefaultNetGameMode)
+	{
+	case 0:
+		CoopCheckbox->SetChecked(true);
+		break;
+	case 1:
+		DeathmatchCheckbox->SetChecked(true);
+		break;
+	case 2:
+		TeamDeathmatchCheckbox->SetChecked(true);
+		break;
+	}
+	AltDeathmatchCheckbox->SetChecked(info.DefaultNetAltDM);
+
+	TeamEdit->SetMaxLength(3);
+	TeamEdit->SetNumericMode(true);
+	TeamEdit->SetTextInt(info.DefaultNetHostTeam);
+
+	MaxPlayersEdit = new LineEdit(this);
+	PortEdit = new LineEdit(this);
+	MaxPlayersLabel = new TextLabel(this);
+	PortLabel = new TextLabel(this);
+
+	MaxPlayersEdit->SetMaxLength(2);
+	MaxPlayersEdit->SetNumericMode(true);
+	MaxPlayersEdit->SetTextInt(info.DefaultNetPlayers);
+	PortEdit->SetMaxLength(5);
+	PortEdit->SetNumericMode(true);
+	if (info.DefaultNetHostPort > 0)
+		PortEdit->SetTextInt(info.DefaultNetHostPort);
+
+	MaxPlayerHintLabel = new TextLabel(this);
+	PortHintLabel = new TextLabel(this);
+	TeamHintLabel = new TextLabel(this);
+
+	MaxPlayerHintLabel->SetStyleColor("color", Colorf::fromRgba8(160, 160, 160));
+	PortHintLabel->SetStyleColor("color", Colorf::fromRgba8(160, 160, 160));
+	TeamHintLabel->SetStyleColor("color", Colorf::fromRgba8(160, 160, 160));
+}
+
+void HostSubPage::SetValues(FStartupSelectionInfo& info) const
+{
+	info.AdditionalNetArgs = "";
+	if (PacketServerCheckbox->GetChecked())
+	{
+		info.AdditionalNetArgs.AppendFormat(" -netmode 1");
+		info.DefaultNetMode = 1;
+	}
+	else if (PeerToPeerCheckbox->GetChecked())
+	{
+		info.AdditionalNetArgs.AppendFormat(" -netmode 0");
+		info.DefaultNetMode = 0;
+	}
+	else
+	{
+		info.DefaultNetMode = -1;
+	}
+
+	info.DefaultNetExtraTic = ExtraTicCheckbox->GetChecked();
+	if (info.DefaultNetExtraTic)
+		info.AdditionalNetArgs.AppendFormat(" -extratic");
+
+	const int dup = TicDupList->GetSelectedItem();
+	if (dup > 0)
+		info.AdditionalNetArgs.AppendFormat(" -dup %d", dup + 1);
+	info.DefaultNetTicDup = dup;
+
+	info.DefaultNetPlayers = clamp(MaxPlayersEdit->GetTextInt(), 1, MAXPLAYERS);
+	info.AdditionalNetArgs.AppendFormat(" -host %d", info.DefaultNetPlayers);
+	const int port = clamp(PortEdit->GetTextInt(), 0, UINT16_MAX);
+	if (port > 0)
+	{
+		info.AdditionalNetArgs.AppendFormat(" -port %d", port);
+		info.DefaultNetHostPort = port;
+	}
+	else
+	{
+		info.DefaultNetHostPort = 0;
+	}
+
+	info.DefaultNetAltDM = AltDeathmatchCheckbox->GetChecked();
+	if (CoopCheckbox->GetChecked())
+	{
+		info.AdditionalNetArgs.AppendFormat(" -coop");
+		info.DefaultNetGameMode = 0;
+	}
+	else if (DeathmatchCheckbox->GetChecked())
+	{
+		if (AltDeathmatchCheckbox->GetChecked())
+			info.AdditionalNetArgs.AppendFormat(" -altdeath");
+		else
+			info.AdditionalNetArgs.AppendFormat(" -deathmatch");
+
+		info.DefaultNetGameMode = 1;
+	}
+	else if (TeamDeathmatchCheckbox->GetChecked())
+	{
+		if (AltDeathmatchCheckbox->GetChecked())
+			info.AdditionalNetArgs.AppendFormat(" -altdeath");
+		else
+			info.AdditionalNetArgs.AppendFormat(" -deathmatch");
+		info.AdditionalNetArgs.AppendFormat(" +teamplay 1");
+		info.DefaultNetGameMode = 2;
+
+		int team = 255;
+		if (!TeamEdit->GetText().empty())
+		{
+			team = TeamEdit->GetTextInt();
+			if (team < 0 || team > 255)
+				team = 255;
+		}
+		info.AdditionalNetArgs.AppendFormat(" +team %d", team);
+		info.DefaultNetHostTeam = team;
+	}
+	else
+	{
+		info.DefaultNetGameMode = -1;
+	}
+}
+
+void HostSubPage::UpdateLanguage()
+{
+	NetModesLabel->SetText(GStrings.GetString("PICKER_NETMODE"));
+	AutoNetmodeCheckbox->SetText(GStrings.GetString("PICKER_NETAUTO"));
+	PacketServerCheckbox->SetText(GStrings.GetString("PICKER_NETSERVER"));
+	PeerToPeerCheckbox->SetText(GStrings.GetString("PICKER_NETPEER"));
+
+	TicDupLabel->SetText(GStrings.GetString("PICKER_NETRATE"));
+	ExtraTicCheckbox->SetText(GStrings.GetString("PICKER_NETBACKUP"));
+
+	GameModesLabel->SetText(GStrings.GetString("PICKER_GAMEMODE"));
+	CoopCheckbox->SetText(GStrings.GetString("PICKER_COOP"));
+	DeathmatchCheckbox->SetText(GStrings.GetString("PICKER_DM"));
+	AltDeathmatchCheckbox->SetText(GStrings.GetString("PICKER_ALTDM"));
+	TeamDeathmatchCheckbox->SetText(GStrings.GetString("PICKER_TDM"));
+	TeamLabel->SetText(GStrings.GetString("PICKER_TEAM"));
+
+	MaxPlayersLabel->SetText(GStrings.GetString("PICKER_PLAYERS"));
+	PortLabel->SetText(GStrings.GetString("PICKER_PORT"));
+
+	MaxPlayerHintLabel->SetText(GStrings.GetString("PICKER_PLAYERHINT"));
+	PortHintLabel->SetText(GStrings.GetString("PICKER_PORTHINT"));
+	TeamHintLabel->SetText(GStrings.GetString("PICKER_TEAMHINT"));
+}
+
+void HostSubPage::OnGeometryChanged()
+{
+	const double w = GetWidth();
+	const double h = GetHeight();
+
+	constexpr double LabelOfsSize = 90.0;
+	constexpr double hintOfs = 170.0;
+
+	double y = 0.0;
+
+	MaxPlayersLabel->SetFrameGeometry(0.0, y, LabelOfsSize, MaxPlayersLabel->GetPreferredHeight());
+	MaxPlayersEdit->SetFrameGeometry(MaxPlayersLabel->GetWidth(), y, 30.0, EditHeight);
+	y += EditHeight + 2.0;
+
+	PortLabel->SetFrameGeometry(0.0, y, LabelOfsSize, PortLabel->GetPreferredHeight());
+	PortEdit->SetFrameGeometry(PortLabel->GetWidth(), y, 60.0, EditHeight);
+
+	MaxPlayerHintLabel->SetFrameGeometry(hintOfs, 0.0, w - hintOfs, MaxPlayerHintLabel->GetPreferredHeight());
+	PortHintLabel->SetFrameGeometry(hintOfs, y, w - hintOfs, PortHintLabel->GetPreferredHeight());
+
+	y += EditHeight + 7.0;
+
+	const double optionsTop = y;
+	TicDupLabel->SetFrameGeometry(0.0, y, 100.0, TicDupLabel->GetPreferredHeight());
+	y += TicDupLabel->GetPreferredHeight();
+	TicDupList->SetFrameGeometry(0.0, y, 100.0, (TicDupList->GetItemAmount() + 1) * 20.0);
+	y += TicDupList->GetHeight() + ExtraTicCheckbox->GetPreferredHeight() + 2.0;
+
+	ExtraTicCheckbox->SetFrameGeometry(0.0, y, w, ExtraTicCheckbox->GetPreferredHeight());
+	y += ExtraTicCheckbox->GetPreferredHeight();
+
+	const double optionsBottom = y;
+	y = optionsTop;
+
+	constexpr double NetModeXOfs = 115.0;
+	NetModesLabel->SetFrameGeometry(NetModeXOfs, y, w - NetModeXOfs, NetModesLabel->GetPreferredHeight());
+	y += NetModesLabel->GetPreferredHeight();
+
+	AutoNetmodeCheckbox->SetFrameGeometry(NetModeXOfs, y, w - NetModeXOfs, AutoNetmodeCheckbox->GetPreferredHeight());
+	y += AutoNetmodeCheckbox->GetPreferredHeight();
+
+	PacketServerCheckbox->SetFrameGeometry(NetModeXOfs, y, w - NetModeXOfs, PacketServerCheckbox->GetPreferredHeight());
+	y += PacketServerCheckbox->GetPreferredHeight();
+
+	PeerToPeerCheckbox->SetFrameGeometry(NetModeXOfs, y, w - NetModeXOfs, PeerToPeerCheckbox->GetPreferredHeight());
+	y += PeerToPeerCheckbox->GetPreferredHeight();
+
+	y = max(optionsBottom, y) + 10.0;
+	GameModesLabel->SetFrameGeometry(0.0, y, w, GameModesLabel->GetPreferredHeight());
+	y += GameModesLabel->GetPreferredHeight();
+
+	CoopCheckbox->SetFrameGeometry(0.0, y, w, CoopCheckbox->GetPreferredHeight());
+	y += CoopCheckbox->GetPreferredHeight();
+
+	DeathmatchCheckbox->SetFrameGeometry(0.0, y, w, DeathmatchCheckbox->GetPreferredHeight());
+	y += DeathmatchCheckbox->GetPreferredHeight();
+
+	TeamDeathmatchCheckbox->SetFrameGeometry(0.0, y, w, TeamDeathmatchCheckbox->GetPreferredHeight());
+	y += TeamDeathmatchCheckbox->GetPreferredHeight() + 2.0;
+
+	TeamLabel->SetFrameGeometry(14.0, y, LabelOfsSize - 14.0, TeamLabel->GetPreferredHeight());
+	TeamEdit->SetFrameGeometry(TeamLabel->GetWidth() + 14.0, y, 45.0, EditHeight);
+	TeamHintLabel->SetFrameGeometry(hintOfs, y, w - hintOfs, TeamHintLabel->GetPreferredHeight());
+	y += EditHeight + 2.0;
+
+	AltDeathmatchCheckbox->SetFrameGeometry(0.0, y, w, AltDeathmatchCheckbox->GetPreferredHeight());
+
+	MainTab->UpdatePlayButton();
+}
+
+JoinSubPage::JoinSubPage(NetworkPage* main, const FStartupSelectionInfo& info) : Widget(nullptr), MainTab(main)
+{
+	AddressEdit = new LineEdit(this);
+	AddressPortEdit = new LineEdit(this);
+	AddressLabel = new TextLabel(this);
+	AddressPortLabel = new TextLabel(this);
+
+	AddressEdit->SetText(info.DefaultNetAddress.GetChars());
+	AddressPortEdit->SetMaxLength(5);
+	AddressPortEdit->SetNumericMode(true);
+	if (info.DefaultNetJoinPort > 0)
+		AddressPortEdit->SetTextInt(info.DefaultNetJoinPort);
+
+	TeamDeathmatchLabel = new TextLabel(this);
+	TeamLabel = new TextLabel(this);
+	TeamEdit = new LineEdit(this);
+
+	TeamEdit->SetMaxLength(3);
+	TeamEdit->SetNumericMode(true);
+	TeamEdit->SetTextInt(info.DefaultNetJoinTeam);
+
+	AddressPortHintLabel = new TextLabel(this);
+	TeamHintLabel = new TextLabel(this);
+
+	AddressPortHintLabel->SetStyleColor("color", Colorf::fromRgba8(160, 160, 160));
+	TeamHintLabel->SetStyleColor("color", Colorf::fromRgba8(160, 160, 160));
+}
+
+void JoinSubPage::SetValues(FStartupSelectionInfo& info) const
+{
+	FString addr = AddressEdit->GetText();
+	info.DefaultNetAddress = addr;
+	const int port = clamp(AddressPortEdit->GetTextInt(), 0, UINT16_MAX);
+	if (port > 0)
+	{
+		addr.AppendFormat(":%d", port);
+		info.DefaultNetJoinPort = port;
+	}
+	else
+	{
+		info.DefaultNetJoinPort = 0;
+	}
+
+	info.AdditionalNetArgs = "";
+	info.AdditionalNetArgs.AppendFormat(" -join %s", addr.GetChars());
+
+	int team = 255;
+	if (!TeamEdit->GetText().empty())
+	{
+		team = TeamEdit->GetTextInt();
+		if (team < 0 || team > 255)
+			team = 255;
+	}
+	info.AdditionalNetArgs.AppendFormat(" +team %d", team);
+	info.DefaultNetJoinTeam = team;
+}
+
+void JoinSubPage::UpdateLanguage()
+{
+	AddressLabel->SetText(GStrings.GetString("PICKER_IP"));
+	AddressPortLabel->SetText(GStrings.GetString("PICKER_PORT"));
+
+	TeamDeathmatchLabel->SetText(GStrings.GetString("PICKER_TDMLABEL"));
+	TeamLabel->SetText(GStrings.GetString("PICKER_TEAM"));
+
+	AddressPortHintLabel->SetText(GStrings.GetString("PICKER_PORTHINT"));
+	TeamHintLabel->SetText(GStrings.GetString("PICKER_TEAMHINT"));
+}
+
+void JoinSubPage::OnGeometryChanged()
+{
+	const double w = GetWidth();
+	const double h = GetHeight();
+
+	constexpr double LabelOfsSize = 90.0;
+	constexpr double hintOfs = 170.0;
+
+	double y = 0.0;
+
+	AddressLabel->SetFrameGeometry(0.0, y, LabelOfsSize, AddressLabel->GetPreferredHeight());
+	AddressEdit->SetFrameGeometry(AddressLabel->GetWidth(), y, 120.0, EditHeight);
+	y += EditHeight + 2.0;
+
+	AddressPortLabel->SetFrameGeometry(0.0, y, LabelOfsSize, AddressPortLabel->GetPreferredHeight());
+	AddressPortEdit->SetFrameGeometry(AddressPortLabel->GetWidth(), y, 60.0, EditHeight);
+
+	AddressPortHintLabel->SetFrameGeometry(hintOfs, y, w - hintOfs, AddressPortHintLabel->GetPreferredHeight());
+	y += EditHeight + 12.0;
+
+	TeamDeathmatchLabel->SetFrameGeometry(0.0, y, w, TeamDeathmatchLabel->GetPreferredHeight());
+	y += TeamDeathmatchLabel->GetPreferredHeight();
+
+	TeamLabel->SetFrameGeometry(0.0, y, LabelOfsSize, TeamLabel->GetPreferredHeight());
+	TeamEdit->SetFrameGeometry(TeamLabel->GetWidth(), y, 45.0, EditHeight);
+	TeamHintLabel->SetFrameGeometry(hintOfs, y, w - hintOfs, TeamHintLabel->GetPreferredHeight());
+
+	MainTab->UpdatePlayButton();
+}
diff --git a/src/launcher/networkpage.h b/src/launcher/networkpage.h
new file mode 100644
index 000000000..23cce71cd
--- /dev/null
+++ b/src/launcher/networkpage.h
@@ -0,0 +1,112 @@
+#pragma once
+
+#include 
+#include "gstrings.h"
+
+class LauncherWindow;
+class CheckboxLabel;
+class LineEdit;
+class ListView;
+class TextLabel;
+class PushButton;
+class TabWidget;
+struct WadStuff;
+struct FStartupSelectionInfo;
+
+class HostSubPage;
+class JoinSubPage;
+
+class NetworkPage : public Widget
+{
+public:
+	NetworkPage(LauncherWindow* launcher, const FStartupSelectionInfo& info);
+	void UpdateLanguage();
+	void UpdatePlayButton();
+	bool IsInHost() const;
+	void SetValues(FStartupSelectionInfo& info) const;
+	void InitializeTabs(const FStartupSelectionInfo& info);
+
+private:
+	void OnGeometryChanged() override;
+	void OnSetFocus() override;
+	void OnIWADsListActivated();
+
+	LauncherWindow* Launcher = nullptr;
+
+	// Direct hook into the play page for these so their editing is synchronized.
+	LineEdit* ParametersEdit = nullptr;
+	ListView* IWADsList = nullptr;
+	TextLabel* ParametersLabel = nullptr;
+
+	HostSubPage* HostPage = nullptr;
+	JoinSubPage* JoinPage = nullptr;
+	TabWidget* StartPages = nullptr;
+	
+	LineEdit* SaveFileEdit = nullptr;
+	TextLabel* SaveFileLabel = nullptr;
+	CheckboxLabel* SaveFileCheckbox = nullptr;
+	CheckboxLabel* SaveArgsCheckbox = nullptr;
+};
+
+class HostSubPage : public Widget
+{
+public:
+	HostSubPage(NetworkPage* main, const FStartupSelectionInfo& info);
+	void UpdateLanguage();
+	void SetValues(FStartupSelectionInfo& info) const;
+
+private:
+	void OnGeometryChanged() override;
+
+	NetworkPage* MainTab = nullptr;
+
+	TextLabel* NetModesLabel = nullptr;
+	CheckboxLabel* AutoNetmodeCheckbox = nullptr;
+	CheckboxLabel* PeerToPeerCheckbox = nullptr;
+	CheckboxLabel* PacketServerCheckbox = nullptr;
+	ListView* TicDupList = nullptr;
+	TextLabel* TicDupLabel = nullptr;
+	CheckboxLabel* ExtraTicCheckbox = nullptr;
+
+	TextLabel* GameModesLabel = nullptr;
+	CheckboxLabel* CoopCheckbox = nullptr;
+	CheckboxLabel* DeathmatchCheckbox = nullptr;
+	CheckboxLabel* AltDeathmatchCheckbox = nullptr;
+	CheckboxLabel* TeamDeathmatchCheckbox = nullptr;
+	TextLabel* TeamLabel = nullptr;
+	LineEdit* TeamEdit = nullptr;
+
+	LineEdit* MaxPlayersEdit = nullptr;
+	LineEdit* PortEdit = nullptr;
+	TextLabel* MaxPlayersLabel = nullptr;
+	TextLabel* PortLabel = nullptr;
+
+	TextLabel* MaxPlayerHintLabel = nullptr;
+	TextLabel* PortHintLabel = nullptr;
+	TextLabel* TeamHintLabel = nullptr;
+};
+
+class JoinSubPage : public Widget
+{
+public:
+	JoinSubPage(NetworkPage* main, const FStartupSelectionInfo& info);
+	void UpdateLanguage();
+	void SetValues(FStartupSelectionInfo& info) const;
+
+private:
+	void OnGeometryChanged() override;
+
+	NetworkPage* MainTab = nullptr;
+
+	LineEdit* AddressEdit = nullptr;
+	LineEdit* AddressPortEdit = nullptr;
+	TextLabel* AddressLabel = nullptr;
+	TextLabel* AddressPortLabel = nullptr;
+
+	TextLabel* TeamDeathmatchLabel = nullptr;
+	TextLabel* TeamLabel = nullptr;
+	LineEdit* TeamEdit = nullptr;
+
+	TextLabel* AddressPortHintLabel = nullptr;
+	TextLabel* TeamHintLabel = nullptr;
+};
diff --git a/src/launcher/playgamepage.cpp b/src/launcher/playgamepage.cpp
index 53676b94e..bdfb2309d 100644
--- a/src/launcher/playgamepage.cpp
+++ b/src/launcher/playgamepage.cpp
@@ -7,8 +7,9 @@
 #include 
 #include 
 #include 
+#include 
 
-PlayGamePage::PlayGamePage(LauncherWindow* launcher, WadStuff* wads, int numwads, int defaultiwad) : Widget(nullptr), Launcher(launcher)
+PlayGamePage::PlayGamePage(LauncherWindow* launcher, const FStartupSelectionInfo& info) : Widget(nullptr), Launcher(launcher)
 {
 	WelcomeLabel = new TextLabel(this);
 	VersionLabel = new TextLabel(this);
@@ -17,44 +18,43 @@ PlayGamePage::PlayGamePage(LauncherWindow* launcher, WadStuff* wads, int numwads
 	ParametersLabel = new TextLabel(this);
 	GamesList = new ListView(this);
 	ParametersEdit = new LineEdit(this);
+	SaveArgsCheckbox = new CheckboxLabel(this);
 
-	for (int i = 0; i < numwads; i++)
+	SaveArgsCheckbox->SetChecked(info.bSaveArgs);
+	if (!info.DefaultArgs.IsEmpty())
+		ParametersEdit->SetText(info.DefaultArgs.GetChars());
+
+	for (const auto& wad : *info.Wads)
 	{
-		const char* filepart = strrchr(wads[i].Path.GetChars(), '/');
-		if (filepart == NULL)
-			filepart = wads[i].Path.GetChars();
+		const char* filepart = strrchr(wad.Path.GetChars(), '/');
+		if (filepart == nullptr)
+			filepart = wad.Path.GetChars();
 		else
-			filepart++;
+			++filepart;
 
 		FString work;
-		if (*filepart) work.Format("%s (%s)", wads[i].Name.GetChars(), filepart);
-		else work = wads[i].Name.GetChars();
+		if (*filepart)
+			work.Format("%s (%s)", wad.Name.GetChars(), filepart);
+		else
+			work = wad.Name.GetChars();
 
 		GamesList->AddItem(work.GetChars());
 	}
 
-	if (defaultiwad >= 0 && defaultiwad < numwads)
+	if (info.DefaultIWAD >= 0 && info.DefaultIWAD < info.Wads->Size())
 	{
-		GamesList->SetSelectedItem(defaultiwad);
-		GamesList->ScrollToItem(defaultiwad);
+		GamesList->SetSelectedItem(info.DefaultIWAD);
+		GamesList->ScrollToItem(info.DefaultIWAD);
 	}
 
 	GamesList->OnActivated = [=]() { OnGamesListActivated(); };
 }
 
-void PlayGamePage::SetExtraArgs(const std::string& args)
+void PlayGamePage::SetValues(FStartupSelectionInfo& info) const
 {
-	ParametersEdit->SetText(args);
-}
-
-std::string PlayGamePage::GetExtraArgs()
-{
-	return ParametersEdit->GetText();
-}
-
-int PlayGamePage::GetSelectedGame()
-{
-	return GamesList->GetSelectedItem();
+	info.DefaultIWAD = GamesList->GetSelectedItem();
+	info.DefaultArgs = ParametersEdit->GetText();
+	info.bSaveArgs = SaveArgsCheckbox->GetChecked();
 }
 
 void PlayGamePage::UpdateLanguage()
@@ -67,6 +67,7 @@ void PlayGamePage::UpdateLanguage()
 	FString versionText = GStrings.GetString("PICKER_VERSION");
 	versionText.Substitute("%s", GetVersionString());
 	VersionLabel->SetText(versionText.GetChars());
+	SaveArgsCheckbox->SetText(GStrings.GetString("PICKER_REMPARM"));
 }
 
 void PlayGamePage::OnGamesListActivated()
@@ -95,12 +96,13 @@ void PlayGamePage::OnGeometryChanged()
 
 	double listViewTop = y;
 
-	y = GetHeight() - 10.0;
+	y = GetHeight() - 10.0 - SaveArgsCheckbox->GetPreferredHeight();
+	SaveArgsCheckbox->SetFrameGeometry(0.0, y, GetWidth(), SaveArgsCheckbox->GetPreferredHeight());
+	y -= 2.0;
 
 	double editHeight = 24.0;
 	y -= editHeight;
 	ParametersEdit->SetFrameGeometry(0.0, y, GetWidth(), editHeight);
-	y -= 5.0;
 
 	double labelHeight = ParametersLabel->GetPreferredHeight();
 	y -= labelHeight;
@@ -109,4 +111,6 @@ void PlayGamePage::OnGeometryChanged()
 
 	double listViewBottom = y - 10.0;
 	GamesList->SetFrameGeometry(0.0, listViewTop, GetWidth(), std::max(listViewBottom - listViewTop, 0.0));
+
+	Launcher->UpdatePlayButton();
 }
diff --git a/src/launcher/playgamepage.h b/src/launcher/playgamepage.h
index dd3bd170a..a41b8f5d4 100644
--- a/src/launcher/playgamepage.h
+++ b/src/launcher/playgamepage.h
@@ -6,18 +6,16 @@ class LauncherWindow;
 class TextLabel;
 class ListView;
 class LineEdit;
+class CheckboxLabel;
 struct WadStuff;
+struct FStartupSelectionInfo;
 
 class PlayGamePage : public Widget
 {
 public:
-	PlayGamePage(LauncherWindow* launcher, WadStuff* wads, int numwads, int defaultiwad);
+	PlayGamePage(LauncherWindow* launcher, const FStartupSelectionInfo& info);
 	void UpdateLanguage();
-
-	void SetExtraArgs(const std::string& args);
-	std::string GetExtraArgs();
-
-	int GetSelectedGame();
+	void SetValues(FStartupSelectionInfo& info) const;
 
 private:
 	void OnGeometryChanged() override;
@@ -32,4 +30,5 @@ private:
 	TextLabel* ParametersLabel = nullptr;
 	ListView* GamesList = nullptr;
 	LineEdit* ParametersEdit = nullptr;
+	CheckboxLabel* SaveArgsCheckbox = nullptr;
 };
diff --git a/src/launcher/settingspage.cpp b/src/launcher/settingspage.cpp
index 01b6817df..14b9f2db0 100644
--- a/src/launcher/settingspage.cpp
+++ b/src/launcher/settingspage.cpp
@@ -9,14 +9,7 @@
 #include 
 #include 
 
-#ifdef RENDER_BACKENDS
-EXTERN_CVAR(Int, vid_preferbackend);
-#endif
-
-EXTERN_CVAR(String, language)
-EXTERN_CVAR(Bool, queryiwad);
-
-SettingsPage::SettingsPage(LauncherWindow* launcher, int* autoloadflags) : Widget(nullptr), Launcher(launcher), AutoloadFlags(autoloadflags)
+SettingsPage::SettingsPage(LauncherWindow* launcher, const FStartupSelectionInfo& info) : Widget(nullptr), Launcher(launcher)
 {
 	LangLabel = new TextLabel(this);
 	GeneralLabel = new TextLabel(this);
@@ -28,30 +21,27 @@ SettingsPage::SettingsPage(LauncherWindow* launcher, int* autoloadflags) : Widge
 	BrightmapsCheckbox = new CheckboxLabel(this);
 	WidescreenCheckbox = new CheckboxLabel(this);
 
+	FullscreenCheckbox->SetChecked(info.DefaultFullscreen);
+	DontAskAgainCheckbox->SetChecked(!info.DefaultQueryIWAD);
+
+	DisableAutoloadCheckbox->SetChecked(info.DefaultStartFlags & 1);
+	LightsCheckbox->SetChecked(info.DefaultStartFlags & 2);
+	BrightmapsCheckbox->SetChecked(info.DefaultStartFlags & 4);
+	WidescreenCheckbox->SetChecked(info.DefaultStartFlags & 8);
+
 #ifdef RENDER_BACKENDS
 	BackendLabel = new TextLabel(this);
 	VulkanCheckbox = new CheckboxLabel(this);
 	OpenGLCheckbox = new CheckboxLabel(this);
 	GLESCheckbox = new CheckboxLabel(this);
-#endif
 
-	FullscreenCheckbox->SetChecked(vid_fullscreen);
-	DontAskAgainCheckbox->SetChecked(!queryiwad);
-
-	int flags = *autoloadflags;
-	DisableAutoloadCheckbox->SetChecked(flags & 1);
-	LightsCheckbox->SetChecked(flags & 2);
-	BrightmapsCheckbox->SetChecked(flags & 4);
-	WidescreenCheckbox->SetChecked(flags & 8);
-
-#ifdef RENDER_BACKENDS
 	OpenGLCheckbox->SetRadioStyle(true);
 	VulkanCheckbox->SetRadioStyle(true);
 	GLESCheckbox->SetRadioStyle(true);
 	OpenGLCheckbox->FuncChanged = [this](bool on) { if (on) { VulkanCheckbox->SetChecked(false); GLESCheckbox->SetChecked(false); }};
 	VulkanCheckbox->FuncChanged = [this](bool on) { if (on) { OpenGLCheckbox->SetChecked(false); GLESCheckbox->SetChecked(false); }};
 	GLESCheckbox->FuncChanged = [this](bool on) { if (on) { VulkanCheckbox->SetChecked(false); OpenGLCheckbox->SetChecked(false); }};
-	switch (vid_preferbackend)
+	switch (info.DefaultBackend)
 	{
 	case 0:
 		OpenGLCheckbox->SetChecked(true);
@@ -101,32 +91,33 @@ SettingsPage::SettingsPage(LauncherWindow* launcher, int* autoloadflags) : Widge
 	for (auto& l : languages)
 	{
 		LangList->AddItem(l.second.GetChars());
-		if (!l.first.CompareNoCase(::language))
+		if (!l.first.CompareNoCase(info.DefaultLanguage))
 			LangList->SetSelectedItem(i);
-		i++;
+		++i;
 	}
 
 	LangList->OnChanged = [=](int i) { OnLanguageChanged(i); };
 }
 
-void SettingsPage::Save()
+void SettingsPage::SetValues(FStartupSelectionInfo& info) const
 {
-	vid_fullscreen = FullscreenCheckbox->GetChecked();
-	queryiwad = !DontAskAgainCheckbox->GetChecked();
+	info.DefaultFullscreen = FullscreenCheckbox->GetChecked();
+	info.DefaultQueryIWAD = !DontAskAgainCheckbox->GetChecked();
+	info.DefaultLanguage = languages[LangList->GetSelectedItem()].first.GetChars();
 
 	int flags = 0;
 	if (DisableAutoloadCheckbox->GetChecked()) flags |= 1;
 	if (LightsCheckbox->GetChecked()) flags |= 2;
 	if (BrightmapsCheckbox->GetChecked()) flags |= 4;
 	if (WidescreenCheckbox->GetChecked()) flags |= 8;
-	*AutoloadFlags = flags;
+	info.DefaultStartFlags = flags;
 
 #ifdef RENDER_BACKENDS
 	int v = 1;
 	if (OpenGLCheckbox->GetChecked()) v = 0;
 	else if (VulkanCheckbox->GetChecked()) v = 1;
 	else if (GLESCheckbox->GetChecked()) v = 2;
-	if (v != vid_preferbackend) vid_preferbackend = v;
+	info.DefaultBackend = v;
 #endif
 }
 
@@ -152,8 +143,7 @@ void SettingsPage::UpdateLanguage()
 
 void SettingsPage::OnLanguageChanged(int i)
 {
-	::language = languages[i].first.GetChars();
-	GStrings.UpdateLanguage(::language); // CVAR callbacks are not active yet.
+	GStrings.UpdateLanguage(languages[i].first.GetChars());
 	UpdateLanguage();
 	Update();
 	Launcher->UpdateLanguage();
@@ -204,4 +194,6 @@ void SettingsPage::OnGeometryChanged()
 		y += LangLabel->GetPreferredHeight();
 		LangList->SetFrameGeometry(0.0, y, w, std::max(h - y, 0.0));
 	}
+
+	Launcher->UpdatePlayButton();
 }
diff --git a/src/launcher/settingspage.h b/src/launcher/settingspage.h
index 77a6ff048..1b96a1e6c 100644
--- a/src/launcher/settingspage.h
+++ b/src/launcher/settingspage.h
@@ -3,20 +3,20 @@
 #include 
 #include "gstrings.h"
 
-// #define RENDER_BACKENDS
+#define RENDER_BACKENDS
 
 class LauncherWindow;
 class TextLabel;
 class CheckboxLabel;
 class ListView;
+struct FStartupSelectionInfo;
 
 class SettingsPage : public Widget
 {
 public:
-	SettingsPage(LauncherWindow* launcher, int* autoloadflags);
+	SettingsPage(LauncherWindow* launcher, const FStartupSelectionInfo& info);
 	void UpdateLanguage();
-
-	void Save();
+	void SetValues(FStartupSelectionInfo& info) const;
 
 private:
 	void OnLanguageChanged(int i);
@@ -41,8 +41,6 @@ private:
 #endif
 	ListView* LangList = nullptr;
 
-	int* AutoloadFlags = nullptr;
-
 	TArray> languages;
 	bool hideLanguage = false;
 };

From 17f58f514f7297cfcb7795fb71d8638df508fdbc Mon Sep 17 00:00:00 2001
From: Boondorl 
Date: Sat, 5 Jul 2025 14:54:34 -0400
Subject: [PATCH 253/384] Fixed missing multiplayer tab localization

---
 src/launcher/launcherwindow.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/launcher/launcherwindow.cpp b/src/launcher/launcherwindow.cpp
index e6ab31839..c69b42f93 100644
--- a/src/launcher/launcherwindow.cpp
+++ b/src/launcher/launcherwindow.cpp
@@ -91,7 +91,7 @@ void LauncherWindow::UpdateLanguage()
 {
 	Pages->SetTabText(PlayGame, GStrings.GetString("PICKER_TAB_PLAY"));
 	Pages->SetTabText(Settings, GStrings.GetString("OPTMNU_TITLE"));
-	Pages->SetTabText(Network, "Multiplayer");
+	Pages->SetTabText(Network, GStrings.GetString("PICKER_TAB_MULTI"));
 	PlayGame->UpdateLanguage();
 	Settings->UpdateLanguage();
 	Network->UpdateLanguage();

From 0f72a671fcde63c6b43aa3546b8be0627ae67bbd Mon Sep 17 00:00:00 2001
From: Marcus Minhorst <136136617+the-phinet@users.noreply.github.com>
Date: Sun, 6 Jul 2025 19:06:59 -0400
Subject: [PATCH 254/384] Revert "Gamepad Improvements"

This reverts commit 2a5cce543bfd265f327b796b28904584a8db1e95.
---
 src/common/console/c_bind.cpp                 |   4 +-
 src/common/console/keydef.h                   |   9 +-
 src/common/engine/m_joy.cpp                   | 330 +--------------
 src/common/engine/m_joy.h                     |  45 +-
 src/common/menu/joystickmenu.cpp              |  50 ---
 src/common/menu/menu.cpp                      |   2 +-
 .../platform/posix/cocoa/i_joystick.cpp       | 112 +----
 src/common/platform/posix/sdl/i_input.cpp     | 250 ++++--------
 src/common/platform/posix/sdl/i_input.h       |  13 -
 src/common/platform/posix/sdl/i_joystick.cpp  | 384 ++++--------------
 src/common/platform/win32/i_dijoy.cpp         | 168 +-------
 src/common/platform/win32/i_rawps2.cpp        | 160 +-------
 src/common/platform/win32/i_xinput.cpp        | 167 +-------
 wadsrc/static/menudef.txt                     |   9 -
 .../zscript/engine/ui/menu/joystickmenu.zs    | 106 +----
 wadsrc/static/zscript/engine/ui/menu/menu.zs  |  19 -
 16 files changed, 215 insertions(+), 1613 deletions(-)
 delete mode 100644 src/common/platform/posix/sdl/i_input.h

diff --git a/src/common/console/c_bind.cpp b/src/common/console/c_bind.cpp
index 078a9a2b1..fe794c736 100644
--- a/src/common/console/c_bind.cpp
+++ b/src/common/console/c_bind.cpp
@@ -147,9 +147,7 @@ const char *KeyNames[NUM_KEYS] =
 	"DPadUp","DPadDown","DPadLeft","DPadRight",	// Gamepad buttons
 	"Pad_Start","Pad_Back","LThumb","RThumb",
 	"LShoulder","RShoulder","LTrigger","RTrigger",
-	"Pad_A", "Pad_B", "Pad_X", "Pad_Y",
-	"Paddle_1", "Paddle_2", "Paddle_3", "Paddle_4",
-	"Guide", "Pad_Misc", "Pad_Touchpad"
+	"Pad_A", "Pad_B", "Pad_X", "Pad_Y"
 };
 
 FKeyBindings Bindings;
diff --git a/src/common/console/keydef.h b/src/common/console/keydef.h
index 620b5f5e7..108806774 100644
--- a/src/common/console/keydef.h
+++ b/src/common/console/keydef.h
@@ -134,15 +134,8 @@ enum EKeyCodes
 	KEY_PAD_B				= 0x1C1,
 	KEY_PAD_X				= 0x1C2,
 	KEY_PAD_Y				= 0x1C3,
-	KEY_PAD_PADDLE1			= 0x1C4,
-	KEY_PAD_PADDLE2			= 0x1C5,
-	KEY_PAD_PADDLE3			= 0x1C6,
-	KEY_PAD_PADDLE4			= 0x1C7,
-	KEY_PAD_GUIDE			= 0x1C8,
-	KEY_PAD_MISC1			= 0x1C9,
-	KEY_PAD_TOUCHPAD		= 0x1CA,
 
-	NUM_KEYS				= 0x1CB,
+	NUM_KEYS				= 0x1C4,
 
 	NUM_JOYAXISBUTTONS		= 8,
 };
diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp
index 9df33efe9..1e4b2f3e0 100644
--- a/src/common/engine/m_joy.cpp
+++ b/src/common/engine/m_joy.cpp
@@ -33,11 +33,6 @@
 // HEADER FILES ------------------------------------------------------------
 
 #include 
-#include "c_dispatch.h"
-#include "gain_analysis.h"
-#include "keydef.h"
-#include "name.h"
-#include "tarray.h"
 #include "vectors.h"
 #include "m_joy.h"
 #include "configfile.h"
@@ -45,7 +40,6 @@
 #include "d_eventbase.h"
 #include "cmdlib.h"
 #include "printf.h"
-#include "zstring.h"
 
 // MACROS ------------------------------------------------------------------
 
@@ -63,26 +57,9 @@ EXTERN_CVAR(Bool, joy_ps2raw)
 EXTERN_CVAR(Bool, joy_dinput)
 EXTERN_CVAR(Bool, joy_xinput)
 
-extern const float JOYDEADZONE_DEFAULT = 0.1; // reduced from 0.25
-
-extern const float JOYSENSITIVITY_DEFAULT = 1.0;
-
-extern const float JOYTHRESH_DEFAULT = 0.05;
-extern const float JOYTHRESH_TRIGGER = 0.05;
-extern const float JOYTHRESH_STICK_X = 0.65;
-extern const float JOYTHRESH_STICK_Y = 0.35;
-
-extern const CubicBezier JOYCURVE[NUM_JOYCURVE] = {
-	{{0.3, 0.0, 0.7, 0.4}}, // DEFAULT -> QUADRATIC
-
-	{{0.0, 0.0, 1.0, 1.0}}, // LINEAR
-	{{0.3, 0.0, 0.7, 0.4}}, // QUADRATIC
-	{{0.5, 0.0, 0.7, 0.2}}, // CUBIC
-};
-
 // PUBLIC DATA DEFINITIONS -------------------------------------------------
 
-CUSTOM_CVARD(Bool, use_joystick, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present")
+CUSTOM_CVARD(Bool, use_joystick, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present") 
 {
 #ifdef _WIN32
 	joy_ps2raw->Callback();
@@ -186,48 +163,6 @@ bool M_LoadJoystickConfig(IJoystickConfig *joy)
 			joy->SetAxisScale(i, (float)atof(value));
 		}
 
-		mysnprintf(key + axislen, countof(key) - axislen, "threshold");
-		value = GameConfig->GetValueForKey(key);
-		if (value)
-		{
-			joy->SetAxisDigitalThreshold(i, (float)atof(value));
-		}
-
-		mysnprintf(key + axislen, countof(key) - axislen, "curve");
-		value = GameConfig->GetValueForKey(key);
-		if (value)
-		{
-			joy->SetAxisResponseCurve(i, (EJoyCurve)clamp(atoi(value), (int)JOYCURVE_CUSTOM, (int)NUM_JOYCURVE-1));
-		}
-
-		mysnprintf(key + axislen, countof(key) - axislen, "curve-x1");
-		value = GameConfig->GetValueForKey(key);
-		if (value)
-		{
-			joy->SetAxisResponseCurvePoint(i, 0, (float)atof(value));
-		}
-
-		mysnprintf(key + axislen, countof(key) - axislen, "curve-y1");
-		value = GameConfig->GetValueForKey(key);
-		if (value)
-		{
-			joy->SetAxisResponseCurvePoint(i, 1, (float)atof(value));
-		}
-
-		mysnprintf(key + axislen, countof(key) - axislen, "curve-x2");
-		value = GameConfig->GetValueForKey(key);
-		if (value)
-		{
-			joy->SetAxisResponseCurvePoint(i, 2, (float)atof(value));
-		}
-
-		mysnprintf(key + axislen, countof(key) - axislen, "curve-y2");
-		value = GameConfig->GetValueForKey(key);
-		if (value)
-		{
-			joy->SetAxisResponseCurvePoint(i, 3, (float)atof(value));
-		}
-
 		mysnprintf(key + axislen, countof(key) - axislen, "map");
 		value = GameConfig->GetValueForKey(key);
 		if (value)
@@ -292,33 +227,6 @@ void M_SaveJoystickConfig(IJoystickConfig *joy)
 				mysnprintf(value, countof(value), "%g", joy->GetAxisScale(i));
 				GameConfig->SetValueForKey(key, value);
 			}
-			if (!joy->IsAxisDigitalThresholdDefault(i))
-			{
-				mysnprintf(key + axislen, countof(key) - axislen, "threshold");
-				mysnprintf(value, countof(value), "%g", joy->GetAxisDigitalThreshold(i));
-				GameConfig->SetValueForKey(key, value);
-			}
-			if (!joy->IsAxisResponseCurveDefault(i))
-			{
-				mysnprintf(key + axislen, countof(key) - axislen, "curve");
-				mysnprintf(value, countof(value), "%d", joy->GetAxisResponseCurve(i));
-				GameConfig->SetValueForKey(key, value);
-			}
-			if (joy->GetAxisResponseCurve(i) == JOYCURVE_CUSTOM)
-			{
-				mysnprintf(key + axislen, countof(key) - axislen, "curve-x1");
-				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 0));
-				GameConfig->SetValueForKey(key, value);
-				mysnprintf(key + axislen, countof(key) - axislen, "curve-y1");
-				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 1));
-				GameConfig->SetValueForKey(key, value);
-				mysnprintf(key + axislen, countof(key) - axislen, "curve-x2");
-				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 2));
-				GameConfig->SetValueForKey(key, value);
-				mysnprintf(key + axislen, countof(key) - axislen, "curve-y2");
-				mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 3));
-				GameConfig->SetValueForKey(key, value);
-			}
 			if (!joy->IsAxisMapDefault(i))
 			{
 				mysnprintf(key + axislen, countof(key) - axislen, "map");
@@ -335,174 +243,6 @@ void M_SaveJoystickConfig(IJoystickConfig *joy)
 	}
 }
 
-CCMD (gamepad)
-{
-	int COMMAND = 1, IDENTIFIER = 2, VALUE = 3;
-	int argc = argv.argc()-1;
-
-	TArray sticks;
-	I_GetJoysticks(sticks);
-
-	auto usage = []()
-	{
-		Printf(
-			"usage:"
-			"\n\tgamepad list"
-			"\n\tgamepad reset       pad"
-			"\n\tgamepad enabled     pad      [0|1]"
-			"\n\tgamepad background  pad      [0|1]"
-			"\n\tgamepad sensitivity pad      [float]"
-			"\n\tgamepad deadzone    pad.axis [float]"
-			"\n\tgamepad scale       pad.axis [float]"
-			"\n\tgamepad threshold   pad.axis [float]"
-			"\n\tgamepad curve       pad.axis [-1|0|1|2|3]"
-			"\n\tgamepad curve-x1    pad.axis [float]"
-			"\n\tgamepad curve-y1    pad.axis [float]"
-			"\n\tgamepad curve-x2    pad.axis [float]"
-			"\n\tgamepad curve-y2    pad.axis [float]"
-			"\n\tgamepad map         pad.axis [-1|0|1|2|3|4]"
-			"\n"
-		);
-	};
-
-	if (argc < COMMAND)
-	{
-		return usage();
-	};
-
-	FName command = argv[COMMAND];
-
-	if (argc < IDENTIFIER)
-	{
-		if (command == "list")
-		{
-			for (int i = 0; i < sticks.SSize(); i++)
-			{
-				Printf("%d: '%s'\n", i, sticks[i]->GetName().GetChars());
-				for (int j = 0; j < sticks[i]->GetNumAxes(); j++)
-				{
-					Printf("  %d.%d: '%s'\n", i, j, sticks[i]->GetAxisName(j));
-				}
-			}
-			return;
-		}
-		return usage();
-	}
-
-	const char * id = argv[IDENTIFIER];
-	const char * hasAxis = strchr(id, '.');
-
-	int pad, axis;
-
-	try {
-		pad = (int)std::stod(id);
-
-		if (pad < 0 || pad >= sticks.SSize())
-		{
-			return (void) Printf("Pad # out of range\n");
-		}
-	} catch (...) {
-		return (void) Printf("Failed to parse pad #\n");
-	}
-
-	if (hasAxis)
-	{
-		try {
-			axis = (int)std::stod(hasAxis+1);
-
-			if (axis < 0 || axis >= sticks[pad]->GetNumAxes())
-			{
-				return (void) Printf("Axis # out of range\n");
-			}
-		} catch (...) {
-			return (void) Printf("Failed to parse axis #\n");
-		}
-	}
-
-	float value = 0;
-	bool set = argc >= VALUE;
-
-	if (set)
-	{
-		try {
-			value = std::stod(argv[VALUE]);
-		} catch (...) {
-			return (void) Printf("Failed to parse args\n");
-		}
-	}
-
-	if (command == "reset")
-	{
-		if (set) return usage();
-		sticks[pad]->SetDefaultConfig();
-		sticks[pad]->SetEnabled(true);
-		sticks[pad]->SetEnabledInBackground(sticks[pad]->AllowsEnabledInBackground());
-		sticks[pad]->SetSensitivity(1);
-		return;
-	}
-	if (command == "enabled")
-	{
-		if (set) sticks[pad]->SetEnabled((int)value);
-		return (void) Printf("%d\n", sticks[pad]->GetEnabled());
-	}
-	if (command == "background")
-	{
-		if (set) sticks[pad]->SetEnabledInBackground((int)value);
-		return (void) Printf("%d\n", sticks[pad]->GetEnabledInBackground());
-	}
-	if (command == "sensitivity")
-	{
-		if (set) sticks[pad]->SetSensitivity(value);
-		return (void) Printf("%g\n", sticks[pad]->GetSensitivity());
-	}
-	if (command == "deadzone")
-	{
-		if (set) sticks[pad]->SetAxisDeadZone(axis, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisDeadZone(axis));
-	}
-	if (command == "scale")
-	{
-		if (set) sticks[pad]->SetAxisScale(axis, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisScale(axis));
-	}
-	if (command == "threshold")
-	{
-		if (set) sticks[pad]->SetAxisDigitalThreshold(axis, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisDigitalThreshold(axis));
-	}
-	if (command == "curve")
-	{
-		if (set) sticks[pad]->SetAxisResponseCurve(axis, (EJoyCurve)value);
-		return (void) Printf("%d\n", sticks[pad]->GetAxisResponseCurve(axis));
-	}
-	if (command == "curve-x1")
-	{
-		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 0, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 0));
-	}
-	if (command == "curve-y1")
-	{
-		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 1, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 1));
-	}
-	if (command == "curve-x2")
-	{
-		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 2, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 2));
-	}
-	if (command == "curve-y2")
-	{
-		if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 3, value);
-		return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 3));
-	}
-	if (command == "map")
-	{
-		if (set) sticks[pad]->SetAxisMap(axis, (EJoyAxis)value);
-		return (void) Printf("%d\n", sticks[pad]->GetAxisMap(axis));
-	}
-
-	return usage();
-}
 
 //===========================================================================
 //
@@ -538,48 +278,6 @@ double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons)
 	return axisval;
 }
 
-//===========================================================================
-//
-// Joy_ApplyResponseCurveBezier
-//
-// Applies cubic bezier easing function
-// Curve is defined by control points [(0,0) (x1,y1) (x2,y2) (1,1)]
-// https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/cubic-bezier
-//
-//===========================================================================
-
-double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input)
-{
-	// clamp + trivial cases
-	if (input == 0) return 0;
-	double sign = (input >= 0)? 1.0: -1.0;
-	input = abs(input);
-	input = (input > 1.0)? 1.0: input;
-	if (input == 1.0) return sign*input;
-
-	double t = input, T;
-	float x1 = curve.x1, y1 = curve.y1, x2 = curve.x2, y2 = curve.y2;
-
-	const int max_iter = 4;
-	for (auto i = 0; i < max_iter; i++)
-	{
-		T = 1-t;
-
-		double x = 3*T*T*t*x1 + 3*T*t*t*x2 + t*t*t;
-		double dx = 3*T*T*x1 + 6*T*t*(x2-x1) + 3*t*t*(1-x2);
-
-		// no div by 0
-		if (abs(dx) < 0.00001) break;
-
-		t = clamp(t - (x-input)/dx, 0.0, 1.0);
-	}
-
-	T = 1-t;
-	t = 3*T*T*t*y1 + 3*T*t*t*y2 + t*t*t;
-
-	return sign*t;
-}
-
 //===========================================================================
 //
 // Joy_XYAxesToButtons
@@ -617,22 +315,6 @@ int Joy_XYAxesToButtons(double x, double y)
 	return JoyAngleButtons[int(rad) & 7];
 }
 
-//===========================================================================
-//
-// Joy_GenerateButtonEvent
-//
-// Send either a button up or button down event for supplied key code
-//
-//===========================================================================
-
-void Joy_GenerateButtonEvent(bool down, EKeyCodes which)
-{
-	event_t event = { 0,0,0,0,0,0,0 };
-	event.type = down ? EV_KeyDown : EV_KeyUp;
-	event.data1 = which;
-	D_PostEvent(&event);
-}
-
 //===========================================================================
 //
 // Joy_GenerateButtonEvents
@@ -648,12 +330,15 @@ void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, in
 	int changed = oldbuttons ^ newbuttons;
 	if (changed != 0)
 	{
+		event_t ev = { 0, 0, 0, 0, 0, 0, 0 };
 		int mask = 1;
 		for (int j = 0; j < numbuttons; mask <<= 1, ++j)
 		{
 			if (changed & mask)
 			{
-				Joy_GenerateButtonEvent(newbuttons & mask, static_cast(base + j));
+				ev.data1 = base + j;
+				ev.type = (newbuttons & mask) ? EV_KeyDown : EV_KeyUp;
+				D_PostEvent(&ev);
 			}
 		}
 	}
@@ -664,12 +349,15 @@ void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, co
 	int changed = oldbuttons ^ newbuttons;
 	if (changed != 0)
 	{
+		event_t ev = { 0, 0, 0, 0, 0, 0, 0 };
 		int mask = 1;
 		for (int j = 0; j < numbuttons; mask <<= 1, ++j)
 		{
 			if (changed & mask)
 			{
-				Joy_GenerateButtonEvent(newbuttons & mask, static_cast(keys[j]));
+				ev.data1 = keys[j];
+				ev.type = (newbuttons & mask) ? EV_KeyDown : EV_KeyUp;
+				D_PostEvent(&ev);
 			}
 		}
 	}
diff --git a/src/common/engine/m_joy.h b/src/common/engine/m_joy.h
index 64be2e164..e8d9d3b13 100644
--- a/src/common/engine/m_joy.h
+++ b/src/common/engine/m_joy.h
@@ -2,30 +2,9 @@
 #define M_JOY_H
 
 #include "basics.h"
-#include "keydef.h"
 #include "tarray.h"
 #include "c_cvars.h"
 
-union CubicBezier {
-	struct {
-		float x1;
-		float y1;
-		float x2;
-		float y2;
-	};
-	float pts[4];
-};
-
-enum EJoyCurve {
-	JOYCURVE_CUSTOM = -1,
-	JOYCURVE_DEFAULT,
-	JOYCURVE_LINEAR,
-	JOYCURVE_QUADRATIC,
-	JOYCURVE_CUBIC,
-
-	NUM_JOYCURVE
-};
-
 enum EJoyAxis
 {
 	JOYAXIS_None = -1,
@@ -34,22 +13,10 @@ enum EJoyAxis
 	JOYAXIS_Forward,
 	JOYAXIS_Side,
 	JOYAXIS_Up,
-	//	JOYAXIS_Roll,		// Ha ha. No roll for you.
+//	JOYAXIS_Roll,		// Ha ha. No roll for you.
 	NUM_JOYAXIS,
 };
 
-extern const float JOYDEADZONE_DEFAULT;
-
-extern const float JOYSENSITIVITY_DEFAULT;
-
-extern const float JOYTHRESH_DEFAULT;
-
-extern const float JOYTHRESH_TRIGGER;
-extern const float JOYTHRESH_STICK_X;
-extern const float JOYTHRESH_STICK_Y;
-
-extern const CubicBezier JOYCURVE[NUM_JOYCURVE];
-
 // Generic configuration interface for a controller.
 struct IJoystickConfig
 {
@@ -64,16 +31,10 @@ struct IJoystickConfig
 	virtual EJoyAxis GetAxisMap(int axis) = 0;
 	virtual const char *GetAxisName(int axis) = 0;
 	virtual float GetAxisScale(int axis) = 0;
-	virtual float GetAxisDigitalThreshold(int axis) = 0;
-	virtual EJoyCurve GetAxisResponseCurve(int axis) = 0;
-	virtual float GetAxisResponseCurvePoint(int axis, int point) = 0;
 
 	virtual void SetAxisDeadZone(int axis, float zone) = 0;
 	virtual void SetAxisMap(int axis, EJoyAxis gameaxis) = 0;
 	virtual void SetAxisScale(int axis, float scale) = 0;
-	virtual void SetAxisDigitalThreshold(int axis, float threshold) = 0;
-	virtual void SetAxisResponseCurve(int axis, EJoyCurve preset) = 0;
-	virtual void SetAxisResponseCurvePoint(int axis, int point, float value) = 0;
 
 	virtual bool GetEnabled() = 0;
 	virtual void SetEnabled(bool enabled) = 0;
@@ -87,8 +48,6 @@ struct IJoystickConfig
 	virtual bool IsAxisDeadZoneDefault(int axis) = 0;
 	virtual bool IsAxisMapDefault(int axis) = 0;
 	virtual bool IsAxisScaleDefault(int axis) = 0;
-	virtual bool IsAxisDigitalThresholdDefault(int axis) = 0;
-	virtual bool IsAxisResponseCurveDefault(int axis) = 0;
 
 	virtual void SetDefaultConfig() = 0;
 	virtual FString GetIdentifier() = 0;
@@ -99,12 +58,10 @@ EXTERN_CVAR(Bool, use_joystick);
 bool M_LoadJoystickConfig(IJoystickConfig *joy);
 void M_SaveJoystickConfig(IJoystickConfig *joy);
 
-void Joy_GenerateButtonEvent(bool down, EKeyCodes which);
 void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, int base);
 void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, const int *keys);
 int Joy_XYAxesToButtons(double x, double y);
 double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons);
-double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input);
 
 // These ought to be provided by a system-specific i_input.cpp.
 void I_GetAxes(float axes[NUM_JOYAXIS]);
diff --git a/src/common/menu/joystickmenu.cpp b/src/common/menu/joystickmenu.cpp
index ad86dd985..3d3e881b1 100644
--- a/src/common/menu/joystickmenu.cpp
+++ b/src/common/menu/joystickmenu.cpp
@@ -84,56 +84,6 @@ DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisDeadZone)
 	return 0;
 }
 
-DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisDigitalThreshold)
-{
-	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
-	PARAM_INT(axis);
-	ACTION_RETURN_FLOAT(self->GetAxisDigitalThreshold(axis));
-}
-
-DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisDigitalThreshold)
-{
-	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
-	PARAM_INT(axis);
-	PARAM_FLOAT(dt);
-	self->SetAxisDigitalThreshold(axis, (float)dt);
-	return 0;
-}
-
-DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisResponseCurve)
-{
-	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
-	PARAM_INT(axis);
-	ACTION_RETURN_INT(self->GetAxisResponseCurve(axis));
-}
-
-DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisResponseCurve)
-{
-	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
-	PARAM_INT(axis);
-	PARAM_INT(curve);
-	self->SetAxisResponseCurve(axis, (EJoyCurve)curve);
-	return 0;
-}
-
-DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisResponseCurvePoint)
-{
-	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
-	PARAM_INT(axis);
-	PARAM_INT(point);
-	ACTION_RETURN_INT(self->GetAxisResponseCurvePoint(axis, point));
-}
-
-DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisResponseCurvePoint)
-{
-	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
-	PARAM_INT(axis);
-	PARAM_INT(point);
-	PARAM_FLOAT(value);
-	self->SetAxisResponseCurvePoint(axis, point, value);
-	return 0;
-}
-
 DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisMap)
 {
 	PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig);
diff --git a/src/common/menu/menu.cpp b/src/common/menu/menu.cpp
index 6fbe1dd1b..a07dfd36f 100644
--- a/src/common/menu/menu.cpp
+++ b/src/common/menu/menu.cpp
@@ -680,7 +680,7 @@ bool M_Responder (event_t *ev)
 		else if (menuactive != MENU_WaitKey && (ev->type == EV_KeyDown || ev->type == EV_KeyUp))
 		{
 			// eat blocked controller events without dispatching them.
-			if (ev->data1 >= KEY_FIRSTJOYBUTTON && m_blockcontrollers && ev->type == EV_KeyDown) return true;
+			if (ev->data1 >= KEY_FIRSTJOYBUTTON && m_blockcontrollers) return true;
 
 			keyup = ev->type == EV_KeyUp;
 
diff --git a/src/common/platform/posix/cocoa/i_joystick.cpp b/src/common/platform/posix/cocoa/i_joystick.cpp
index ffb9f90a0..44d1fb961 100644
--- a/src/common/platform/posix/cocoa/i_joystick.cpp
+++ b/src/common/platform/posix/cocoa/i_joystick.cpp
@@ -94,23 +94,15 @@ public:
 	virtual EJoyAxis GetAxisMap(int axis);
 	virtual const char* GetAxisName(int axis);
 	virtual float GetAxisScale(int axis);
-	float GetAxisDigitalThreshold(int axis);
-	EJoyCurve GetAxisResponseCurve(int axis);
-	float GetAxisResponseCurvePoint(int axis, int point);
 
 	virtual void SetAxisDeadZone(int axis, float deadZone);
 	virtual void SetAxisMap(int axis, EJoyAxis gameAxis);
 	virtual void SetAxisScale(int axis, float scale);
-	void SetAxisDigitalThreshold(int axis, float threshold);
-	void SetAxisResponseCurve(int axis, EJoyCurve preset);
-	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	virtual bool IsSensitivityDefault();
 	virtual bool IsAxisDeadZoneDefault(int axis);
 	virtual bool IsAxisMapDefault(int axis);
 	virtual bool IsAxisScaleDefault(int axis);
-	bool IsAxisDigitalThresholdDefault(int axis);
-	bool IsAxisResponseCurveDefault(int axis);
 
 	virtual bool GetEnabled();
 	virtual void SetEnabled(bool enabled);
@@ -154,11 +146,6 @@ private:
 		float defaultDeadZone;
 		float sensitivity;
 		float defaultSensitivity;
-		float digitalThreshold;
-		float defaultDigitalThreshold;
-		EJoyCurve responseCurvePreset;
-		EJoyCurve defaultResponseCurvePreset;
-		CubicBezier responseCurve;
 
 		EJoyAxis gameAxis;
 		EJoyAxis defaultGameAxis;
@@ -190,6 +177,10 @@ private:
 
 	io_object_t m_notification;
 
+
+	static const float DEFAULT_DEADZONE;
+	static const float DEFAULT_SENSITIVITY;
+
 	void ProcessAxes();
 	bool ProcessAxis  (const IOHIDEventStruct& event);
 	bool ProcessButton(const IOHIDEventStruct& event);
@@ -209,6 +200,10 @@ private:
 };
 
 
+const float IOKitJoystick::DEFAULT_DEADZONE    = 0.25f;
+const float IOKitJoystick::DEFAULT_SENSITIVITY = 1.0f;
+
+
 IOHIDDeviceInterface** CreateDeviceInterface(const io_object_t device)
 {
 	IOCFPlugInInterface** plugInInterface = NULL;
@@ -291,7 +286,7 @@ IOHIDQueueInterface** CreateDeviceQueue(IOHIDDeviceInterface** const interface)
 IOKitJoystick::IOKitJoystick(const io_object_t device)
 : m_interface(CreateDeviceInterface(device))
 , m_queue(CreateDeviceQueue(m_interface))
-, m_sensitivity(JOYSENSITIVITY_DEFAULT)
+, m_sensitivity(DEFAULT_SENSITIVITY)
 , m_enabled(true)
 , m_useAxesPolling(true)
 , m_notification(0)
@@ -391,21 +386,6 @@ float IOKitJoystick::GetAxisScale(int axis)
 	return IS_AXIS_VALID ? m_axes[axis].sensitivity : 0.0f;
 }
 
-float IOKitJoystick::GetAxisDigitalThreshold(int axis)
-{
-	return IS_AXIS_VALID ? m_axes[axis].digitalThreshold : JOYTHRESH_DEFAULT;
-}
-
-EJoyCurve IOKitJoystick::GetAxisResponseCurve(int axis)
-{
-	return IS_AXIS_VALID ? m_axes[axis].responseCurvePreset : JOYCURVE_DEFAULT;
-}
-
-float IOKitJoystick::GetAxisResponseCurvePoint(int axis, int point)
-{
-	return ( IS_AXIS_VALID && unsigned(point) < 4 )? m_axes[axis].responseCurve.pts[point] : 0;
-}
-
 void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone)
 {
 	if (IS_AXIS_VALID)
@@ -432,37 +412,10 @@ void IOKitJoystick::SetAxisScale(int axis, float scale)
 	}
 }
 
-void IOKitJoystick::SetAxisDigitalThreshold(int axis, float threshold)
-{
-	if (IS_AXIS_VALID)
-	{
-		m_axes[axis].digitalThreshold = threshold;
-	}
-}
-
-void IOKitJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset)
-{
-	if (IS_AXIS_VALID)
-	{
-		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
-		m_axes[axis].responseCurvePreset = preset;
-		if (preset == JOYCURVE_CUSTOM) return;
-		m_axes[axis].responseCurve = JOYCURVE[preset];
-	}
-}
-
-void IOKitJoystick::SetAxisResponseCurvePoint(int axis, int point, float value)
-{
-	if (IS_AXIS_VALID && unsigned(point) < 4)
-	{
-		m_axes[axis].responseCurvePreset = JOYCURVE_CUSTOM;
-		m_axes[axis].responseCurve.pts[point] = value;
-	}
-}
 
 bool IOKitJoystick::IsSensitivityDefault()
 {
-	return JOYSENSITIVITY_DEFAULT == m_sensitivity;
+	return DEFAULT_SENSITIVITY == m_sensitivity;
 }
 
 bool IOKitJoystick::IsAxisDeadZoneDefault(int axis)
@@ -486,19 +439,7 @@ bool IOKitJoystick::IsAxisScaleDefault(int axis)
 		: true;
 }
 
-bool IOKitJoystick::IsAxisDigitalThresholdDefault(int axis)
-{
-	return IS_AXIS_VALID
-		? (m_axes[axis].digitalThreshold == m_axes[axis].defaultDigitalThreshold)
-		: true;
-}
 
-bool IOKitJoystick::IsAxisResponseCurveDefault(int axis)
-{
-	return IS_AXIS_VALID
-		? m_axes[axis].responseCurvePreset == m_axes[axis].defaultResponseCurvePreset
-		: true;
-}
 
 bool IOKitJoystick::GetEnabled()
 {
@@ -513,18 +454,15 @@ void IOKitJoystick::SetEnabled(bool enabled)
 
 void IOKitJoystick::SetDefaultConfig()
 {
-	m_sensitivity = JOYSENSITIVITY_DEFAULT;
+	m_sensitivity = DEFAULT_SENSITIVITY;
 
 	const size_t axisCount = m_axes.Size();
 
 	for (size_t i = 0; i < axisCount; ++i)
 	{
-		m_axes[i].deadZone    = JOYDEADZONE_DEFAULT;
-		m_axes[i].sensitivity = JOYSENSITIVITY_DEFAULT;
+		m_axes[i].deadZone    = DEFAULT_DEADZONE;
+		m_axes[i].sensitivity = DEFAULT_SENSITIVITY;
 		m_axes[i].gameAxis    = JOYAXIS_None;
-		m_axes[i].digitalThreshold = JOYTHRESH_DEFAULT;
-		m_axes[i].responseCurvePreset = JOYCURVE_DEFAULT;
-		m_axes[i].responseCurve = JOYCURVE[JOYCURVE_DEFAULT];
 	}
 
 	// Two axes? Horizontal is yaw and vertical is forward.
@@ -532,10 +470,7 @@ void IOKitJoystick::SetDefaultConfig()
 	if (2 == axisCount)
 	{
 		m_axes[0].gameAxis = JOYAXIS_Yaw;
-		m_axes[0].digitalThreshold = JOYTHRESH_STICK_X;
-
 		m_axes[1].gameAxis = JOYAXIS_Forward;
-		m_axes[1].digitalThreshold = JOYTHRESH_STICK_Y;
 	}
 
 	// Three axes? First two are movement, third is yaw.
@@ -543,13 +478,8 @@ void IOKitJoystick::SetDefaultConfig()
 	else if (axisCount >= 3)
 	{
 		m_axes[0].gameAxis = JOYAXIS_Side;
-		m_axes[0].digitalThreshold = JOYTHRESH_STICK_X;
-
 		m_axes[1].gameAxis = JOYAXIS_Forward;
-		m_axes[1].digitalThreshold = JOYTHRESH_STICK_Y;
-
 		m_axes[2].gameAxis = JOYAXIS_Yaw;
-		m_axes[2].digitalThreshold = JOYTHRESH_STICK_X;
 
 		// Four axes? First two are movement, last two are looking around.
 
@@ -557,14 +487,12 @@ void IOKitJoystick::SetDefaultConfig()
 		{
 			m_axes[3].gameAxis = JOYAXIS_Pitch;
 //	???		m_axes[3].sensitivity = 0.75f;
-			m_axes[3].digitalThreshold = JOYTHRESH_STICK_Y;
 
 			// Five axes? Use the fifth one for moving up and down.
 
 			if (axisCount >= 5)
 			{
 				m_axes[4].gameAxis = JOYAXIS_Up;
-				m_axes[4].digitalThreshold = JOYTHRESH_STICK_Y;
 			}
 		}
 	}
@@ -576,11 +504,9 @@ void IOKitJoystick::SetDefaultConfig()
 
 	for (size_t i = 0; i < axisCount; ++i)
 	{
-		m_axes[i].defaultDeadZone            = m_axes[i].deadZone;
-		m_axes[i].defaultSensitivity         = m_axes[i].sensitivity;
-		m_axes[i].defaultGameAxis            = m_axes[i].gameAxis;
-		m_axes[i].defaultDigitalThreshold    = m_axes[i].digitalThreshold;
-		m_axes[i].defaultResponseCurvePreset = m_axes[i].responseCurvePreset;
+		m_axes[i].defaultDeadZone    = m_axes[i].deadZone;
+		m_axes[i].defaultSensitivity = m_axes[i].sensitivity;
+		m_axes[i].defaultGameAxis    = m_axes[i].gameAxis;
 	}
 }
 
@@ -676,9 +602,8 @@ void IOKitJoystick::ProcessAxes()
 			const double scaledValue = scaledMin +
 				(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
 			const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);
-			const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue);
 
-			axis.value = static_cast(smoothedValue * m_sensitivity * axis.sensitivity);
+			axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity);
 		}
 		else
 		{
@@ -710,9 +635,8 @@ bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event)
 		const double scaledValue = scaledMin +
 			(event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue);
 		const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL);
-		const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue);
 
-		axis.value = static_cast(smoothedValue * m_sensitivity * axis.sensitivity);
+		axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity);
 
 		return true;
 	}
diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp
index da7ac59c2..0148eaad2 100644
--- a/src/common/platform/posix/sdl/i_input.cpp
+++ b/src/common/platform/posix/sdl/i_input.cpp
@@ -31,14 +31,11 @@
 **
 */
 #include 
-#include 
-#include "i_input.h"
-#include "c_cvars.h"
-#include "dobject.h"
 #include "m_argv.h"
 #include "m_joy.h"
 #include "v_video.h"
 
+#include "d_eventbase.h"
 #include "d_gui.h"
 #include "c_buttons.h"
 #include "c_console.h"
@@ -50,147 +47,88 @@
 #include "engineerrors.h"
 #include "i_interface.h"
 
+
+static void I_CheckGUICapture ();
+static void I_CheckNativeMouse ();
+
 bool GUICapture;
 static bool NativeMouse = true;
 
 CVAR (Bool,  use_mouse,				true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
 
+
 extern int WaitingForKey;
 
 static const SDL_Keycode DIKToKeySym[256] =
 {
-	SDLK_UNKNOWN,  SDLK_ESCAPE,       SDLK_1,            SDLK_2,
-	SDLK_3,        SDLK_4,            SDLK_5,            SDLK_6,
-	SDLK_7,        SDLK_8,            SDLK_9,            SDLK_0,
-	SDLK_MINUS,    SDLK_EQUALS,       SDLK_BACKSPACE,    SDLK_TAB,
-	SDLK_q,        SDLK_w,            SDLK_e,            SDLK_r,
-	SDLK_t,        SDLK_y,            SDLK_u,            SDLK_i,
-	SDLK_o,        SDLK_p,            SDLK_LEFTBRACKET,  SDLK_RIGHTBRACKET,
-	SDLK_RETURN,   SDLK_LCTRL,        SDLK_a,            SDLK_s,
-	SDLK_d,        SDLK_f,            SDLK_g,            SDLK_h,
-	SDLK_j,        SDLK_k,            SDLK_l,            SDLK_SEMICOLON,
-	SDLK_QUOTE,    SDLK_BACKQUOTE,    SDLK_LSHIFT,       SDLK_BACKSLASH,
-	SDLK_z,        SDLK_x,            SDLK_c,            SDLK_v,
-	SDLK_b,        SDLK_n,            SDLK_m,            SDLK_COMMA,
-	SDLK_PERIOD,   SDLK_SLASH,        SDLK_RSHIFT,       SDLK_KP_MULTIPLY,
-	SDLK_LALT,     SDLK_SPACE,        SDLK_CAPSLOCK,     SDLK_F1,
-	SDLK_F2,       SDLK_F3,           SDLK_F4,           SDLK_F5,
-	SDLK_F6,       SDLK_F7,           SDLK_F8,           SDLK_F9,
-	SDLK_F10,      SDLK_NUMLOCKCLEAR, SDLK_SCROLLLOCK,   SDLK_KP_7,
-	SDLK_KP_8,     SDLK_KP_9,         SDLK_KP_MINUS,     SDLK_KP_4,
-	SDLK_KP_5,     SDLK_KP_6,         SDLK_KP_PLUS,      SDLK_KP_1,
-	SDLK_KP_2,     SDLK_KP_3,         SDLK_KP_0,         SDLK_KP_PERIOD,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_F11,
-	SDLK_F12,      SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_F13,      SDLK_F14,          SDLK_F15,          SDLK_F16,
-	SDLK_F17,      SDLK_F18,          SDLK_F19,          SDLK_F20,
-	SDLK_F21,      SDLK_F22,          SDLK_F23,          SDLK_F24,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_KP_EQUALS,    SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_AT,           SDLK_COLON,        SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_KP_ENTER, SDLK_RCTRL,        SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_KP_COMMA,
-	SDLK_UNKNOWN,  SDLK_KP_DIVIDE,    SDLK_UNKNOWN,      SDLK_SYSREQ,
-	SDLK_RALT,     SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_PAUSE,        SDLK_UNKNOWN,      SDLK_HOME,
-	SDLK_UP,       SDLK_PAGEUP,       SDLK_UNKNOWN,      SDLK_LEFT,
-	SDLK_UNKNOWN,  SDLK_RIGHT,        SDLK_UNKNOWN,      SDLK_END,
-	SDLK_DOWN,     SDLK_PAGEDOWN,     SDLK_INSERT,       SDLK_DELETE,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_LGUI,
-	SDLK_RGUI,     SDLK_MENU,         SDLK_POWER,        SDLK_SLEEP,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_AC_SEARCH,    SDLK_AC_BOOKMARKS, SDLK_AC_REFRESH,
-	SDLK_AC_STOP,  SDLK_AC_FORWARD,   SDLK_AC_BACK,      SDLK_COMPUTER,
-	SDLK_MAIL,     SDLK_MEDIASELECT,  SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN,
-	SDLK_UNKNOWN,  SDLK_UNKNOWN,      SDLK_UNKNOWN,      SDLK_UNKNOWN
+	0, SDLK_ESCAPE, SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6,
+	SDLK_7, SDLK_8, SDLK_9, SDLK_0,SDLK_MINUS, SDLK_EQUALS, SDLK_BACKSPACE, SDLK_TAB,
+	SDLK_q, SDLK_w, SDLK_e, SDLK_r, SDLK_t, SDLK_y, SDLK_u, SDLK_i,
+	SDLK_o, SDLK_p, SDLK_LEFTBRACKET, SDLK_RIGHTBRACKET, SDLK_RETURN, SDLK_LCTRL, SDLK_a, SDLK_s,
+	SDLK_d, SDLK_f, SDLK_g, SDLK_h, SDLK_j, SDLK_k, SDLK_l, SDLK_SEMICOLON,
+	SDLK_QUOTE, SDLK_BACKQUOTE, SDLK_LSHIFT, SDLK_BACKSLASH, SDLK_z, SDLK_x, SDLK_c, SDLK_v,
+	SDLK_b, SDLK_n, SDLK_m, SDLK_COMMA, SDLK_PERIOD, SDLK_SLASH, SDLK_RSHIFT, SDLK_KP_MULTIPLY,
+	SDLK_LALT, SDLK_SPACE, SDLK_CAPSLOCK, SDLK_F1, SDLK_F2, SDLK_F3, SDLK_F4, SDLK_F5,
+	SDLK_F6, SDLK_F7, SDLK_F8, SDLK_F9, SDLK_F10, SDLK_NUMLOCKCLEAR, SDLK_SCROLLLOCK, SDLK_KP_7,
+	SDLK_KP_8, SDLK_KP_9, SDLK_KP_MINUS, SDLK_KP_4, SDLK_KP_5, SDLK_KP_6, SDLK_KP_PLUS, SDLK_KP_1,
+	SDLK_KP_2, SDLK_KP_3, SDLK_KP_0, SDLK_KP_PERIOD, 0, 0, 0, SDLK_F11,
+	SDLK_F12, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, SDLK_F13, SDLK_F14, SDLK_F15, SDLK_F16,
+	SDLK_F17, SDLK_F18, SDLK_F19, SDLK_F20, SDLK_F21, SDLK_F22, SDLK_F23, SDLK_F24,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, SDLK_KP_EQUALS, 0, 0,
+	0, SDLK_AT, SDLK_COLON, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, SDLK_KP_ENTER, SDLK_RCTRL, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, SDLK_KP_COMMA, 0, SDLK_KP_DIVIDE, 0, SDLK_SYSREQ,
+	SDLK_RALT, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, SDLK_PAUSE, 0, SDLK_HOME,
+	SDLK_UP, SDLK_PAGEUP, 0, SDLK_LEFT, 0, SDLK_RIGHT, 0, SDLK_END,
+	SDLK_DOWN, SDLK_PAGEDOWN, SDLK_INSERT, SDLK_DELETE, 0, 0, 0, 0,
+	0, 0, 0, SDLK_LGUI, SDLK_RGUI, SDLK_MENU, SDLK_POWER, SDLK_SLEEP,
+	0, 0, 0, 0, 0, SDLK_AC_SEARCH, SDLK_AC_BOOKMARKS, SDLK_AC_REFRESH,
+	SDLK_AC_STOP, SDLK_AC_FORWARD, SDLK_AC_BACK, SDLK_COMPUTER, SDLK_MAIL, SDLK_MEDIASELECT, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0
 };
 
 static const SDL_Scancode DIKToKeyScan[256] =
 {
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_ESCAPE,       SDL_SCANCODE_1,            SDL_SCANCODE_2,
-	SDL_SCANCODE_3,          SDL_SCANCODE_4,            SDL_SCANCODE_5,            SDL_SCANCODE_6,
-	SDL_SCANCODE_7,          SDL_SCANCODE_8,            SDL_SCANCODE_9,            SDL_SCANCODE_0,
-	SDL_SCANCODE_MINUS,      SDL_SCANCODE_EQUALS,       SDL_SCANCODE_BACKSPACE,    SDL_SCANCODE_TAB,
-	SDL_SCANCODE_Q,          SDL_SCANCODE_W,            SDL_SCANCODE_E,            SDL_SCANCODE_R,
-	SDL_SCANCODE_T,          SDL_SCANCODE_Y,            SDL_SCANCODE_U,            SDL_SCANCODE_I,
-	SDL_SCANCODE_O,          SDL_SCANCODE_P,            SDL_SCANCODE_LEFTBRACKET,  SDL_SCANCODE_RIGHTBRACKET,
-	SDL_SCANCODE_RETURN,     SDL_SCANCODE_LCTRL,        SDL_SCANCODE_A,            SDL_SCANCODE_S,
-	SDL_SCANCODE_D,          SDL_SCANCODE_F,            SDL_SCANCODE_G,            SDL_SCANCODE_H,
-	SDL_SCANCODE_J,          SDL_SCANCODE_K,            SDL_SCANCODE_L,            SDL_SCANCODE_SEMICOLON,
-	SDL_SCANCODE_APOSTROPHE, SDL_SCANCODE_GRAVE,        SDL_SCANCODE_LSHIFT,       SDL_SCANCODE_BACKSLASH,
-	SDL_SCANCODE_Z,          SDL_SCANCODE_X,            SDL_SCANCODE_C,            SDL_SCANCODE_V,
-	SDL_SCANCODE_B,          SDL_SCANCODE_N,            SDL_SCANCODE_M,            SDL_SCANCODE_COMMA,
-	SDL_SCANCODE_PERIOD,     SDL_SCANCODE_SLASH,        SDL_SCANCODE_RSHIFT,       SDL_SCANCODE_KP_MULTIPLY,
-	SDL_SCANCODE_LALT,       SDL_SCANCODE_SPACE,        SDL_SCANCODE_CAPSLOCK,     SDL_SCANCODE_F1,
-	SDL_SCANCODE_F2,         SDL_SCANCODE_F3,           SDL_SCANCODE_F4,           SDL_SCANCODE_F5,
-	SDL_SCANCODE_F6,         SDL_SCANCODE_F7,           SDL_SCANCODE_F8,           SDL_SCANCODE_F9,
-	SDL_SCANCODE_F10,        SDL_SCANCODE_NUMLOCKCLEAR, SDL_SCANCODE_SCROLLLOCK,   SDL_SCANCODE_KP_7,
-	SDL_SCANCODE_KP_8,       SDL_SCANCODE_KP_9,         SDL_SCANCODE_KP_MINUS,     SDL_SCANCODE_KP_4,
-	SDL_SCANCODE_KP_5,       SDL_SCANCODE_KP_6,         SDL_SCANCODE_KP_PLUS,      SDL_SCANCODE_KP_1,
-	SDL_SCANCODE_KP_2,       SDL_SCANCODE_KP_3,         SDL_SCANCODE_KP_0,         SDL_SCANCODE_KP_PERIOD,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_F11,
-	SDL_SCANCODE_F12,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_F13,        SDL_SCANCODE_F14,          SDL_SCANCODE_F15,          SDL_SCANCODE_F16,
-	SDL_SCANCODE_F17,        SDL_SCANCODE_F18,          SDL_SCANCODE_F19,          SDL_SCANCODE_F20,
-	SDL_SCANCODE_F21,        SDL_SCANCODE_F22,          SDL_SCANCODE_F23,          SDL_SCANCODE_F24,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_KP_EQUALS,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_KP_ENTER,   SDL_SCANCODE_RCTRL,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_KP_COMMA,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_KP_DIVIDE,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_SYSREQ,
-	SDL_SCANCODE_RALT,       SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_PAUSE,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_HOME,
-	SDL_SCANCODE_UP,         SDL_SCANCODE_PAGEUP,       SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_LEFT,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_RIGHT,        SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_END,
-	SDL_SCANCODE_DOWN,       SDL_SCANCODE_PAGEDOWN,     SDL_SCANCODE_INSERT,       SDL_SCANCODE_DELETE,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_LGUI,
-	SDL_SCANCODE_RGUI,       SDL_SCANCODE_MENU,         SDL_SCANCODE_POWER,        SDL_SCANCODE_SLEEP,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_AC_SEARCH,    SDL_SCANCODE_AC_BOOKMARKS, SDL_SCANCODE_AC_REFRESH,
-	SDL_SCANCODE_AC_STOP,    SDL_SCANCODE_AC_FORWARD,   SDL_SCANCODE_AC_BACK,      SDL_SCANCODE_COMPUTER,
-	SDL_SCANCODE_MAIL,       SDL_SCANCODE_MEDIASELECT,  SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,
-	SDL_SCANCODE_UNKNOWN,    SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN,      SDL_SCANCODE_UNKNOWN
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_ESCAPE, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, SDL_SCANCODE_5, SDL_SCANCODE_6,
+	SDL_SCANCODE_7, SDL_SCANCODE_8, SDL_SCANCODE_9, SDL_SCANCODE_0 ,SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS, SDL_SCANCODE_BACKSPACE, SDL_SCANCODE_TAB,
+	SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_E, SDL_SCANCODE_R, SDL_SCANCODE_T, SDL_SCANCODE_Y, SDL_SCANCODE_U, SDL_SCANCODE_I,
+	SDL_SCANCODE_O, SDL_SCANCODE_P, SDL_SCANCODE_LEFTBRACKET, SDL_SCANCODE_RIGHTBRACKET, SDL_SCANCODE_RETURN, SDL_SCANCODE_LCTRL, SDL_SCANCODE_A, SDL_SCANCODE_S,
+	SDL_SCANCODE_D, SDL_SCANCODE_F, SDL_SCANCODE_G, SDL_SCANCODE_H, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L, SDL_SCANCODE_SEMICOLON,
+	SDL_SCANCODE_APOSTROPHE, SDL_SCANCODE_GRAVE, SDL_SCANCODE_LSHIFT, SDL_SCANCODE_BACKSLASH, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_C, SDL_SCANCODE_V,
+	SDL_SCANCODE_B, SDL_SCANCODE_N, SDL_SCANCODE_M, SDL_SCANCODE_COMMA, SDL_SCANCODE_PERIOD, SDL_SCANCODE_SLASH, SDL_SCANCODE_RSHIFT, SDL_SCANCODE_KP_MULTIPLY,
+	SDL_SCANCODE_LALT, SDL_SCANCODE_SPACE, SDL_SCANCODE_CAPSLOCK, SDL_SCANCODE_F1, SDL_SCANCODE_F2, SDL_SCANCODE_F3, SDL_SCANCODE_F4, SDL_SCANCODE_F5,
+	SDL_SCANCODE_F6, SDL_SCANCODE_F7, SDL_SCANCODE_F8, SDL_SCANCODE_F9, SDL_SCANCODE_F10, SDL_SCANCODE_NUMLOCKCLEAR, SDL_SCANCODE_SCROLLLOCK, SDL_SCANCODE_KP_7,
+	SDL_SCANCODE_KP_8, SDL_SCANCODE_KP_9, SDL_SCANCODE_KP_MINUS, SDL_SCANCODE_KP_4, SDL_SCANCODE_KP_5, SDL_SCANCODE_KP_6, SDL_SCANCODE_KP_PLUS, SDL_SCANCODE_KP_1,
+	SDL_SCANCODE_KP_2, SDL_SCANCODE_KP_3, SDL_SCANCODE_KP_0, SDL_SCANCODE_KP_PERIOD, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F11,
+	SDL_SCANCODE_F12, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F13, SDL_SCANCODE_F14, SDL_SCANCODE_F15, SDL_SCANCODE_F16,
+	SDL_SCANCODE_F17, SDL_SCANCODE_F18, SDL_SCANCODE_F19, SDL_SCANCODE_F20, SDL_SCANCODE_F21, SDL_SCANCODE_F22, SDL_SCANCODE_F23, SDL_SCANCODE_F24,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_EQUALS, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_ENTER, SDL_SCANCODE_RCTRL, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_COMMA, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_DIVIDE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_SYSREQ,
+	SDL_SCANCODE_RALT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_PAUSE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_HOME,
+	SDL_SCANCODE_UP, SDL_SCANCODE_PAGEUP, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_END,
+	SDL_SCANCODE_DOWN, SDL_SCANCODE_PAGEDOWN, SDL_SCANCODE_INSERT, SDL_SCANCODE_DELETE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LGUI, SDL_SCANCODE_RGUI, SDL_SCANCODE_MENU, SDL_SCANCODE_POWER, SDL_SCANCODE_SLEEP,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_AC_SEARCH, SDL_SCANCODE_AC_BOOKMARKS, SDL_SCANCODE_AC_REFRESH,
+	SDL_SCANCODE_AC_STOP, SDL_SCANCODE_AC_FORWARD, SDL_SCANCODE_AC_BACK, SDL_SCANCODE_COMPUTER, SDL_SCANCODE_MAIL, SDL_SCANCODE_MEDIASELECT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN
 };
 
 static TMap InitKeySymMap ()
@@ -525,60 +463,14 @@ void MessagePump (const SDL_Event &sev)
 
 	case SDL_JOYBUTTONDOWN:
 	case SDL_JOYBUTTONUP:
-		if (SDL_GameControllerFromInstanceID(sev.jdevice.which))
-			break; // let SDL_CONTROLLERBUTTON* handle this
 		event.type = sev.type == SDL_JOYBUTTONDOWN ? EV_KeyDown : EV_KeyUp;
 		event.data1 = KEY_FIRSTJOYBUTTON + sev.jbutton.button;
 		if(event.data1 != 0)
-			I_JoyConsumeEvent(sev.jdevice.which, &event);
-		break;
-
-	case SDL_CONTROLLERBUTTONDOWN:
-	case SDL_CONTROLLERBUTTONUP:
-		event.type = sev.type == SDL_CONTROLLERBUTTONDOWN ? EV_KeyDown : EV_KeyUp;
-		switch (sev.cbutton.button)
-		{
-			case SDL_CONTROLLER_BUTTON_A:             event.data1 = KEY_PAD_A;          break;
-			case SDL_CONTROLLER_BUTTON_B:             event.data1 = KEY_PAD_B;          break;
-			case SDL_CONTROLLER_BUTTON_X:             event.data1 = KEY_PAD_X;          break;
-			case SDL_CONTROLLER_BUTTON_Y:             event.data1 = KEY_PAD_Y;          break;
-			case SDL_CONTROLLER_BUTTON_BACK:          event.data1 = KEY_PAD_BACK;       break;
-			case SDL_CONTROLLER_BUTTON_GUIDE:         event.data1 = KEY_PAD_GUIDE;      break;
-			case SDL_CONTROLLER_BUTTON_START:         event.data1 = KEY_PAD_START;      break;
-			case SDL_CONTROLLER_BUTTON_LEFTSTICK:     event.data1 = KEY_PAD_LTHUMB;     break;
-			case SDL_CONTROLLER_BUTTON_RIGHTSTICK:    event.data1 = KEY_PAD_RTHUMB;     break;
-			case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:  event.data1 = KEY_PAD_LSHOULDER;  break;
-			case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: event.data1 = KEY_PAD_RSHOULDER;  break;
-			case SDL_CONTROLLER_BUTTON_DPAD_UP:       event.data1 = KEY_PAD_DPAD_UP;    break;
-			case SDL_CONTROLLER_BUTTON_DPAD_DOWN:     event.data1 = KEY_PAD_DPAD_DOWN;  break;
-			case SDL_CONTROLLER_BUTTON_DPAD_LEFT:     event.data1 = KEY_PAD_DPAD_LEFT;  break;
-			case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:    event.data1 = KEY_PAD_DPAD_RIGHT; break;
-			case SDL_CONTROLLER_BUTTON_MISC1:         event.data1 = KEY_PAD_MISC1;      break;
-			case SDL_CONTROLLER_BUTTON_PADDLE1:       event.data1 = KEY_PAD_PADDLE1;    break;
-			case SDL_CONTROLLER_BUTTON_PADDLE2:       event.data1 = KEY_PAD_PADDLE2;    break;
-			case SDL_CONTROLLER_BUTTON_PADDLE3:       event.data1 = KEY_PAD_PADDLE3;    break;
-			case SDL_CONTROLLER_BUTTON_PADDLE4:       event.data1 = KEY_PAD_PADDLE4;    break;
-			case SDL_CONTROLLER_BUTTON_TOUCHPAD:      event.data1 = KEY_PAD_TOUCHPAD;   break;
-			default:                                  event.data1 = 0;
-		}
-		if(event.data1 != 0)
-			I_JoyConsumeEvent(sev.cbutton.which, &event);
+			D_PostEvent(&event);
 		break;
 
 	case SDL_JOYDEVICEADDED:
-	case SDL_CONTROLLERDEVICEADDED:
-		if (sev.type == SDL_JOYDEVICEADDED && SDL_IsGameController(sev.jdevice.which)) // DeviceIndex Here
-			break; // skip double event
-		I_UpdateDeviceList();
-		event.type = EV_DeviceChange;
-		D_PostEvent (&event);
-		break;
-
 	case SDL_JOYDEVICEREMOVED:
-	case SDL_CONTROLLERDEVICEREMOVED:
-	case SDL_CONTROLLERDEVICEREMAPPED:
-		if (sev.type == SDL_JOYDEVICEREMOVED && SDL_GameControllerFromInstanceID(sev.jdevice.which))
-			break; // skip double event
 		I_UpdateDeviceList();
 		event.type = EV_DeviceChange;
 		D_PostEvent (&event);
diff --git a/src/common/platform/posix/sdl/i_input.h b/src/common/platform/posix/sdl/i_input.h
deleted file mode 100644
index 6ca2e05aa..000000000
--- a/src/common/platform/posix/sdl/i_input.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#ifndef I_INPUT_H
-#define I_INPUT_H
-
-#include "d_eventbase.h"
-
-extern int WaitingForKey;
-
-static void I_CheckGUICapture ();
-static void I_CheckNativeMouse ();
-
-void I_JoyConsumeEvent(int instanceID, event_t * event);
-
-#endif
diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp
index d37b4f3a6..8dc9fd8ac 100644
--- a/src/common/platform/posix/sdl/i_joystick.cpp
+++ b/src/common/platform/posix/sdl/i_joystick.cpp
@@ -31,80 +31,47 @@
 **
 */
 #include 
-#include 
-#include 
 
 #include "basics.h"
 #include "cmdlib.h"
 
-#include "d_eventbase.h"
-#include "i_input.h"
 #include "m_joy.h"
+#include "keydef.h"
+
+#define DEFAULT_DEADZONE 0.25f;
+
+// Very small deadzone so that floating point magic doesn't happen
+#define MIN_DEADZONE 0.000001f
 
 class SDLInputJoystick: public IJoystickConfig
 {
 public:
-	SDLInputJoystick(int DeviceIndex) :
-	DeviceIndex(DeviceIndex),
-	InstanceID(SDL_JoystickGetDeviceInstanceID(DeviceIndex)),
-	Multiplier(JOYSENSITIVITY_DEFAULT),
-	Enabled(true),
-	SettingsChanged(false)
+	SDLInputJoystick(int DeviceIndex) : DeviceIndex(DeviceIndex), Multiplier(1.0f) , Enabled(true)
 	{
-		if (SDL_IsGameController(DeviceIndex))
+		Device = SDL_JoystickOpen(DeviceIndex);
+		if(Device != NULL)
 		{
-			Mapping = SDL_GameControllerOpen(DeviceIndex);
-			Device = NULL;
+			NumAxes = SDL_JoystickNumAxes(Device);
+			NumHats = SDL_JoystickNumHats(Device);
 
-			DefaultAxes = DefaultControllerAxes;
-			DefaultAxesCount = sizeof(DefaultControllerAxes) / sizeof(DefaultAxisConfig);
-
-			if(Mapping != NULL)
-			{
-				NumAxes = SDL_CONTROLLER_AXIS_MAX;
-				NumHats = 0;
-
-				SetDefaultConfig();
-			}
+			SetDefaultConfig();
 		}
-		else
-		{
-			Device = SDL_JoystickOpen(DeviceIndex);
-			Mapping = NULL;
-
-			DefaultAxes = DefaultJoystickAxes;
-			DefaultAxesCount = sizeof(DefaultJoystickAxes) / sizeof(DefaultAxisConfig);
-
-			if(Device != NULL)
-			{
-				NumAxes = SDL_JoystickNumAxes(Device);
-				NumHats = SDL_JoystickNumHats(Device);
-
-				SetDefaultConfig();
-			}
-		}
-		M_LoadJoystickConfig(this);
 	}
 	~SDLInputJoystick()
 	{
-		if(IsValid() && SettingsChanged)
+		if(Device != NULL)
 			M_SaveJoystickConfig(this);
-		if (Mapping)
-			SDL_GameControllerClose(Mapping);
-		if (Device)
-			SDL_JoystickClose(Device);
+		SDL_JoystickClose(Device);
 	}
 
 	bool IsValid() const
 	{
-		return Device != NULL || Mapping != NULL;
+		return Device != NULL;
 	}
 
 	FString GetName()
 	{
-		return (Mapping)
-			? SDL_GameControllerName(Mapping)
-			: SDL_JoystickName(Device);
+		return SDL_JoystickName(Device);
 	}
 	float GetSensitivity()
 	{
@@ -112,7 +79,6 @@ public:
 	}
 	void SetSensitivity(float scale)
 	{
-		SettingsChanged = true;
 		Multiplier = scale;
 	}
 
@@ -136,145 +102,58 @@ public:
 	{
 		return Axes[axis].Multiplier;
 	}
-	float GetAxisDigitalThreshold(int axis)
-	{
-		return Axes[axis].DigitalThreshold;
-	}
-	EJoyCurve GetAxisResponseCurve(int axis)
-	{
-		return Axes[axis].ResponseCurvePreset;
-	}
-	float GetAxisResponseCurvePoint(int axis, int point)
-	{
-		return unsigned(point) < 4
-			? Axes[axis].ResponseCurve.pts[point]
-			: 0;
-	};
 
 	void SetAxisDeadZone(int axis, float zone)
 	{
-		SettingsChanged = true;
-		Axes[axis].DeadZone = clamp(zone, 0.f, 1.f);
+		Axes[axis].DeadZone = clamp(zone, MIN_DEADZONE, 1.f);
 	}
 	void SetAxisMap(int axis, EJoyAxis gameaxis)
 	{
-		SettingsChanged = true;
 		Axes[axis].GameAxis = gameaxis;
 	}
 	void SetAxisScale(int axis, float scale)
 	{
-		SettingsChanged = true;
 		Axes[axis].Multiplier = scale;
 	}
-	void SetAxisDigitalThreshold(int axis, float threshold)
-	{
-		SettingsChanged = true;
-		Axes[axis].DigitalThreshold = threshold;
-	}
-	void SetAxisResponseCurve(int axis, EJoyCurve preset)
-	{
-		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
-		SettingsChanged = true;
-		Axes[axis].ResponseCurvePreset = preset;
-		if (preset == JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurve = JOYCURVE[preset];
-	}
-	void SetAxisResponseCurvePoint(int axis, int point, float value)
-	{
-		if (unsigned(point) < 4)
-		{
-			SettingsChanged = true;
-			Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
-			Axes[axis].ResponseCurve.pts[point] = value;
-		}
-	}
 
 	// Used by the saver to not save properties that are at their defaults.
 	bool IsSensitivityDefault()
 	{
-		return Multiplier == JOYSENSITIVITY_DEFAULT;
+		return Multiplier == 1.0f;
 	}
 	bool IsAxisDeadZoneDefault(int axis)
 	{
-		if(axis >= DefaultAxesCount)
-			return Axes[axis].DeadZone == JOYDEADZONE_DEFAULT;
-		return Axes[axis].DeadZone == DefaultAxes[axis].DeadZone;
+		return Axes[axis].DeadZone <= MIN_DEADZONE;
 	}
 	bool IsAxisMapDefault(int axis)
 	{
-		if(axis >= DefaultAxesCount)
+		if(axis >= 5)
 			return Axes[axis].GameAxis == JOYAXIS_None;
-		return Axes[axis].GameAxis == DefaultAxes[axis].GameAxis;
+		return Axes[axis].GameAxis == DefaultAxes[axis];
 	}
 	bool IsAxisScaleDefault(int axis)
 	{
-		if(axis >= DefaultAxesCount)
-			return Axes[axis].Multiplier == JOYSENSITIVITY_DEFAULT;
-		return Axes[axis].Multiplier == DefaultAxes[axis].Multiplier;
-	}
-	bool IsAxisDigitalThresholdDefault(int axis)
-	{
-		if(axis >= DefaultAxesCount)
-			return Axes[axis].DigitalThreshold == JOYTHRESH_DEFAULT;
-		return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold;
-	}
-	bool IsAxisResponseCurveDefault(int axis)
-	{
-		if(axis >= DefaultAxesCount)
-			return Axes[axis].ResponseCurvePreset == JOYCURVE_DEFAULT;
-		return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset;
+		return Axes[axis].Multiplier == 1.0f;
 	}
 
 	void SetDefaultConfig()
 	{
-		if (Axes.size() == 0)
-		{
-			for(int i = 0;i < GetNumAxes();i++)
-			{
-				Axes.Push({});
-			}
-		}
-
 		for(int i = 0;i < GetNumAxes();i++)
 		{
-			if (Mapping) {
-				switch(i) {
-					case SDL_CONTROLLER_AXIS_LEFTX: Axes[i].Name = "Left Stick X"; break;
-					case SDL_CONTROLLER_AXIS_LEFTY: Axes[i].Name = "Left Stick Y"; break;
-					case SDL_CONTROLLER_AXIS_RIGHTX: Axes[i].Name = "Right Stick X"; break;
-					case SDL_CONTROLLER_AXIS_RIGHTY: Axes[i].Name = "Right Stick Y"; break;
-					case SDL_CONTROLLER_AXIS_TRIGGERLEFT: Axes[i].Name = "Left Trigger"; break;
-					case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: Axes[i].Name = "Right Trigger"; break;
-					default: Axes[i].Name.Format("Axis %d", i+1); break;
-				}
-			} else {
-				if(i < NumAxes)
-					Axes[i].Name.Format("Axis %d", i+1);
-				else
-					Axes[i].Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y');
-			}
-
-			Axes[i].Value = 0.0;
-			Axes[i].ButtonValue = 0;
-
-			if (i < DefaultAxesCount)
-			{
-				Axes[i].GameAxis = DefaultAxes[i].GameAxis;
-				Axes[i].DeadZone = DefaultAxes[i].DeadZone;
-				Axes[i].Multiplier = DefaultAxes[i].Multiplier;
-				Axes[i].DigitalThreshold = DefaultAxes[i].DigitalThreshold;
-				Axes[i].ResponseCurvePreset = DefaultAxes[i].ResponseCurvePreset;
-				Axes[i].ResponseCurve = JOYCURVE[DefaultAxes[i].ResponseCurvePreset];
-			}
+			AxisInfo info;
+			if(i < NumAxes)
+				info.Name.Format("Axis %d", i+1);
 			else
-			{
-				Axes[i].GameAxis = JOYAXIS_None;
-				Axes[i].DeadZone = JOYDEADZONE_DEFAULT;
-				Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT;
-				Axes[i].DigitalThreshold = JOYTHRESH_DEFAULT;
-				Axes[i].ResponseCurvePreset = JOYCURVE_DEFAULT;
-				Axes[i].ResponseCurve = JOYCURVE[JOYCURVE_DEFAULT];
-			}
+				info.Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y');
+			info.DeadZone = DEFAULT_DEADZONE;
+			info.Multiplier = 1.0f;
+			info.Value = 0.0;
+			info.ButtonValue = 0;
+			if(i >= 5)
+				info.GameAxis = JOYAXIS_None;
+			else
+				info.GameAxis = DefaultAxes[i];
+			Axes.Push(info);
 		}
 	}
 
@@ -282,10 +161,9 @@ public:
 	{
 		return Enabled;
 	}
-
+	
 	void SetEnabled(bool enabled)
 	{
-		SettingsChanged = true;
 		Enabled = enabled;
 	}
 
@@ -310,101 +188,63 @@ public:
 		}
 	}
 
-	void ProcessInput() {
+	void ProcessInput()
+	{
 		uint8_t buttonstate;
 
-		if (Mapping)
+		for (int i = 0; i < NumAxes; ++i)
 		{
-			// GameController API available
+			buttonstate = 0;
 
-			auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold;
-			auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold;
+			Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0;
+			Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
 
-			for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i)
+			// Map button to axis
+			// X and Y are handled differently so if we have 2 or more axes then we'll use that code instead.
+			if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS))
 			{
-				buttonstate = 0;
-
-				Axes[i].Value = SDL_GameControllerGetAxis(Mapping, static_cast(i))/32767.0;
-				Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
-				Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value);
+				Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
+				Axes[i].ButtonValue = buttonstate;
 			}
+		}
 
-			auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold;
-			auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold;
-
-			if (lastTriggerL != currTriggerL) Joy_GenerateButtonEvent(currTriggerL, KEY_PAD_LTRIGGER);
-			if (lastTriggerR != currTriggerR) Joy_GenerateButtonEvent(currTriggerR, KEY_PAD_RTRIGGER);
-
-			// todo: right stick
-			buttonstate = Joy_XYAxesToButtons(
-				abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value,
-				abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value
-			);
+		if(NumAxes > 1)
+		{
+			buttonstate = Joy_XYAxesToButtons(Axes[0].Value, Axes[1].Value);
 			Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
 			Axes[0].ButtonValue = buttonstate;
 		}
-		else
+
+		// Map POV hats to buttons and axes.  Why axes?  Well apparently I have
+		// a gamepad where the left control stick is a POV hat (instead of the
+		// d-pad like you would expect, no that's pressure sensitive).  Also
+		// KDE's joystick dialog maps them to axes as well.
+		for (int i = 0; i < NumHats; ++i)
 		{
-			// Joystick API fallback
+			AxisInfo &x = Axes[NumAxes + i*2];
+			AxisInfo &y = Axes[NumAxes + i*2 + 1];
 
-			for (int i = 0; i < NumAxes; ++i)
+			buttonstate = SDL_JoystickGetHat(Device, i);
+
+			// If we're going to assume that we can pass SDL's value into
+			// Joy_GenerateButtonEvents then we might as well assume the format here.
+			if(buttonstate & 0x1) // Up
+				y.Value = -1.0;
+			else if(buttonstate & 0x4) // Down
+				y.Value = 1.0;
+			else
+				y.Value = 0.0;
+			if(buttonstate & 0x2) // Left
+				x.Value = 1.0;
+			else if(buttonstate & 0x8) // Right
+				x.Value = -1.0;
+			else
+				x.Value = 0.0;
+
+			if(i < 4)
 			{
-				buttonstate = 0;
-
-				Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0;
-				Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate);
-				Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value);
-
-				// Map button to axis
-				// X and Y are handled differently so if we have 2 or more axes then we'll use that code instead.
-				if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS))
-				{
-					Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
-					Axes[i].ButtonValue = buttonstate;
-				}
-			}
-
-			if(NumAxes > 1)
-			{
-				buttonstate = Joy_XYAxesToButtons(
-					abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value,
-					abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value
-				);
-				Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
-				Axes[0].ButtonValue = buttonstate;
-			}
-
-			// Map POV hats to buttons and axes.  Why axes?  Well apparently I have
-			// a gamepad where the left control stick is a POV hat (instead of the
-			// d-pad like you would expect, no that's pressure sensitive).  Also
-			// KDE's joystick dialog maps them to axes as well.
-			for (int i = 0; i < NumHats; ++i)
-			{
-				AxisInfo &x = Axes[NumAxes + i*2];
-				AxisInfo &y = Axes[NumAxes + i*2 + 1];
-
-				buttonstate = SDL_JoystickGetHat(Device, i);
-
-				// If we're going to assume that we can pass SDL's value into
-				// Joy_GenerateButtonEvents then we might as well assume the format here.
-				if(buttonstate & 0x1) // Up
-					y.Value = -1.0;
-				else if(buttonstate & 0x4) // Down
-					y.Value = 1.0;
-				else
-					y.Value = 0.0;
-				if(buttonstate & 0x2) // Left
-					x.Value = 1.0;
-				else if(buttonstate & 0x8) // Right
-					x.Value = -1.0;
-				else
-					x.Value = 0.0;
-
-				if(i < 4)
-				{
-					Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4);
-					x.ButtonValue = buttonstate;
-				}
+				Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4);
+				x.ButtonValue = buttonstate;
 			}
 		}
 	}
@@ -415,66 +255,33 @@ protected:
 		FString Name;
 		float DeadZone;
 		float Multiplier;
-		float DigitalThreshold;
-		EJoyCurve ResponseCurvePreset;
-		CubicBezier ResponseCurve;
 		EJoyAxis GameAxis;
 		double Value;
 		uint8_t ButtonValue;
 	};
-	struct DefaultAxisConfig
-	{
-		float DeadZone;
-		EJoyAxis GameAxis;
-		float Multiplier;
-		float DigitalThreshold;
-		EJoyCurve ResponseCurvePreset;
-	};
-	static const DefaultAxisConfig DefaultJoystickAxes[5];
-	static const DefaultAxisConfig DefaultControllerAxes[6];
-	const DefaultAxisConfig * DefaultAxes;
-	int DefaultAxesCount;
+	static const EJoyAxis DefaultAxes[5];
 
 	int					DeviceIndex;
-	int					InstanceID;
 	SDL_Joystick		*Device;
-	SDL_GameController	*Mapping;
 
 	float				Multiplier;
 	bool				Enabled;
 	TArray	Axes;
 	int					NumAxes;
 	int					NumHats;
-	bool 				SettingsChanged;
 
 	friend class SDLInputJoystickManager;
 };
 
 // [Nash 4 Feb 2024] seems like on Linux, the third axis is actually the Left Trigger, resulting in the player uncontrollably looking upwards.
-const SDLInputJoystick::DefaultAxisConfig SDLInputJoystick::DefaultJoystickAxes[5] = {
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_DEFAULT, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT}
-};
-
-// Defaults if we have access to the GameController API for this device
-const SDLInputJoystick::DefaultAxisConfig SDLInputJoystick::DefaultControllerAxes[6] = {
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT},
-	{JOYDEADZONE_DEFAULT, JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT},
-};
+const EJoyAxis SDLInputJoystick::DefaultAxes[5] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_None, JOYAXIS_Yaw, JOYAXIS_Pitch};
 
 class SDLInputJoystickManager
 {
 public:
 	SDLInputJoystickManager()
 	{
-		UpdateDeviceList();
+		this->UpdateDeviceList();
 	}
 
 	void UpdateDeviceList()
@@ -511,19 +318,6 @@ public:
 			if(Joysticks[i]->Enabled) Joysticks[i]->ProcessInput();
 	}
 
-	bool IsJoystickEnabled(int instanceID)
-	{
-		for(unsigned int i = 0; i < Joysticks.Size(); i++)
-		{
-			if (Joysticks[i]->InstanceID != instanceID)
-			{
-				continue;
-			}
-			return Joysticks[i]->Enabled;
-		}
-		return false;
-	}
-
 protected:
 	TDeletingArray Joysticks;
 };
@@ -532,7 +326,7 @@ static SDLInputJoystickManager *JoystickManager;
 void I_StartupJoysticks()
 {
 #ifndef NO_SDL_JOYSTICK
-	if(SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) >= 0)
+	if(SDL_InitSubSystem(SDL_INIT_JOYSTICK) >= 0)
 		JoystickManager = new SDLInputJoystickManager();
 #endif
 }
@@ -541,7 +335,7 @@ void I_ShutdownInput()
 	if(JoystickManager)
 	{
 		delete JoystickManager;
-		SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
+		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
 	}
 }
 
@@ -571,16 +365,6 @@ void I_ProcessJoysticks()
 		JoystickManager->ProcessInput();
 }
 
-void I_JoyConsumeEvent(int instanceID, event_t * event)
-{
-	if (event->type == EV_KeyDown)
-	{
-		bool okay = use_joystick && JoystickManager && JoystickManager->IsJoystickEnabled(instanceID);
-		if (!okay) return;
-	}
-	D_PostEvent(event);
-}
-
 IJoystickConfig *I_UpdateDeviceList()
 {
 	JoystickManager->UpdateDeviceList();
diff --git a/src/common/platform/win32/i_dijoy.cpp b/src/common/platform/win32/i_dijoy.cpp
index 2340bfc84..33c4574c9 100644
--- a/src/common/platform/win32/i_dijoy.cpp
+++ b/src/common/platform/win32/i_dijoy.cpp
@@ -146,6 +146,7 @@ public:
 
 // MACROS ------------------------------------------------------------------
 
+#define DEFAULT_DEADZONE			0.25f
 
 // TYPES -------------------------------------------------------------------
 
@@ -169,23 +170,15 @@ public:
 	EJoyAxis GetAxisMap(int axis);
 	const char *GetAxisName(int axis);
 	float GetAxisScale(int axis);
-	float GetAxisDigitalThreshold(int axis);
-	EJoyCurve GetAxisResponseCurve(int axis);
-	float GetAxisResponseCurvePoint(int axis, int point);
 
 	void SetAxisDeadZone(int axis, float deadzone);
 	void SetAxisMap(int axis, EJoyAxis gameaxis);
 	void SetAxisScale(int axis, float scale);
-	void SetAxisDigitalThreshold(int axis, float threshold);
-	void SetAxisResponseCurve(int axis, EJoyCurve preset);
-	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	bool IsSensitivityDefault();
 	bool IsAxisDeadZoneDefault(int axis);
 	bool IsAxisMapDefault(int axis);
 	bool IsAxisScaleDefault(int axis);
-	bool IsAxisDigitalThresholdDefault(int axis);
-	bool IsAxisResponseCurveDefault(int axis);
 
 	bool GetEnabled();
 	void SetEnabled(bool enabled);
@@ -208,9 +201,6 @@ protected:
 		float Value;
 		float DeadZone, DefaultDeadZone;
 		float Multiplier, DefaultMultiplier;
-		float DigitalThreshold, DefaultDigitalThreshold;
-		EJoyCurve ResponseCurvePreset, DefaultResponseCurvePreset;
-		CubicBezier ResponseCurve;
 		EJoyAxis GameAxis, DefaultGameAxis;
 		uint8_t ButtonValue;
 	};
@@ -464,21 +454,16 @@ void FDInputJoystick::ProcessInput()
 		axisval = (value - info->Min) * 2.0 / (info->Max - info->Min) - 1.0;
 		// Cancel out dead zone
 		axisval = Joy_RemoveDeadZone(axisval, info->DeadZone, &buttonstate);
-		axisval = Joy_ApplyResponseCurveBezier(info->ResponseCurve, axisval);
 		info->Value = float(axisval);
 		if (i < NUM_JOYAXISBUTTONS && (i > 2 || Axes.Size() == 1))
 		{
-			if (abs(axisval) < info->DigitalThreshold) buttonstate = 0;
 			Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2);
 		}
 		else if (i == 1)
 		{
 			// Since we sorted the axes, we know that the first two are definitely X and Y.
 			// They are probably a single stick, so use angular position to determine buttons.
-			buttonstate = Joy_XYAxesToButtons(
-				(abs(Axes[0].Value) < Axes[0].DigitalThreshold) ? 0 : Axes[0].Value,
-				(abs(axisval)       < info->DigitalThreshold)   ? 0 : axisval
-			);
+			buttonstate = Joy_XYAxesToButtons(Axes[0].Value, axisval);
 			Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS);
 		}
 		info->ButtonValue = buttonstate;
@@ -776,54 +761,38 @@ void FDInputJoystick::SetDefaultConfig()
 {
 	unsigned i;
 
-	Multiplier = JOYSENSITIVITY_DEFAULT;
+	Multiplier = 1;
 	for (i = 0; i < Axes.Size(); ++i)
 	{
-		Axes[i].DeadZone = JOYDEADZONE_DEFAULT;
-		Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT;
+		Axes[i].DeadZone = DEFAULT_DEADZONE;
+		Axes[i].Multiplier = 1;
 		Axes[i].GameAxis = JOYAXIS_None;
-		Axes[i].DigitalThreshold = JOYTHRESH_DEFAULT;
-		Axes[i].ResponseCurvePreset = JOYCURVE_DEFAULT;
-		Axes[i].ResponseCurve = JOYCURVE[JOYCURVE_DEFAULT];
 	}
 	// Triggers on a 360 controller have a much smaller deadzone.
 	if (Axes.Size() == 5 && Axes[4].Guid == GUID_ZAxis)
 	{
 		Axes[4].DeadZone = 30 / 256.f;
-		Axes[4].DigitalThreshold = JOYTHRESH_TRIGGER;
 	}
 	// Two axes? Horizontal is yaw and vertical is forward.
 	if (Axes.Size() == 2)
 	{
 		Axes[0].GameAxis = JOYAXIS_Yaw;
-		Axes[0].DigitalThreshold = JOYTHRESH_STICK_X;
-
 		Axes[1].GameAxis = JOYAXIS_Forward;
-		Axes[1].DigitalThreshold = JOYTHRESH_STICK_Y;
 	}
 	// Three axes? First two are movement, third is yaw.
 	else if (Axes.Size() >= 3)
 	{
 		Axes[0].GameAxis = JOYAXIS_Side;
-		Axes[0].DigitalThreshold = JOYTHRESH_STICK_X;
-
 		Axes[1].GameAxis = JOYAXIS_Forward;
-		Axes[1].DigitalThreshold = JOYTHRESH_STICK_Y;
-
 		Axes[2].GameAxis = JOYAXIS_Yaw;
-		Axes[2].DigitalThreshold = JOYTHRESH_STICK_X;
-
 		// Four axes? First two are movement, last two are looking around.
 		if (Axes.Size() >= 4)
 		{
-			Axes[3].GameAxis = JOYAXIS_Pitch;
-			// Axes[3].Multiplier = 0.75f;
-			Axes[3].DigitalThreshold = JOYTHRESH_STICK_Y;
+			Axes[3].GameAxis = JOYAXIS_Pitch;	Axes[3].Multiplier = 0.75f;
 			// Five axes? Use the fifth one for moving up and down.
 			if (Axes.Size() >= 5)
 			{
 				Axes[4].GameAxis = JOYAXIS_Up;
-				Axes[4].DigitalThreshold = JOYTHRESH_STICK_Y;
 			}
 		}
 	}
@@ -836,8 +805,6 @@ void FDInputJoystick::SetDefaultConfig()
 		Axes[i].DefaultDeadZone = Axes[i].DeadZone;
 		Axes[i].DefaultMultiplier = Axes[i].Multiplier;
 		Axes[i].DefaultGameAxis = Axes[i].GameAxis;
-		Axes[i].DefaultDigitalThreshold = Axes[i].DigitalThreshold;
-		Axes[i].DefaultResponseCurvePreset = Axes[i].ResponseCurvePreset;
 	}
 }
 
@@ -882,7 +849,7 @@ void FDInputJoystick::SetSensitivity(float scale)
 
 bool FDInputJoystick::IsSensitivityDefault()
 {
-	return Multiplier == JOYSENSITIVITY_DEFAULT;
+	return Multiplier == 1;
 }
 
 //===========================================================================
@@ -956,51 +923,6 @@ float FDInputJoystick::GetAxisScale(int axis)
 	return Axes[axis].Multiplier;
 }
 
-//===========================================================================
-//
-// FDInputJoystick :: GetAxisDigitalThreshold
-//
-//===========================================================================
-
-float FDInputJoystick::GetAxisDigitalThreshold(int axis)
-{
-	if (unsigned(axis) >= Axes.Size())
-	{
-		return JOYTHRESH_DEFAULT;
-	}
-	return Axes[axis].DigitalThreshold;
-}
-
-//===========================================================================
-//
-// FDInputJoystick :: GetAxisResponseCurve
-//
-//===========================================================================
-
-EJoyCurve FDInputJoystick::GetAxisResponseCurve(int axis)
-{
-	if (unsigned(axis) >= Axes.Size())
-	{
-		return JOYCURVE_DEFAULT;
-	}
-	return Axes[axis].ResponseCurvePreset;
-}
-
-//===========================================================================
-//
-// FDInputJoystick :: GetAxisResponseCurvePoint
-//
-//===========================================================================
-
-float FDInputJoystick::GetAxisResponseCurvePoint(int axis, int point)
-{
-	if (unsigned(axis) >= Axes.Size() || unsigned(point) >= 4)
-	{
-		return 0;
-	}
-	return Axes[axis].ResponseCurve.pts[point];
-}
-
 //===========================================================================
 //
 // FDInputJoystick :: SetAxisDeadZone
@@ -1043,52 +965,6 @@ void FDInputJoystick::SetAxisScale(int axis, float scale)
 	}
 }
 
-//===========================================================================
-//
-// FDInputJoystick :: SetAxisDigitalThreshold
-//
-//===========================================================================
-
-void FDInputJoystick::SetAxisDigitalThreshold(int axis, float threshold)
-{
-	if (unsigned(axis) < Axes.Size())
-	{
-		Axes[axis].DigitalThreshold = threshold;
-	}
-}
-
-//===========================================================================
-//
-// FDInputJoystick :: SetAxisResponseCurve
-//
-//===========================================================================
-
-void FDInputJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset)
-{
-	if (unsigned(axis) < Axes.Size())
-	{
-		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurvePreset = preset;
-		if (preset == JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurve = JOYCURVE[preset];
-	}
-}
-
-//===========================================================================
-//
-// FDInputJoystick :: SetAxisResponseCurvePoint
-//
-//===========================================================================
-
-void FDInputJoystick::SetAxisResponseCurvePoint(int axis, int point, float value)
-{
-	if (unsigned(axis) < Axes.Size() && unsigned(point) < 4)
-	{
-		Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
-		Axes[axis].ResponseCurve.pts[point] = value;
-	}
-}
-
 //===========================================================================
 //
 // FDInputJoystick :: IsAxisDeadZoneDefault
@@ -1119,36 +995,6 @@ bool FDInputJoystick::IsAxisScaleDefault(int axis)
 	return true;
 }
 
-//===========================================================================
-//
-// FDInputJoystick :: IsAxisDigitalThresholdDefault
-//
-//===========================================================================
-
-bool FDInputJoystick::IsAxisDigitalThresholdDefault(int axis)
-{
-	if (unsigned(axis) < Axes.Size())
-	{
-		return Axes[axis].DigitalThreshold == Axes[axis].DefaultDigitalThreshold;
-	}
-	return true;
-}
-
-//===========================================================================
-//
-// FDInputJoystick :: IsAxisResponseCurveDefault
-//
-//===========================================================================
-
-bool FDInputJoystick::IsAxisResponseCurveDefault(int axis)
-{
-	if (unsigned(axis) < Axes.Size())
-	{
-		return Axes[axis].ResponseCurvePreset == Axes[axis].DefaultResponseCurvePreset;
-	}
-	return true;
-}
-
 //===========================================================================
 //
 // FDInputJoystick :: GetEnabled
diff --git a/src/common/platform/win32/i_rawps2.cpp b/src/common/platform/win32/i_rawps2.cpp
index 09bb314b0..2f89739bd 100644
--- a/src/common/platform/win32/i_rawps2.cpp
+++ b/src/common/platform/win32/i_rawps2.cpp
@@ -48,6 +48,7 @@
 
 // MACROS ------------------------------------------------------------------
 
+#define DEFAULT_DEADZONE			0.25f
 #define STATUS_SWITCH_TIME			3
 
 #define VID_PLAY_COM							0x0b43
@@ -103,23 +104,15 @@ public:
 	EJoyAxis GetAxisMap(int axis);
 	const char *GetAxisName(int axis);
 	float GetAxisScale(int axis);
-	float GetAxisDigitalThreshold(int axis);
-	EJoyCurve GetAxisResponseCurve(int axis);
-	float GetAxisResponseCurvePoint(int axis, int point);
 
 	void SetAxisDeadZone(int axis, float deadzone);
 	void SetAxisMap(int axis, EJoyAxis gameaxis);
 	void SetAxisScale(int axis, float scale);
-	void SetAxisDigitalThreshold(int axis, float threshold);
-	void SetAxisResponseCurve(int axis, EJoyCurve preset);
-	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	bool IsSensitivityDefault();
 	bool IsAxisDeadZoneDefault(int axis);
 	bool IsAxisMapDefault(int axis);
 	bool IsAxisScaleDefault(int axis);
-	bool IsAxisDigitalThresholdDefault(int axis);
-	bool IsAxisResponseCurveDefault(int axis);
 
 	bool GetEnabled();
 	void SetEnabled(bool enabled);
@@ -137,9 +130,6 @@ protected:
 		float Value;
 		float DeadZone;
 		float Multiplier;
-		float DigitalThreshold;
-		EJoyCurve ResponseCurvePreset;
-		CubicBezier ResponseCurve;
 		EJoyAxis GameAxis;
 		uint8_t ButtonValue;
 	};
@@ -147,8 +137,6 @@ protected:
 	{
 		EJoyAxis GameAxis;
 		float Multiplier;
-		float DigitalThreshold;
-		EJoyCurve ResponseCurvePreset;
 	};
 	enum
 	{
@@ -369,11 +357,11 @@ static const char *AxisNames[] =
 
 FRawPS2Controller::DefaultAxisConfig FRawPS2Controller::DefaultAxes[NUM_AXES] =
 {
-	// Game axis, multiplier, digital threshold, response curve A, response curve B
-	{ JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbLX
-	{ JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbLY
-	{ JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbRX
-	{ JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbRY
+	// Game axis, multiplier
+	{ JOYAXIS_Side, 1 },		// ThumbLX
+	{ JOYAXIS_Forward, 1 },		// ThumbLY
+	{ JOYAXIS_Yaw, 1 },			// ThumbRX
+	{ JOYAXIS_Pitch, 0.75 },	// ThumbRY
 };
 
 // CODE --------------------------------------------------------------------
@@ -564,16 +552,11 @@ void FRawPS2Controller::ProcessThumbstick(int value1, AxisInfo *axis1, int value
 	axisval2 = value2 * (2.0 / 255) - 1.0;
 	axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL);
 	axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL);
-	axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1);
-	axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2);
 	axis1->Value = float(axisval1);
 	axis2->Value = float(axisval2);
 
 	// We store all four buttons in the first axis and ignore the second.
-	buttonstate = Joy_XYAxesToButtons(
-		(abs(axisval1) < axis1->DigitalThreshold) ? 0 : axisval1,
-		(abs(axisval2) < axis2->DigitalThreshold) ? 0 : axisval2
-	);
+	buttonstate = Joy_XYAxesToButtons(axisval1, axisval2);
 	Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base);
 	axis1->ButtonValue = buttonstate;
 }
@@ -661,10 +644,10 @@ void FRawPS2Controller::AddAxes(float axes[NUM_JOYAXIS])
 
 void FRawPS2Controller::SetDefaultConfig()
 {
-	Multiplier = JOYSENSITIVITY_DEFAULT;
+	Multiplier = 1;
 	for (int i = 0; i < NUM_AXES; ++i)
 	{
-		Axes[i].DeadZone = JOYDEADZONE_DEFAULT;
+		Axes[i].DeadZone = DEFAULT_DEADZONE;
 		Axes[i].GameAxis = DefaultAxes[i].GameAxis;
 		Axes[i].Multiplier = DefaultAxes[i].Multiplier;
 	}
@@ -729,7 +712,7 @@ void FRawPS2Controller::SetSensitivity(float scale)
 
 bool FRawPS2Controller::IsSensitivityDefault()
 {
-	return Multiplier == JOYSENSITIVITY_DEFAULT;
+	return Multiplier == 1;
 }
 
 //==========================================================================
@@ -803,51 +786,6 @@ float FRawPS2Controller::GetAxisScale(int axis)
 	return 0;
 }
 
-//==========================================================================
-//
-// FRawPS2Controller :: GetAxisDigitalThreshold
-//
-//==========================================================================
-
-float FRawPS2Controller::GetAxisDigitalThreshold(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].DigitalThreshold;
-	}
-	return JOYTHRESH_DEFAULT;
-}
-
-//==========================================================================
-//
-// FRawPS2Controller :: GetAxisResponseCurve
-//
-//==========================================================================
-
-EJoyCurve FRawPS2Controller::GetAxisResponseCurve(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].ResponseCurvePreset;
-	}
-	return JOYCURVE_DEFAULT;
-}
-
-//==========================================================================
-//
-// FRawPS2Controller :: GetAxisResponseCurvePoint
-//
-//==========================================================================
-
-float FRawPS2Controller::GetAxisResponseCurvePoint(int axis, int point)
-{
-	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
-	{
-		return Axes[axis].ResponseCurve.pts[point];
-	}
-	return 0;
-}
-
 //==========================================================================
 //
 // FRawPS2Controller :: SetAxisDeadZone
@@ -890,52 +828,6 @@ void FRawPS2Controller::SetAxisScale(int axis, float scale)
 	}
 }
 
-//==========================================================================
-//
-// FRawPS2Controller :: SetAxisDigitalThreshold
-//
-//==========================================================================
-
-void FRawPS2Controller::SetAxisDigitalThreshold(int axis, float threshold)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		Axes[axis].DigitalThreshold = threshold;
-	}
-}
-
-//==========================================================================
-//
-// FRawPS2Controller :: SetAxisResponseCurve
-//
-//==========================================================================
-
-void FRawPS2Controller::SetAxisResponseCurve(int axis, EJoyCurve preset)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurvePreset = preset;
-		if (preset == JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurve = JOYCURVE[preset];
-	}
-}
-
-//==========================================================================
-//
-// FRawPS2Controller :: SetAxisResponseCurvePoint
-//
-//==========================================================================
-
-void FRawPS2Controller::SetAxisResponseCurvePoint(int axis, int point, float value)
-{
-	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
-	{
-		Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
-		Axes[axis].ResponseCurve.pts[point] = value;
-	}
-}
-
 //===========================================================================
 //
 // FRawPS2Controller :: IsAxisDeadZoneDefault
@@ -946,7 +838,7 @@ bool FRawPS2Controller::IsAxisDeadZoneDefault(int axis)
 {
 	if (unsigned(axis) < NUM_AXES)
 	{
-		return Axes[axis].DeadZone == JOYDEADZONE_DEFAULT;
+		return Axes[axis].DeadZone == DEFAULT_DEADZONE;
 	}
 	return true;
 }
@@ -966,36 +858,6 @@ bool FRawPS2Controller::IsAxisScaleDefault(int axis)
 	return true;
 }
 
-//===========================================================================
-//
-// FRawPS2Controller :: IsAxisDigitalThresholdDefault
-//
-//===========================================================================
-
-bool FRawPS2Controller::IsAxisDigitalThresholdDefault(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold;
-	}
-	return true;
-}
-
-//===========================================================================
-//
-// FRawPS2Controller :: IsAxisResponseCurveDefault
-//
-//===========================================================================
-
-bool FRawPS2Controller::IsAxisResponseCurveDefault(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset;
-	}
-	return true;
-}
-
 //===========================================================================
 //
 // FRawPS2Controller :: GetEnabled
diff --git a/src/common/platform/win32/i_xinput.cpp b/src/common/platform/win32/i_xinput.cpp
index 890888a4b..0f3c19237 100644
--- a/src/common/platform/win32/i_xinput.cpp
+++ b/src/common/platform/win32/i_xinput.cpp
@@ -94,23 +94,15 @@ public:
 	EJoyAxis GetAxisMap(int axis);
 	const char *GetAxisName(int axis);
 	float GetAxisScale(int axis);
-	float GetAxisDigitalThreshold(int axis);
-	EJoyCurve GetAxisResponseCurve(int axis);
-	float GetAxisResponseCurvePoint(int axis, int point);
 
 	void SetAxisDeadZone(int axis, float deadzone);
 	void SetAxisMap(int axis, EJoyAxis gameaxis);
 	void SetAxisScale(int axis, float scale);
-	void SetAxisDigitalThreshold(int axis, float threshold);
-	void SetAxisResponseCurve(int axis, EJoyCurve preset);
-	void SetAxisResponseCurvePoint(int axis, int point, float value);
 
 	bool IsSensitivityDefault();
 	bool IsAxisDeadZoneDefault(int axis);
 	bool IsAxisMapDefault(int axis);
 	bool IsAxisScaleDefault(int axis);
-	bool IsAxisDigitalThresholdDefault(int axis);
-	bool IsAxisResponseCurveDefault(int axis);
 
 	bool GetEnabled();
 	void SetEnabled(bool enabled);
@@ -130,17 +122,12 @@ protected:
 		float Multiplier;
 		EJoyAxis GameAxis;
 		uint8_t ButtonValue;
-		float DigitalThreshold;
-		EJoyCurve ResponseCurvePreset;
-		CubicBezier ResponseCurve;
 	};
 	struct DefaultAxisConfig
 	{
 		float DeadZone;
 		EJoyAxis GameAxis;
 		float Multiplier;
-		float DigitalThreshold;
-		EJoyCurve ResponseCurvePreset;
 	};
 	enum
 	{
@@ -224,13 +211,13 @@ static const char *AxisNames[] =
 
 FXInputController::DefaultAxisConfig FXInputController::DefaultAxes[NUM_AXES] =
 {
-	// Dead zone, game axis, multiplier, digitalthreshold, curveA, curveB
-	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f,  JOYAXIS_Side,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbLX
-	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f,  JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbLY
-	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw,     JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbRX
-	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch,   JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbRY
-	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f,      JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT }, // LeftTrigger
-	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f,      JOYAXIS_None,    JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT }  // RightTrigger
+	// Dead zone, game axis, multiplier
+	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, 1 },		// ThumbLX
+	{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, 1 },	// ThumbLY
+	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, 1 },		// ThumbRX
+	{ XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, 0.75 },	// ThumbRY
+	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 },			// LeftTrigger
+	{ XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 }			// RightTrigger
 };
 
 // CODE --------------------------------------------------------------------
@@ -340,16 +327,11 @@ void FXInputController::ProcessThumbstick(int value1, AxisInfo *axis1,
 	axisval2 = (value2 - SHRT_MIN) * 2.0 / 65536 - 1.0;
 	axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL);
 	axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL);
-	axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1);
-	axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2);
 	axis1->Value = float(axisval1);
 	axis2->Value = float(axisval2);
 
 	// We store all four buttons in the first axis and ignore the second.
-	buttonstate = Joy_XYAxesToButtons(
-		abs(axisval1) < axis1->DigitalThreshold ? 0: axisval1,
-		abs(axisval2) < axis2->DigitalThreshold ? 0: axisval2
-	);
+	buttonstate = Joy_XYAxesToButtons(axisval1, axisval2);
 	Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base);
 	axis1->ButtonValue = buttonstate;
 }
@@ -369,11 +351,6 @@ void FXInputController::ProcessTrigger(int value, AxisInfo *axis, int base)
 	double axisval;
 
 	axisval = Joy_RemoveDeadZone(value / 256.0, axis->DeadZone, &buttonstate);
-	axisval = Joy_ApplyResponseCurveBezier(axis->ResponseCurve, axisval);
-
-	// TODO: probably just put this into Joy_RemoveDeadZone
-	if (abs(axisval) < axis->DigitalThreshold) buttonstate = 0;
-
 	Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 1, base);
 	axis->ButtonValue = buttonstate;
 	axis->Value = float(axisval);
@@ -454,15 +431,12 @@ void FXInputController::AddAxes(float axes[NUM_JOYAXIS])
 
 void FXInputController::SetDefaultConfig()
 {
-	Multiplier = JOYSENSITIVITY_DEFAULT;
+	Multiplier = 1;
 	for (int i = 0; i < NUM_AXES; ++i)
 	{
 		Axes[i].DeadZone = DefaultAxes[i].DeadZone;
 		Axes[i].GameAxis = DefaultAxes[i].GameAxis;
 		Axes[i].Multiplier = DefaultAxes[i].Multiplier;
-		Axes[i].DigitalThreshold = DefaultAxes[i].DigitalThreshold;
-		Axes[i].ResponseCurve = JOYCURVE[DefaultAxes[i].ResponseCurvePreset];
-		Axes[i].ResponseCurvePreset = DefaultAxes[i].ResponseCurvePreset;
 	}
 }
 
@@ -520,7 +494,7 @@ void FXInputController::SetSensitivity(float scale)
 
 bool FXInputController::IsSensitivityDefault()
 {
-	return Multiplier == JOYSENSITIVITY_DEFAULT;
+	return Multiplier == 1;
 }
 
 //==========================================================================
@@ -591,51 +565,6 @@ float FXInputController::GetAxisScale(int axis)
 	{
 		return Axes[axis].Multiplier;
 	}
-	return JOYSENSITIVITY_DEFAULT;
-}
-
-//==========================================================================
-//
-// FXInputController :: GetAxisDigitalThreshold
-//
-//==========================================================================
-
-float FXInputController::GetAxisDigitalThreshold(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].DigitalThreshold;
-	}
-	return JOYTHRESH_DEFAULT;
-}
-
-//==========================================================================
-//
-// FXInputController :: GetAxisResponseCurve
-//
-//==========================================================================
-
-EJoyCurve FXInputController::GetAxisResponseCurve(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].ResponseCurvePreset;
-	}
-	return JOYCURVE_DEFAULT;
-}
-
-//==========================================================================
-//
-// FXInputController :: GetAxisResponseCurvePoint
-//
-//==========================================================================
-
-float FXInputController::GetAxisResponseCurvePoint(int axis, int point)
-{
-	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
-	{
-		return Axes[axis].ResponseCurve.pts[point];
-	}
 	return 0;
 }
 
@@ -681,52 +610,6 @@ void FXInputController::SetAxisScale(int axis, float scale)
 	}
 }
 
-//==========================================================================
-//
-// FXInputController :: SetAxisDigitalThreshold
-//
-//==========================================================================
-
-void FXInputController::SetAxisDigitalThreshold(int axis, float threshold)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		Axes[axis].DigitalThreshold = threshold;
-	}
-}
-
-//==========================================================================
-//
-// FXInputController :: SetAxisResponseCurve
-//
-//==========================================================================
-
-void FXInputController::SetAxisResponseCurve(int axis, EJoyCurve preset)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurvePreset = preset;
-		if (preset == JOYCURVE_CUSTOM) return;
-		Axes[axis].ResponseCurve = JOYCURVE[preset];
-	}
-}
-
-//==========================================================================
-//
-// FXInputController :: SetAxisResponseCurvePoint
-//
-//==========================================================================
-
-void FXInputController::SetAxisResponseCurvePoint(int axis, int point, float value)
-{
-	if (unsigned(axis) < NUM_AXES && unsigned(point) < 4)
-	{
-		Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM;
-		Axes[axis].ResponseCurve.pts[point] = value;
-	}
-}
-
 //===========================================================================
 //
 // FXInputController :: IsAxisDeadZoneDefault
@@ -757,36 +640,6 @@ bool FXInputController::IsAxisScaleDefault(int axis)
 	return true;
 }
 
-//===========================================================================
-//
-// FXInputController :: IsAxisDigitalThresholdDefault
-//
-//===========================================================================
-
-bool FXInputController::IsAxisDigitalThresholdDefault(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold;
-	}
-	return true;
-}
-
-//===========================================================================
-//
-// FXInputController :: IsAxisResponseCurveDefault
-//
-//===========================================================================
-
-bool FXInputController::IsAxisResponseCurveDefault(int axis)
-{
-	if (unsigned(axis) < NUM_AXES)
-	{
-		return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset;
-	}
-	return true;
-}
-
 //===========================================================================
 //
 // FXInputController :: GetEnabled
diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt
index 2577b9aa7..046a7f1f6 100644
--- a/wadsrc/static/menudef.txt
+++ b/wadsrc/static/menudef.txt
@@ -837,15 +837,6 @@ OptionMenu "JoystickOptions" protected
 	Title "$JOYMNU_OPTIONS"
 }
 
-OptionValue "JoyAxisCurveNames"
-{
-	-1, "$OPTVAL_CUSTOM"
-	0, "$OPTVAL_DEFAULT"
-	1, "$OPTVAL_LINEAR"
-	2, "$OPTVAL_QUADRATIC"
-	3, "$OPTVAL_CUBIC"
-}
-
 OptionValue "JoyAxisMapNames"
 {
 	-1, "$OPTVAL_NONE"
diff --git a/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs b/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs
index 1b813818f..02669c1a3 100644
--- a/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs
+++ b/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs
@@ -130,94 +130,7 @@ class OptionMenuSliderJoyDeadZone : OptionMenuSliderBase
 
 //=============================================================================
 //
-//
-//
-//=============================================================================
-
-class OptionMenuSliderJoyDigitalThreshold : OptionMenuSliderBase
-{
-	int mAxis;
-	int mNeg;
-	JoystickConfig mJoy;
-
-	OptionMenuSliderJoyDigitalThreshold Init(String label, int axis, double min, double max, double step, int showval, JoystickConfig joy)
-	{
-		Super.Init(label, min, max, step, showval);
-		mAxis = axis;
-		mNeg = 1;
-		mJoy = joy;
-		return self;
-	}
-
-	override double GetSliderValue()
-	{
-		double d = mJoy.GetAxisDigitalThreshold(mAxis);
-		mNeg = d < 0? -1:1;
-		return d;
-	}
-
-	override void SetSliderValue(double val)
-	{
-		mJoy.SetAxisDigitalThreshold(mAxis, val * mNeg);
-	}
-}
-
-//=============================================================================
-//
-//
-//
-//=============================================================================
-
-class OptionMenuItemJoyCurve : OptionMenuItemOptionBase
-{
-	int mAxis;
-	JoystickConfig mJoy;
-
-	OptionMenuItemJoyCurve Init(String label, int axis, Name values, int center, JoystickConfig joy)
-	{
-		Super.Init(label, 'none', values, null, center);
-		mAxis = axis;
-		mJoy = joy;
-		return self;
-	}
-
-	override int GetSelection()
-	{
-		double f = mJoy.GetAxisResponseCurve(mAxis);
-		let opt = OptionValues.GetCount(mValues);
-		if (opt > 0)
-		{
-			// Map from joystick curve to menu selection.
-			for(int i = 0; i < opt; i++)
-			{
-				if (f ~== OptionValues.GetValue(mValues, i))
-				{
-					return i;
-				}
-			}
-		}
-		return JoystickConfig.JOYCURVE_CUSTOM;
-	}
-
-	override void SetSelection(int selection)
-	{
-		let opt = OptionValues.GetCount(mValues);
-		// Map from menu selection to joystick curve.
-		if (opt == 0 || selection >= opt)
-		{
-			selection = JoystickConfig.JOYCURVE_DEFAULT;
-		}
-		else
-		{
-			selection = int(OptionValues.GetValue(mValues, selection));
-		}
-		mJoy.setAxisResponseCurve(mAxis, selection);
-	}
-}
-
-//=============================================================================
-//
-//
+// 
 //
 //=============================================================================
 
@@ -427,25 +340,8 @@ class OptionMenuItemJoyConfigMenu : OptionMenuItemSubmenu
 					opt.mItems.Push(it);
 					it = new("OptionMenuItemInverter").Init("$JOYMNU_INVERT", i, false, joy);
 					opt.mItems.Push(it);
-					it = new("OptionMenuItemJoyCurve").Init("$JOYMNU_CURVE", i, "JoyAxisCurveNames", false, joy);
-					opt.mItems.Push(it);
-					// // is there a way to do something like this?
-					// // add extra options if the selected value is something specific?
-					// if (joy.GetAxisResponseCurve(i) == JoystickConfig.JOYCURVE_CUSTOM)
-					// {
-					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_X1", false);
-					// 	opt.mItems.Push(it);
-					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_Y1", false);
-					// 	opt.mItems.Push(it);
-					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_X2", false);
-					// 	opt.mItems.Push(it);
-					// 	it = new("OptionMenuItemStaticText").Init("$JOYMNU_CURVE_Y2", false);
-					// 	opt.mItems.Push(it);
-					// }
 					it = new("OptionMenuSliderJoyDeadZone").Init("$JOYMNU_DEADZONE", i, 0, 0.9, 0.05, 3, joy);
 					opt.mItems.Push(it);
-					it = new("OptionMenuSliderJoyDigitalThreshold").Init("$JOYMNU_THRESHOLD", i, 0, 0.9, 0.05, 3, joy);
-					opt.mItems.Push(it);
 				}
 			}
 			else
diff --git a/wadsrc/static/zscript/engine/ui/menu/menu.zs b/wadsrc/static/zscript/engine/ui/menu/menu.zs
index ed98a97e9..7a0ef2036 100644
--- a/wadsrc/static/zscript/engine/ui/menu/menu.zs
+++ b/wadsrc/static/zscript/engine/ui/menu/menu.zs
@@ -68,16 +68,6 @@ struct JoystickConfig native version("2.4")
 		NUM_JOYAXIS,
 	};
 
-	enum EJoyCurve {
-		JOYCURVE_CUSTOM = -1,
-		JOYCURVE_DEFAULT,
-		JOYCURVE_LINEAR,
-		JOYCURVE_QUADRATIC,
-		JOYCURVE_CUBIC,
-
-		NUM_JOYCURVE
-	};
-
 	native float GetSensitivity();
 	native void SetSensitivity(float scale);
 
@@ -87,15 +77,6 @@ struct JoystickConfig native version("2.4")
 	native float GetAxisDeadZone(int axis);
 	native void SetAxisDeadZone(int axis, float zone);
 
-	native float GetAxisDigitalThreshold(int axis);
-	native void SetAxisDigitalThreshold(int axis, float thresh);
-
-	native int GetAxisResponseCurve(int axis);
-	native void SetAxisResponseCurve(int axis, int preset);
-
-	native float GetAxisResponseCurvePoint(int axis, int point);
-	native void SetAxisResponseCurvePoint(int axis, int point, float value);
-
 	native int GetAxisMap(int axis);
 	native void SetAxisMap(int axis, int gameaxis);
 

From 45444b0bf3f943d1999172a6cb4296e4126f6d5d Mon Sep 17 00:00:00 2001
From: nashmuhandes 
Date: Sun, 6 Jul 2025 13:14:47 +0800
Subject: [PATCH 255/384] Improve banner art created by Kinsie

---
 wadsrc/static/credits/banner.txt |   2 ++
 wadsrc/static/widgets/banner.png | Bin 396260 -> 464603 bytes
 2 files changed, 2 insertions(+)
 create mode 100644 wadsrc/static/credits/banner.txt

diff --git a/wadsrc/static/credits/banner.txt b/wadsrc/static/credits/banner.txt
new file mode 100644
index 000000000..590bf7b94
--- /dev/null
+++ b/wadsrc/static/credits/banner.txt
@@ -0,0 +1,2 @@
+GZDoom banner artwork for the new ZWidget-based launcher created by Kinsie,
+based off the existing GZDoom logo artwork by Tormentor667.
diff --git a/wadsrc/static/widgets/banner.png b/wadsrc/static/widgets/banner.png
index 22bc39c785b9208a5a18e5cf067a79b1e90fb565..bb02fe49b4735ba07b0cc4532ca5c3a6c439fccb 100644
GIT binary patch
literal 464603
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 )lcogx5I=YK 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

5PAAstk(b1rjy_9SQTpE{NduJ50!v_FYk)78PC zqR4b6v2w*G2XnReN2>iTycpGVH|X9nuT3a)G1pnos_?Dl!Tx8wwY$4FRywwA*st@H z_xe*a{qLnKnK`yi?v&N@QY$;8eBzMowCQvB_tbp8+LUG!t}j6*V#tV}m%7;?)< zNt;<0xHY_3_%?;_Z^TLAgLd2kdR1{L5ij~JnjMNeX2d)7=myDs_;6i>@t^;4b`x*M zP)CuFOAVpBF5XzO{maag8<#k(YRSE%eQ4WT&JeK!ntw{C`f4dUv>(V>SZs6OX5Re2 z8h=l=H^{IuZA#^0xpMA*7&ilVKoe_4gosrCZ2dY(4DcWMIXRY$+t zBNkR4I`99o|4;5~ciT^yb^g@c?N2SY{d*d(-F<($bv~Ql-`axb|5vX47hd{OUFZ8P z{e%Bc*K6tT3C@n6W*vX*toqTDyAvKfE57jQi}K?{=SMq!^8DHVLsmno@=tElpDTM0 zzj!yfajQ(n_sGhB(hrLrN*g2oe*JUf>YW=`g}xlof4Xz~)YkmZkFOg%&wo>EwBed{ zMZlcON5@X4&iKFUN>yZ5ZQvq%|I@|speeUX(?^~l&Bh9_&a9c^7;Fjx zq}pe?vs$!Tw|3sH6#BQmGhhc>!~9*`#|?`N&5Jb6e?`{)QR`z^JpJ=yb04b-n+(fL z?dulTKg~b7@^_f+6ZO0=ymQa?$LssO?ot-7X6LVB=l?W$yMd|1o|tuiCjY44TDLRC zdcN1+ht8*;E&iguvS#uL``bLKkG#BRb~&4`mgj$4W3ywiPmT)1&BkEct);4e7Wbq+ zzCN$^VywDMiJ}f8kHD76v$of}E_8jpj#FXz=gBMYM6k>gpK$o*?UPq69;PNeNVS?* zDrDjH;8c*7{nT^*3fAX)KQF#Bac`c}dYw&cmb_gk^p$J>&e+v+ek{-7kXgewG5Y$Y zXqMXod8*5FFRb`rEyprBbIFe*)t4_Ra3o4CWKFsH_vrkCr)&96xSUswm>}R##_;6r za_-{PszE*A{3WA^y>J3SeaqZ-CI)@-C1$m z_@jbX|t%nyGx7yns7iM#Nx4QP_F->i`BVSwEKkK*9B&Lw`gU>aB z6@>lTjx<_-2o~cKW~g$O<5FL3%-5`~^6y{Lgq(u`l8Y2)#`U&-&E)OY3Ol09J12Bu zd+W}#8tqPZBz>lpyA+>!E$wi=ZhQE{XGQ%cHB4XiF0Tk=SL&1u`#;l4^x1?b))s0^ zJ9~2i^$ITfWcn>X85TTKWShi=nl9UmQk%8aOBgI>zHTd<^uRJ+t!bZv&%%_kd^BmU)u^d=AF>YewW|jm#y#}4-+x#d0)Ntj!8Y&cY zL|@?hw_oMoZ8qNd5}(QIH;;8gjrYj4DTo@aEZfAM0{qX7s<{UKz?M)_3&-buR**7(F!@~vM^P0YC zuHL-#($?KK7ye!1_P}vkgMHs$+r#abbl#*%-_{Thky4O35|f~vwC9x3)^)Q#OgY)7 zs}$Zlx4(T&;e&E@+vm#CGk=Ah|4>)PvB$8kI%oJLVW4_TZQC3A3s6H);HG z^3a5?d(BO6O!AhluDMcrWL5adc+t00${ZL|T6d&&unKV>%+lVT`e-RbenUzh^J`O= zqq17lKiqzmaZzj^%U_Xa;vQyhwx^@pE-+44YuK;-%}nmH1Cznyj>(Ozd%ikGFg&^A zp5kbv$B?yRmfqYw&yVQwaH&N6xsib9A?z`e3){^(|J01N%Ld8V+A+uJ+

HQ>rht`e+r>jf> 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 literal 396260 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

f8KdkQG{-tmI`(4vE7Jgi@eP33KMM&(!8r#5h zK1RhTl>o^F^#+%g|8s@6M zV%2|N``7T#CAyF${Jh38rPu%Tgi?+$E+`RRZ%oJwP3pvN*KjgFVcRzi*dFx`|lXs@;ugE(X zb!9@T{ASa$w@A+VJbVd5k_f3J@4l4ZOJYcrhu}*73 z*Ng(bgDiOk^LNBwIdN;^?-7<#wOp*O=R)6Hmn&twx45p| zqajnQvT4%1{Oeqw+ZH*kEO_a1RbM97RC}pgi>m0#xn3d-Oiy(7Pj9hXSbyL=i(FyF zk!A-Ov&?&6mQ7n7=4N8@IlGI4f$6xcU9T0_goW2Oq|ccDfnmV`B^T*eul~5|-R~0= zd1_l1*SwG2a!muToB+cn)s+*^m2!3}D~G;ZduFNDiI+K#ucUUxmH&9q&@e0Do2R@* zo8G6yTYEp6H-yUjZag|$y(Z*J)N`NGLm`1qoiZC=g+HEgH}8bh3wy`F^iy(%&BB`R;YsDtNVU9C1&wqqu*_Jx6t;q>yDCIq1ao)PR?DFY#*QSRsg*hrr*uLjr>Y{bNZ1v9$JeF~N z(WS*E;uy2!{b`rj%_n}IUb#(dv3AGFn-4{$I<|&6oc4a?RJ}h-FTmF0?!DQ%$MTNIqmsCEUV*~0D`ur%PZ;RNRmpZS ze+Y_QEnLaM?)|~$mZ3vZ=?dKqhT#jW79DD>yp~+*`{M3alPa4dJI#*moW(seXvW(K z2l$MPmc1#E(9}_|xYImyb=5h&4b$=tG+2c%fAL}GimI1RR?mdDu4`Vf$RYWV!6nWW z35^z-E1g3ZvUSTo;#6-9S$TmWVbxR(C*OlCp(6RR(vwrO4<322iQ`_vs#U!E<}zls z++bSEXS%j`Cs*h?2G;J=#&Tj(8!w$N{r~Amtj();epw7g6MUJI7}7-8Sl=zyU0S!h zcbbEiaeLF!qdiPqaf=qj1oC!Wy4A#593d08Vk=AMioEWwIEJa$G>t=apG}>9w%z-z zR8^V8)K|;~EBALy3TXrP> z=MLUI72ntemP;^Z-@L`fdrv)zb)r>zsqiAs#g%nu@9$PrUaa7y5LXg?*2?5zf!&#+ zxSCZDIMy&931nW86qKUNSak33ir8;eO;ZfzOw5A29cKNk2`m$NysnV>>yqPLD|hUh zX3qICqP+3R5^LuC4Y`F4`k#+}&Puvv^v8!GIA5&G_KTmjtB1ytje+6k-S5t{Y)Sci zVqf{b;(Q+C{W){8u8RBbICxj;K1-j^&A=$WMCoR8&f7+>_7xtCySI zeqvm|@9*k`@1xZ6E+@$ME%!FOS^jcS$G=NPe|Mhy`6=u9t^T_81%Ib){CVrbKh{}! zjncd2`yL;)_jhr=Z|d%F`Fm{2hyC-#zRh0zsrbj8>+{dfnzZTfU!P!m>zdl?&3%Te zZhqzYbE`Aq-R;?>T_3Yk{+|9=qQ5idyXL-^f&bG^{*7+?wmy_okwtK)?eUB877x$w z{_F35zQ***o^Sd!hQfc}OsV~PEq=SixrHU?dhGAtt$F_0@9^gJn@e23W}Z2dDtArs zT8iY$Gy$7xoukDUXKiIVE*N`th19C2uQb%29W6QKy8NcVo@+ocajU6~OjdkglY=lEx4xD8@a_COd!w$)7QoU=#+N-peSjKIYj#FC~F8B4@%D9i^ zISUIax6HqNer2tC^-Dj~+OOhrrLF6JTrGciHp(tk=I5N%I*0xjeO{lo?e_dx3upDr zlx}`lq+yr5WcE#y=Mv65j)$1UQw~Wce7>0(S?}Q)lx260A=b+jwQ!b(Tc6 zEzC7(ck2xU_wPwqIkl73MW$|@z4hz*SYA)_pIf?Qc&5&N_v6v${hwCE_iw#@zgTb5 z$BWOJdl|p*HBRgN)ihPr%_-%A?7sgg`}ZA>XZ*!juX1FYc4*rcJ^sUDb1tUKeZ6|& zd5qXx$&Zr3dt@FuY*e;-kf@!{@xA}Y*GGZ=6DH{W)YlW4x%-(<-6emn(2d6T{_F`A z%Iy50#9*PQCD0o8a5v|2_CHRC*w&v=dTGNU(y`ibcgKbTiNaeqogJcN?oD9-{dL~` zuT#!RY&UqNx%5ZLf+KXN2NQJ|3_`9yZj_84^=n_tVNcg{A<-zcUczF2kL z&Fe3i+t{|)zvd|T8Q=81;#=zLn-=%JbntV2WHaZtVEWVY{rv~`g$38Rf|^T@D7Zgh z`);h}oO{~cDJC&GaQ;U_g<5+3e?-^Kv-}s@(lr z*elQ=$s*3MwP|66?tQ_OV~Op@8(1uwqqM^M)fH^!eq&VhvREKfB9+wtVA-^6xeaVl z_u88;DwamrzgnOD>vK_ne&eGqjf)pKTRMNOv+ikaWepTq#B%nlr@Zj@#$8!kUo-C% zGT)QS;uZUBf$P_>>W$6kJ74z{$4Rg*XgVO^oMJp#jWN62F;z)nL&zlwMID8J zBq!^$yo*&Q-O9Ssp_`j1k}3Cs!RVU<%eOdX8>#7D&kDZF$a2Om^*nIqN_G8*yN`C2 z3GLW@HA*IRn}eFBS8J$xc`-A4%2~##wqX%Wrd}&{?__Vc(lXbt&Mw@weXDk&Mo7s^ z!Ogo(k40SHCFUF6udv5OG;E5h&Uw!{iYr1zZ!KKEM$}cH)k1+GCX`vjBS2qN_eJyy z)pXSFmbD;GzS!=U%yZ^1|<#ht72eOyoHDa3V`j-=&V&1=Uva zi4&Lf1Xot+=FcrWb-ZPsfJ5Y3qb3Jy%fbrNvyW$TEV#8{Y3}#s+{GIue`u#UzIpm> z#qrNE>%OkrAv$e&*u(CPS%)sjZPJbnw^-8Emij=T_vf0h?1dX!R%LB)c;3Os%6a$2 zUXE2_nfty!Rn}yYzMC?0^En;EjK&EXlNUTc8`4*8<1WacZtJ#$Z|`B}h6OhM*&jqA zZn^B8T4~W|YneKpnJ!$qg)qHer(Z?xk->oWzvR>f>z@A5wW{8dL0mwkeQrKXv` zd!uIU`zN8lzWCpYc+{*yzt`LU5!d_j z`q+`uyT4sct5x4Tzb`#cI97ditn$bGnhqAsZ};~+O8WPWGk5i$?)!hYE4Ie(c_2Qu z((7e*l-#Tm@f)G8^U_-`_B6}g{I|`TwQ$d_PatF`JVdsC{wp}X5K0Bv~KOc*Ruc5<^6m9{rhbvF+KeT57RSF|N3^#EVp|LZ{H|LuH1I_ zgys5OJ5K#M>2pKk$B)yGC-RjVN8ag?zwf_0K9O_v#wmeqr~cZ68&poc8kjn7S@73N z8;uG*|E<4{{}g_9_eB4-zd||NV!V8&X*2NcHC$xSeI;nxoK}(O%Wu;*^XZm76r24u zYwN4#mc*;~*R%;V9SG9)i^|kGllASkTxs(MbM;rdQq*3ZcKN&A_SNS((H_p1{|6}R zHAvpMaj)*0_(ZM4UfDOeXPvrza08p|B>rFDdz2G@@Y!?h^YYsNX~p)L1*WPM8;d@! z*gbVy=I)S}tHcgZm;2hyc8^EETwSqCQ(#Zn?l0@l-U&Q&=zYZ%{qF9ckHTB#cdiOZ z+w^+(3C|9TfZ08@H5G4mT`BiG|DeN7Wv@&@eAv~c#ucS*1)Ueet{#4PW#5FIoOgVc zcT^VN;o)ElR#=$4Tzvljt?qw6xBg>Y;djHJ>uCQ0T^=@9S%aLfv0R(>-t752WwYRk z+?8|BF&X(KleNyGQ!x#K7*Oc=cI?N_EATP7NE%){Z;ZX40HZ2={cTO@AGE(mdH+h;BN zz2!~Rsx6h_VuGxTj&)g`z7bQ@%yT9uC)twIgEg>u;hHD!-16l=^b7Z?#a@(RWES7J zqNe@f-AbLe5QM=IE>1;b`+P(sj0W|C2}RI-c7i zm!FBe%TQW+K>JklA0}1CwDa|^{w=-!&-t%)YsKQ|G!fJI1FnjXJ%rwWJ*&QlKfUpV zDaQjJv%I406r1^9t~~txtx;I1G3wvZr+sF|X@;R4Z@;W`u0Is)7Mr~H*j?qiYezK~ z3mPS)8Z?z(+FD)n)_n3itvl09RvuF`*4Svw{jFV9YuV)~ff9C!HOG!`{_S>-GtuOf z;-Zc+w!04xF8KbVu=~3Fhw>>aHh5hXId_Cd?bh3VHuhJX{tR~4;;W?@vicTtxgI%Y zHhCG_q)l0DGj28bx=god5`P#Pwj+>Xd*~h6RZFLyuM12{_!xPObytEGhxmNMC8?{k zH?Q}8+`RpQ_6tc7e!(4<+Rh6P21s9s-doroS2gKPz$Gq2_4ts)6@3j)i;US%JX|62 zVC&k$toIJy(t5u3@(Z^O2j*t5Zqd*a-^?gF^-7z^%{h)~TlF&5f1V?;=vu{FzOeRE zUhVf!G_ve|n<#rvX5Eo$&ADS1m!;2K*S&|hb{aFc7|I)K?QVXUcF*9-TFx^DtJmIs zm&2!RX1+}K#+ERygR!DvQ)k`|-p3U1WJ=PtH!G5+*>)GZu|2qWbm_IA)Jxa;lC@VY z;VNQX-NAD8&LZul!W;%aMRdYNw=w8tNuA?YoMRewN;l(#UDa=p5??kB+t* zo|mdPMAjT-|8eQkwy3l@EZPrEt}iNC*&G?+c3S;SSe>%sT9bvp8q41Pi+}W}ld;2l z!3hq=hFwkv7@tX6D$OfV5sm-zx5KMXr9=6xk=lW92^Q&Nm35n6G{-0~9_BnhUG{oq z&0E&w<$MZ{Lkz#{5>imu7QM3ZLSd_M)2hbs7n8e~jCCGQ__AzmrsOW|{^&c*ULK*b z`jZxJetBt2$qk2x8W+~BRrX}N&Lz?|<50q?($?O>=^?KEvv)ALDdjC%zI4g|Jzkn| zJx}!R&VM?gn{BP_&NDGhOGK7i3cF6}4l|X&sd?qUgR~|ZTv>H~>p7RnVXNd^7i=~8ek-Q2@JOB0-w3t&{7WLU=V{x^ z*%ipjpWZyHzv%#bz?w^p96k)2FHB0B|7hxs$-AfA+gthd)z#ShrL(_QZS`_a+302G zDlt`P$D;R%Ve=0iUq1Qz?s$2*uLV`LyMG?uEuHYq^7YGvpBCpT?c|QN&kxvpW_jlS zp4rtm#pWLW`1pL_ao_8I|33I{aV>aD@~6KRa|JGnFM0XpONEu4sOXo?zU}L(wYJ=y z-f%$Cr8vxV@s5|Nn`0R@!cNB+T30nNaqdzq`>VC6B=-8+Z7cg)i4>791@tL6UpO<=mRQ^K?Lwy)l< zi4BQ}tj##*cWiC#agGD*Nn#5p&rN%C|D!zDDRH^4(-!9yD8{X2Q<@TVJTq+(({H;Y ziah)K@;A9QHf4LiN>mgm(q))*!u$2&UWe<>YrppXOWgKRx$f1D*B4Vyx0MCzE^8=T zxJ=}8%(@%z!!0rr**A$a>)DkuHq5nCmo~dGhdJa|`^$Q@_RuB<3XMyo7oPL99{G1UH?_&!}eG{_?clXs5C)Cc%rEOPOFD+D&o()z5TD$ z=}+Y^>44IksvBl@m&&f?udq9@`S865$5sBi|2Z7MsJQYuW9en9lbdbrro_8G+3Ij5 zL}A6zobOwN3wSgWLjzn3-Ug;CwbgI0(z5#VJM+hq|JkbMvTc%&dwzz+X_xd*KE;u6 z_Pr3he`)+YS2p+9lWU!fk9kgVnp}2hHKXeT(HoBY+*fW|H9S?g|&}#o5 zUwZhX_b@3)Fdc0YatZL866EE)@swL+sj74T+ZJctQ%VfGquNXE-`_XCe%7lm&VL?n zi2D2MrLBU}UB>>Ue)cEk2k+@#nlAkGiAv449*a>?VA%PB za<%aC@+JYLoU%6S?5ht}PB*x$7)ig=1KAq1Z!q3w_O>w zyzic!Drhw|RAns>o?+N?@@(2-DFI&NiPk>0?kwu7A3PLy}wG}I?tImGbVP38?!PuuCSQ%m9?M2AfWNlLUF+b zdNQ$-4iwK-x~`@W&bB~EV1nCu(c?>)xw!+U>{!1)WPx*3&D#P_CedcGDIFoTub!^U zIyr;+(#>hhpM|Yl_3KlCwfn;@43!6#>c%n7{W5j=MxUB*d;)>d2N}HHiYfKX>GmuC zSbF#vV?<(hl*q0Exj@cZ_0`Sq*y7Z+L{$?{XmC!P8M@bOx8;kLJ5qfS&(wX}n;K>^ zHZk0jN@VrT-RN~^rtaqB#ZNCVi7z!}+5ALSuF1_oVd*M<39Vyh3&oZ&uAcmcEml*@ zQR^Asc89r3Lu&mhY?Iy{S#`}X?EA~N$$u-fE^}?yDqVkGV9kcRw=XxCdY%r?nLg2_ zqtnRcYm`;LyX%3*7uU`k-Zib6RwyL4O1j-b^dW$hFHwJU7Q_qsWA=asDI2^;RGpPS46_>b$!TFwc-)5IUh zyxySkKQ!v@YjK%RnRC2WhVQqJd)}3sZ+18BE9;NH_V0`S|0>_d*!AzOSM|A{N{8!e zYRopTd%Z4x&y`!p_W$^Qc>eiNp^}S-{XhG-oQ>HTw|?DS#<%TnQ_k*u^R2z$n?lU| zDdq934Yh$w>pd^G%}zF%o6d3D;#b}}xy3R*FE01o{CS1_SIh02DpH;s53LNj&9aBr z@mluXe;YS<2EVwz-TC_3u)p7)x$RmwccFmOxu479?f(2Zy?*Pf`k$xm|GuvO`8n%_ z{eKBzt;K0KxKiC>dXFauPWWJxr5W|?=g-Rj+xP!XHV-~Q&KUcNNvFp3MLf)S3UsddZ{O2nUM_MQtO`CD>v`g&2V&Sj( zTX!cs^PabE&yyAL($82EH$4_?KP|B}>ZjcQJJHKE>>uX~CLXZ=na=#b+x>6rFTK-Z ze~jM6#jOk5yF0d2bLQ=j7W48S99{e_T-)ME>_Mi*%S2g(w>~~Hfpt@jZD4yG`-ytC zmJH{|)6D}IUVpu?>*&qX`=|f^=(cb7@$%xc+i#z}@V)>1?#gq{_qWP?o!$myBQ9gdBEknt*ZCE{ln|Ba)*Re9x(jc zXFbo2!O3^~j+B?X7Wy1}AhBfiVL$m5JMW)2zVt$~>bZfUr=sD7-)HsSZ&Up8IxgS? zOGnsOhMT9A4Oy2gyE=h&vYzTj-J4f#8-o*~iyO}mW+i(A0_K0?* z_>|8I0jv-6cmEIe4bKe7&8#qa`D4p}O~-2y@`V%JR*0)DohEfzOWb*;!KIr=XQV`& z{?-xrNq_C+W4{tS1j5!XT6lQEMr-v*UE$JVrpJ#%uE=h>`DvTj%R=Xl1Me8^??1fD z`);0r)}Q8oybNcHx_>X0JGUh3-s6owf7SKWOP<_zsz>Nag?{zm?mAyv$1_ z=MJ9Q$jDSyabkwC9m9^-?-HkZ3r_5w-7TiF&*q_WgZYob_WoQe^#yXX(`|$%7>Zs| zwo{GGZU1`Cer0&P!~R=OX5IW8;wO3jKVNNlSoUi*pLPFw+D*;eWp3B}yJT*4pS$M5 zW2vRbJq0E2BtJeeec38wws*$XFArC3wUa5<X5aMtqRPou_#eMoK-}gSR+qmqtW%z;V z>(eK&vYh#&UE%W0Fv0NSORg_4~7m#oILWE@>wjEO2X> zxlJyc?O^DJkf$e>z2hnfy12q1e^0S{>^!Aqd|Izw-P^kKK^{9+E1rRD;4x$~2<#TmH)IS(F~85IRF*Ms!dPr-$*ehOncd z4GBtbM0H=d#CT-;UU~X-s;G>p-r06t$Gw(auCv4AdIgxOoX<(5U*1yE*tA=Gb$axU z*iBwn9C~gVKS*L)Veo3^?pWW?maArmh_FSNvGyNSZJAIyZNh@w8Tn6MI2nkoT6E&( zG8N5>vpku0^;CYl({h>X&0CIayUzt`|1}>>?muZ|@`za_v*hAj+jM)@uC?Dv%P-WN zI<(-;%usGGCl_JOf(fiI5^j|o3Xx3G*nHk|_Y@^dCM$)W8Ag5O->hEO9LfxJh{L; zPqafzGWdYhh6NiBFEC(K?{m#=UHQmVWpT-iG~070WG`)$TikkuCnnc2FhfV9p+oyX z($aHw>XW+kUZ1qPU$E=v+;2UPmsW`d+HJ$B_TxN7>N%I^IB;6L$dug-jN{aX9a za`sKu75D0HY$z5Fa+vM$zbv__eD~j{E9TfX|6j=^?;SC#cICSCy!5w|`ajmtSzcIEb*oh%Yi<7heRZE-{j+la`|xsbv+S0737379JfUANx&B&a z`|s$@dqV8l32S04+2WTbFg&snEKR-Mxh5~K&9}U|`gi-;`MS9V^7HEd{@%BVcm2Dn zKc@};KJ0(r_WON6@o9dC`^xJYX0PYJrT^aE=dumYU6ZrF4-39KBb0pOE#Gg$@~W;C z?ke-jT-!4DpP&EFPL99+dhS}$#}EI1{`2Gh|8JTLjEd(Sd;BqDsZ)9T-QzcR2VH$* zkt4+Vao6IS1Y<$2*zZ~;zVi=+2_(hvXKpjp4!jP+Vegy53T!pxBtgG-Xw?WSDCJ7FLc$h{yW27Xw$pu zmYVI-YpZ_!_%f$B(>!EtRJLsK$>3#&^&V(N{NDB4<;O9XyP=vFR=w{C;XiHktv+{A z-CdUywHq_w|E`fcCWUj|hv}VF(HpYX z9^+YBy~le`k+D+H))=qLUQ?s4=fwM8ku2G8SGex&PVwCyNqT1QwS~`3(r{w2IKBAq zn*-%VyL)CmI3;!9WLt~f=fJfw{#Vu)Ux|L@7J4>vt+~MWim)FY<)^NsS!P}5^^5O0 z)4jOz=gtiqbT1`0@7j8UJ2OSq-uwIdV{(T$_8zhMZI}K_qQ=H#!kH&H8bG zYVt;g4i2}T12^gxD&_xdH~;i*{V)4vtg_daMb0@Ovs?M?{7=s6hj%{TYSYsiWBk`< z?Um#5Rnoim|B?S$Z{K>n{Ks?AcSg^c<-UV@x`$ROF?&cbRXw=yPNbPT$syoMXCHh0 ztp~ADM|eZ0@i{we>^83NKW=($VpTE!rLTK?eyd+^c4$(W^E!pGD#v)43 z*jo6s8?58bmvMD;eaim%vigs=)0_g8BhowL1N=Yqg-y2SiJ07xTP|AOsv{&+JNMTuq4`aV zgf1k{osd^hKhss)p@~G zb^M`^PyNgl+IyVuKD0b2ByoUoH|O&0KOT1kAKoCTH7CLJbnO|Q3<1S}0|v?Q1xYLi zw6&C3k4P@>|8@EMpXL5POtUn0R;${}d@($2^5B`+`h{=u)a%l`vP+zF3=b?=_h`+f zPrFktZjI-8b^5toce?nqFz#n@TDd~j-R>1D-#l$`SE!tS;aT~X zt)|YMl0r5!#aNPxm|}Wn?TK61tF3h84XZ%9gzzgPldxs0Yn2bSUX}5YbQN4+u&tQ8 zG1f$5hNPD0R8i469oqg+-ubLK#K(FfVDBo$4IDcr?2P`%Q*rF%!@c}`MKi)fSEZRo zFwNdA8sb#=_?**ICIM?*iS}#q4HE^0Ew3crsoE8^ChDuq#f^@0&vswz`KEXDK!TAu zuS0v@A;Zgad|Fmy@ajBUSR@#7jcw**4*moKUk#?fqEcBE$(nU`9ubOX?9OR0GBr2M zy10Dq9|pO=8PbBXhXMnmUM0J7IxPzd6z0`VcsAE*VW`3pmbOJHD`u&xZJ2OWW6D;; zhmi}oidQZ<$a(c}SJ2TVYZ#rLarMg7o@_`sQKZE#$JDy0Rj?zl(aVh~sHo+@ss<%? zm2%d^1#uBGj%aKSa)>$lwkL3FM?q-OGp0}}6G@(z)U)_XRx$;eF*S-SRi?jpSh8+II<{Q3z4 z_hc@QPir)Px_WRFw14e+>9k5=VatkFJbR~Jcl|8rKlh91&a9miXNZPA3-s^XbnLF} z-oGmE(`{Ki*S)rt6D`u-9&l2k_@_le!HuR}0UJ*2v*}hSRI|uw>d@$);df)ZOlhbo zE9cJN8Ox{2?CH9CUTx0xqn5_3ocry~^q1Bdf1EzkTl4-O_2q@XmN{7;E71Hs^W)2p zKi|&&z5aEy64%3?xmoky+U&ENd~!+chQc%NGM-d2#4FtF&~VKxJ(%@kd8XX*!%ORK zuL*x^yy{lk=DO1I)!`j0+K(>}ihYr7@`?N9&zy~Ap}CWK^_T6t?);lO=YRIon%Rom zcicR^y-#tA?Vrn+-x=+d|Fus3*D{3_^Y{M{SA8ws_c4Fg;j?dEH~(jS)$MvFr`{$` zZ?@Wzj>6A3O?JOpx^Tmu9a+*>u6w^f{N|zJ_NbROg}JgGzZdBzr#anMW2&3@>z`|A z(cgM+&9}zpZM@!p3U}}8y>ajUue;f8b@Ao0?6G%`?0bH_-YIIKcf@_=bp=-c_WzIm zcmMzO<7cbB9^XD&zWwqB$qkYT%vTZ)B-R-j>M52zwwT-Z_+bUp274L#`DM!sf4%?z zczwNe-O8m+Vg^Uv70C2&ezte&nvNOs(@U0TZab`Qto?V7)Go%-`Z@Fas*8A)9M|{> z=e!Ewc@h44s%XZ}+u#0Hojn$N%-4jou;fgd^u+SjC7W-S9KN;ts?J@v?eAas?zs8j zn{L*zZ@Sy^1z7?Gs%Iq5x|g@;4FCSRUm>T;B--A`=f68tXLd}{McVRx*~7OLH_o^3 z^;^Da*4p{IJe)*aWM(|uIpx>OkCDDJU%sqNPn-SK>*|cmb2|-nu9}#(JJ-+K_B`gw z8pSJjKAe^L>L#2Vre`+tUR1KMV|yov>uS|L^tx|?Pq0s z@_fSt(>1dDzO1-xRR3?0Xi??e(=K~0&*)F&&wo;FZVtN3Yh_bMl-tpK-z(t}tatp& zwxmAmd9Bf_f2%Qf(Yj^Z|F9(_d^^yP!_70kv8;Ck%k7`n^Z&RW8u6VmEV_MhA!FvnH``zim@To&M1&Rb7h5-u-;D zwq&L3{eKPb>#pt(eZmm9eN_GkA0YnZ38Muz_@^=l}QpFKpjlT`lsGO^$PN#4g@^ zgUe63%D2tCrc--W*@6GZ&+0#siTxG-)&E>)D4cRq&{ImyxuHwSK|9CIXYCA|gXIUr zSf=v+XnEXHvvb1o!n7^lX3o24m$Ir$Xu_?7mPy->zE&<{F`L3HcW9BwQ_*c~+!I$e zs^5)$+Y)QM^UF$ez2!op5f8u4GGBal(V_=kKPBbEZ=}fVpS7aBqt?nimFHJSKl3%N z?7j<}$LE=!tPY*rdcSY-JdxCXe!epnc^`Hi-*9Nz*NikD^BvaHg4=yxv9X6-OrBgS zv*9P-|0OG0xgDMqa+o%YxOr_a`@hb|Na5E8Rto zs%GD!-0fF}o0)0d^m1u%-Z??c-9TgK#?{v%PsW^GcxxA@#)@-24oS8dORToLH2WNXM2jpl}Ix7c~6N-{E`Ehjje z{8Xb$rnGGgnH^=L{YHiRSAF0|*2J?cBC+2#>sqpkma49nj-Q_sK9O~jz|~i-O$Q$& zh3cwDavyONl?-FhjhTE*Vd0gP>XDwSZl;U0vGX~kPQG!Rt7l%g$n!bQPHWeBJqvx< zB&Ww9srdTN;Yg>?{%dO1ItIHgV-5+kGuZgjNlQhkb0Y7muZz4aU-o^qTeyCmSGs6* zYv6vSUWu)LomJXJIV`tMXR=TZpYm=_j`|BWp0nTWl4M?9NxYgYD=2+QUt!Hg!_{k- zOHQ2T5fSs?m(k&>4GW@f3eE`dd-bE}HRCfiBmU_j9g<6feP$T-G5&QdcWlz+UZQpP z$n1IjBJVc)M)tR82srb;U^WVwzi{=BS1Ik2=WW|je@W+oVDe{yty7uBYZr>O3%MOv zIOO?AbN`bStD^-pUx_aLy4%fOQ0&?5qesvB9MO5s==1Yh$hN6#9-AY7BS-sai%Ve|T> zZ!DjC3tlYMoTvLbyr|YHc*2X9p-yYq+qGM7eBfRzQ(cj6e?>kmm9w75-#foQ-u`j)!&j$OOIkbfzqRNucsKpK)Gt4meQfNjUS-Z% zr{BCxQ%$M8@kQO_fA7D)Jo}bw{k@0p`akq6-_NLcL{vk8>yANhRKX=%ljoT-&wYyT z$$snq`lBxX^+VCKzk|1LkQ!`mk8bLXrTmV0*i?%Ax~rcC># zvkmj|gtr}9eD&wUzyE*z_@nuHwcel4ug`usTrb(y5_C_zDwo5+r7g_oXwbuT3&P%r zUi8wt{JGi7KE1VaW+@QQd3!^#pHqZLP4O?SP4Y`)}?yYQp zUwhXql$S}(pp;{Jn?>VEq~uFle32}xZADfJl7-H zZaQhV<%|{Ytxc_c+_yUAI;v|$m9{C`KNNRbvR+!Rz{Q0tKE_*#i(CHnZ;K1^;pO$W z?Q73ivSvm+IDKz*VMOy0w^A_|hSiA-$+wD6eok?lAh`Ikh{MsAu+ptThLZiSo^6co zI?%G=sCK(>#g)A(mG1wiSo}$<|Gi{C)3r@IA|6e9Kj|3L&4UiQN7bE3EeW+TSbdzi_MF{M6I^zW>j=ye`wdA)=21MfD;%VO(|>n^3)wXz=! zJlyk#>wA9%(=}PAW0~FCogUf6VUrtvT|h!=%nvZ}QX*Zr!ryYf@#ahDt$E5Z@u!JFBZg zPv^aRWe}{b&=DNQd(LU0u|UfmHR~0-9Ym+bMEB;LF)+SfrqSY@Jwy0Gm|*7|sU`Xa zCgJY6D_7+`J9E`Y&^YkpHpw9KU?W9i4b7d}3!+vZzM{QbBIM+eqjR;os2|XV9OV9tXU~0I-rO9#i zgcYU%^3K{O2G*7bxDIe=B&53T+_6jSR`iV0cPhu+7-PPz44Npp@aI)gg$TF6wO7?$ zU#YCj-WaoxNyVih#xyABd-wxZ`2}vqen*4^#S{DbMJ3%BI@22uTe&h`xnSizC)8T8 zf%D)>6+Y!IYZU?Q$pwxVS8>@t{K+=YfI(%&*2ZF+ri%XBV;bI4PD&w;XE_*ak3RXZ z#&p|~A8WLpz4M7+Iw{lHCdM_P)Btk67;D&XQEHKQdA{Fe#tp*Xn8Z{Z8B<&sS2^%hDR19A&*++LpF>E9 zt5}D#v#XQ@6wNXxhwKqRFs}|n^#-)huU?&&pMJ5)s*vc&*I;^ zIR4LFbn(vfm#%wcopz$*4MkvrY+6N4f%cc z$c~`bdE58fou00}t@f>6Yw7hwE&u&CS1RXze)-}0eld0L*-2t~^E#W2eIGuvh}5h6 z|D$GBZh!y$e}%u|ziw>V_ho;9UC&jotCs1DudJKjwmaKA_WsiC`SHgE5gYL=>)IjibMqc8{lpW0STyJ3WL*Xcg;P6?QkrJUANO3*Rj_&eoe#1-Q$Oca z*QnLzGoRi3U$KZ+RYf#kVbbIw6kh+2${? z5;Ck^UsrLnRpv!~cp4q=d0$jVp{zdgLbS5((ia?W+;_w;c`tP2rILV>BZK+sRP&p^ z`+sbGTp!Dm$-}H|Q+Vq}_6Bp!Jw<yQNd~w3wJ3AG{8Yf530d(AVLmurTJ-<;$K+ z-|qL+D9AWp$|sY!1%| zaHq`S`z7*h_p4L-XVsY}tWMMSoql^>Ro||SZx_r?uDNr4u75|S!+|CV>->L4ZVb_P z>NoXI?hR~H=9_SUu=%T=M6tR?HNK9SA=$0Uevui@6U&g ze=eQ=(9)m()&Kjaymt0hC)cIxHgevvpC0cT)Ozp5?8sx2&9cgw&vrVn7HL>3+&TGr z@jI6${mw6Eot?_RNLe_hPWqG4-@~977#L#z`D$~EYRAVjk<1QVo_$>EQ{Dblsot0U z`cuX%Fv6HcM`TL*uRYU~+e^$0_BnJO*E~~o`LTTd&;5QMpY1>Ye|`Cp2@EM084ilA zXFUJwdZb#bV`b|}68b6|=4|H{@e5TyuOu|VO(yNl z{^R>i4j*mEJ@}YUex1?VY4zd_>Ngmxngf2A-u}n;)ON`NnU5T*F8!0v#I(%rxtwt4 z$-@R`>otsTK0IVwzir1^3C8+E?#=Tz+`eV2A^UXV0>+P={MI$U;{MI3wR*L$t!9gY znatOepm~2MdN9OG$kwGbCtQr&D(A<{@M}`Z)uO9Qu4adR5t?_st7Nat%i`uqoC!f4 zCzx77SPv?O>TdaA7TV+yIA_1c?xtJ!Wqlau&E{^eVN90PSzsWja7x!&nCWZ$x!Mn2ie70}>@kUbcqUYJ-iz<5ef^0Z^Rs?*$`7m$a zlYeGCvObGA7EUrYJa8~*bI07vp3~ZOoDcXUBpP!c+vYNRmf;e~c^$W=+|D?u`Y~$N zLZO&Mfg&YyX4k?=VmX0pgGANT%FUl?brck5TC<;jBD-MUo2y&5+9h#MpVAU1pe35W z$6k1cc!+^gm0-};XU3;{T5pB8N}b$q#d_VX*0^vPi(Cz z{nEnhA}-g!YYifb1}Rgvl^12K4R#ajdK4Hc&0H4A*y-W4)=p*99PW^bw7V^3 z!LZ4wS3)_-W!BRbS$(rol@gZT=JMVoXV4Sm+S%7^cIx8bCbTgNIlt}{ud&5;HMzPBs* ze;Fmb*`<&_Mc=;p^8S*3Aye5;7H8kpN#+R$hVIQU%l^yuX>P}zH&vG_Y;Fqlznrd_ z(b4fNCMhC-RXkwM+_e+M&2Ec#KH7h>EBh>;;GFV*z9l!?{?ty_TQA^rAfIFZ|H<`w z8~*0Mda_?@+uzwT)`^^(D!8KD;uZIqNZnC9bojz?3H$5fKP+m^O4B!1o_lN&&u#VU zrvJrVoU69Qt=~4=ck`Ej*Cyr!*8izIcc0ti_TR$#U-^#@7tXQ^?TVhe`n_Gv6|Q%D z4M$6=uJH+UZ%mP2%^R4PcWwFOFPYnaXMT@RkXY8CWoYD`;&nT8%8la(SkhhvWu>pJ zZTr33eqZ(PU;pxUN3P#?d-hwcP3Janrm}>$$TbQue_LGZ!TF5mHQrO0_prsj8LtnN<(-dz4?B3nPx zHq%4Q)@OOdD*@)$Ml0tmvr25s^oW#LdSlJbD>2a(E~X{NT$XNIbgL*-BP2B7AZNJw z;@#oVMUVfU;=h!)_uJuXVqeO%x?5j+ujTEK`nK)tZ@Y)#%<7BeXS)=73M}M4Jo(`H zZwDVp?`A)-=AW2PU&r6*!@1=gmy@s6+NySKt}kf0_@VvZeATv?XU9LC4iypKllee5 ze#$rXY7dF9U5?(7>@yVZ-F+Cl)#%Xzv)bTa_oiHBtt;7%?Bkg{^W3F! zA**?QR#FG0t}k5rweI8E^^b1yOn7ar)pbPTPOH||X`IXra((I(zQigm+Udu?{JZa^ z{1uF=bB08`?zmABS-lCExv8j*gN&=RI-$cbMyUK z43+ZkcC9=bxTWC3(X;gv_qqR*>A#+{YiF3tGgWra--!xqjh1I!uKr`1c=fhg+lJV+ zpK3njsLaXsoU?>;ikMGrTchCZZ#R4x!#+1j$8n9g48Q?YJUep0t`!9vwr`dxQk zT8qy8dgI|c#{Y5`*4Ac6IUMcud;I1?$|@(OreBe9d_p2?b&Su=Yg)=A5Ttwl$AxLP zm6t`|5&C%Y+v0PwL9?y$3$z84^PlxAoYsjFbD4H`*>Afq^S{Z5$_6Z(mbBdPliB_G zpEO@fC@9@|{i?xz!M1R{MJY#HLq$)_<;mWA`h0C}?KTduZ7IAmU?f5^9xjT+3tYLIc+57qJ z`JchZ3==FKe01CP%*CZv+|hISbH@)lUt5mHRlDAObeHpdn^s4)jahEcp$}~KeHC@b z7CSIKTD-S^m#jg}hyC|1mh*%}sLwqdGBtp4&oOzSsy3E23>K$WMyl9N-^cxdWltN= zf~H$s?yQz2#{@grt6x}h1T~kgUa2>+FK>l^#g3z|b>n;`n3V)X&Mn|P-nDaYY^m+b z&s@=L_aDA>dl$Cv*5kt7g&Fcog!vY~6OrpNdT`Go^+RgQybg}OT?tF{tiFluV#qx5 zep!9~!iY~sP5DnW_lH=pF)1Z1S*7B%XkCE1dMw-9fX;Mly7X=}o@xuF5pt++ulALgRn@t$%Ag`&2xIS?trq1VgR= zFbSlmUAuVI_Oh?@ZvNl9{{QFKx%lqVg_7{_urxOX28%m)`D|TUW`1|Pa^|e8LrAEw zu7<{mhZ~&z1JnMko7Ghdls>;$V0C?F#4N#iLay6Z@f8Vi(3Nv{ZlorN$INV$FK9+}zyGbLY=*!Y* zw=JRqOsADtjo44$5^`2p7Q&#VFky!fgAu2UW5^Z@uh=)bQNLLw^t;rHvsN>*hl@^F zwe?P>;1)xvh5Sx!H{||t&pzbP(dcq?kJT9wt{FxyvJF$yID}*aoR2%IbhwqTPZOBX zu9ve#Y@yooL82Ihz!++86AATHpDhDh~Ke3R?sJ7zWM8_ z`O9~C(q0=Dgo*(v`9pKJ6@?y=>;bYqEQ@7M4-mPbgtsY&!dNOmcQR6a--g~XpbPvvpMJcvR_}`ea`0RyGIR=m)iXEcKLf+zHa&C z|612-J3nmgPI;MkXmkHvgTCT((>Xt^k_yhzU$N@Up1W&)uhUll-SAB}e%F2T+J8$U zWtW#^_HW-H=NJ1c{OS)MllUk1=frQAaf$KjtNHKW|KIiBxP!Op_*5l1xwex&5sUj{ zPnpK$Ucc?)xhwB>;@ykSvQF=bWWRmy(X(%Qdw>6a5v+cxym0rUt7mtwF5SPgRych2 zHr>Z2f6v$deEt8c{Xd)YKllHA|No2s{~zTaja03fexIMwA91(I>Wsg}?1v2Ng;ZBW zt&O_NSN!)&;rpiw;i1PD^Y(AOWx&(^dpS!7-{Zn7Q=jN9$kJ#IT)VSJ(%G>{K2eIX zEz4rpIwPy3L<<3DE={J~eRu2J)9$TNmG%AkGS49Ik=fd3jC@TyqgOgMwI7X{HnVfy zTm|XIM^ky9ycElMFm3krgtW&Nvm@`9E9(4J5oQpYc9Ku|O_%D0ReHkJ?J*ndc#QdP zK09>v%=u@z{}qjQa#+sVH-EW{*rPjBKH9JUf2COJ(TA5W#T!%P>~H^Oiv9SD{kO72 z^=(am1A(C6vs02Zk4YRhNpDy6ycRu(< z`TyZl+uDj5n0a&_9C|wWZ|=t<%;~eM)|}%jUVrAvt#w=5+YL^1%=&#jGkVLdh~;UG z%z1|Pe^eLk>(TQ|E6TbyOU#b(T9ew>++G)x&&>0J?k>^Gxh5-~m)mxDi$?iJ%Z;p) zD)$_}pEjH0kWSXCv_4hd*N=?4Sf6t4zxT*%gWbcN(@mQvzV3-k-FGav{Yq=-u{}>` zFIe5u$EsAgY4+14Cw}&7Er@KBY(KCmPSxMa*YcYugQ#uNb&kDVNmZeNt>?A9PA6OS z+ej*~w|7^VS!rHo% ziN0g|(44(#iSSp6=dwAG8NWZN2)VqundzNzE%Dp&r6CqGU(8fueRyC_vAp#SJLfam zA4=6^}GD<{3~xR|A{_Y&|6+z^UBFSxu$=AiX`hH1x8j+zWv|7|NgXP((c<8xht}E zy%Xj-EV}S&{mZ1cUg5Q&tL4*dTKn@quure-k3D+C;n)YhdMQH$JKbU!BxVb{xlSlKGEdODcmC782Um-UJ#+Q#Qt|9)Ub5<| z!tpN8)w#b){Z}vl#1L`7lt*ca*SUQHQ)61ly|vQLooLI(b6$-Mg^4CpZrV+&XbfGrDX& zV^Y*=trM$O?{s4cU{HLWu=Pr%{j=}OZho;BoAGDr%pEfi`*@#K{%~bo!n6a1OY<%t zH9gL(jay z!L?l2*~zDmt4nFk)oBU#ek@sLIR#9umzP8w$gn7Qws7svv)^Pn9P;u?b3a}B=jot+ zlyypzM}Y1jr2zNFFKbwyRqbjQS|80fzd<`>#`Or!rhpYEt~_G9Xs(oyz$wBQu6K3I z<30yjRRcDwjLlsv!jZb}C%QEJtiBe7Zdo#e#~{f;d6DzfI|-YgF_^9H3~$h$%6v?L zvCDR9S_r4hqRD%*6=!lBy!GGujPWi>R~09w-$9dKIPD3E`_k~NpiLsjNo~c9t#5eT zJtNdhaz%Y3G(YkA3&sR+o!b55a!1V>e%ahctE7n2%WU6ps1~rSSS6$vdLbsT&+cB? z`)`%zk7uZK9(uUBr^<`TBgxfU*=a+Ied5)sukU+{vsWIDpW3s^?(*Zi5iW6Q^Xf~#6zt4iWp%!o<6)3( zT$XIx$4Sm|!GGqiDKfTRzI$gir|G;Mi%w7bAMJeRzrdn@OP%Jq26^q5-*~Nb<=k51 zZl$GHET^B2Yv_FBdF0*|-q86kKR##v_cZdi*Sc*Uf7_A|t(HxE7WMk?^W$ZDXWq2F zJMj10x2On)AnngLZt65Pw@BoNJFPsi>vh=e)w>;*?ylN>bIrST?;m|LTNCy<#BN{x z|J#on&hD=N_3^Czw|4t~53BzjKHtH(%E0;Lf`+Tn3B_3}FD(7?>|NT`^0aG9P5grO zgj}9w%6+zx>HGDA^YP-?nQN|@KaT(Quw60R#%4-o)u~>i0J(4OCGEOuoXPiOOD!xq zS91qC{nGd8kLXz^<7U|Ve|UK+hlfs1#y-O6QW=7>*})m_pz z`MccVJEilcZfQ$f;IYtYQB?QlJt_Bf+QeCMH+cT{x^XCtEo#G~3wO@l*1ek}F}3py z>$+tuTnStMeXdyT&~3o4-p*m194oaXr@7&u^T!;YC957=^v|e{{%iQhR$=d3j?K~w zrwIjQXYyy5p0^P3(Z0Q=fc0=gt=V+dm49yT)Hx=hdar-J+y_UUbp@NZMIR_Dt1mOE z+Blb&!CkulR(Hep{3Dyc#(G`ZKEc&ATHxz*k8RUG=4?oNS38rZaNC95J?Y&_Y;V4~ zv-R7#dY;`AyW!SN^#kGi>{|Egig{+4n(vxrd$}N$Nhf)q@Q6@i4sqF zfHfCaN7TJ!C6jz7Tk$)`6LS{nD>8&_y>oQ8{1f@`J=ziv->%Ez7yRE6ued6Uzk;7p zCVJP++j$3swex?Qvdb;MCsQy-u(UnyWHbZ&9slb7xy#}*K875>xgv^{snE1})ho+T zZ&CgqzAASg&ei1Ex5CK(V@^Yj#D6W0=-qc;9zGd*@Yv%Eg(l~W+x#Xwxmkv|Ue7u^ zdFC8DdsR=>^Rq(VN~tn&?P3gxU_Dc!)82FE&S{Z|N_J6Uj|9a!E%~s%3u}WrESyeq z&e#zBc-bPclm???)_+14R@dy}V9tJ$XUn*A$*OxH!prY7eDnTy`umT6Z2A1VxO-E2 z+_tt^u;+w+XZ*9|H+xUdrAy_r7teM|<8i3y-Y!|3Jp22NZ4>St^UsL?Vv!?xC@^ed zz4bdLQ|8K6&!F|1p9WyR4UU~iXO2`_n=Ge!(C4aO3EwHY?o~L)&CLvri{wJFrb04Gb$2+2O zuD@!mAKkkrKQ)f`&LuN?ME{_ zUU6k5Eu5zF>RBk~qJyqy8Mt%b`0^C*s1$CQG09~$@3L?a1;d^rzOqR%%nkystgFJ) zl{{LzcZifPf9C#_ubJEWS=651Tf9zF&prscxi;%tRvd@ZheeWcqHkX1l|Ec&Jk{pf z%ZSrUD$C6O{Qug&`)bnGD<3TwgeD!?7ax1Hu6welm!i-SkCS(~Rtjix1g%b8lyIUo zYZ1#LN#Qd`n-^_h73Oa&w)EU7V0?CB^~SX2YtO7s`|8frDmI&=QGi|M+ME#UaHcyq zoe#vU)?angGRwWA!L&Y7AuPnH_d-nfEMpsIQK_9xO;wFv0zsx%+59vUmpoGq##NSa*Ue-n&C?^-5!gOSjJTh>2`CKkebsZiSz3oHtzgnYL_k4Gk%74r^s(-PbjRN3yQUXp50S ziRWvc72;vRdOhh8 zcE)dQqQCmK<;q=~)6jiI;ow%=pIe;yuQ2+r;wv)_o)+kDKU&ma7fRzm@pCV3)aFRHSO&W%Xc1m-d|{Hx@>8 z>(w;RNUqoTE*e^L@+tQ{*2hlER=wVGv8I2z<~7|d4>ujO`~NRmq@+sm``@bTOIFq1 zUn+9MPzv$JZ`9XkNW-ecFS$FNM-KY*~8kr0b`jPtWWB zHeaVNP-_4Ga;k8l<%`VB0L7N);uGpjuf2O%c>Ug`KezR+?pt1*YuF;OSaQ+BySLxU zzpvZ1w|4uzoTBp71NIl$wueXDZ&14Tk8x$YfzzQmg>%Bbet4Zf|JAMgcVhpS>4QI3! zu6n&PZ1v*_&nwqlyyN}R^19LUOBF@Bnt}=oMOt{5HGGb_$<)^Ot+&-`+mM8a8>8u5N zr+J%vbKx-99OT==z@NV1`d$CO+nXKT4lx-gwkk$1@4ghIT&!8TIc}+l2iA;g^LZCxQ!d5Y%KK?wz3|~db0dtU&4y1&o-^|NjkLW!BiIkPd&55 zo;l{<(vHrZ6)5as7SWp6cB-y*c5iI@lX+na!dK0BR{!5>UhfhApC^499<;33f9G{~ z#kR7(50|`BIG65rxgs7B9V)7JD{oHg((_+0+OhnyTv%M;d?niEcF+S!uItj((G%70 zJ-prV>t|a{;|gI%hE%nvG%cR(R*!BJHl5u5=;p27y)K3yJG(DBUD~PI$h@HM_cZHR zuhb)QjNTIW59wul#kt=7@Rj$T&CTb`Wu1pJA1_=bbLgdnS)TKoOkas<%w8-f=I&wr z(7?~@Egh)cRZ;I9WXXSHeWCGLW22VNThDBjU(G0F5r|AH&%e{?@_fVg=bu$(6xQ@_ zoW5HA<9xXfn#n>FC4-X;CU$?GWPjb@D65{_=?4z#tJ5BzTX3w{t*5DAlH7Ia8hKUQ zhsoZH#VY*-C$rAyKfAa+>dFUAuiG5bIo(g(ZAvl{cBsa(cD@z#o*@6^y=U=csWlm& z{p#*Ld>B~&H}CJ|WqHqS-aV;1@=z?F`<2|4Rg1c6&faIdakG}WEbl|?d0QSP-2>|l z9IRvZAH4N|abki4Q%JGS`K?tk0zIK%m>#{`b~UYv?%y&29n*9)^-9s4s3C4$3__tt!G>E~0< znQeVbQu_YG%?EFpXeqpKl3kUvSi!$a^?-u2w@Z8;i&Am8zSYNT`E?>TN!AzQ@12}4 zA&_c7T9Sv$>F9kPtqD~elJ+mDT>O8LXy*03bK3jPcl_d3XiP0+p8PZ}SyWh+rDM{G&jOu) ze3FE+Q&JW^lar7*%8+4m_tBLj=Y5UpcfD>7640F2+j?>0xp$$B4-Vcvc{6k2>}ydE zp0RQ44l}Np&1Ml(yQuT*(aDz|$vSlzy?teqG4KD~kZ&4ygxx2d5XrbW|FY-T_xIS& z#`$(vU$fdh{r*pfwUOE99`{8g?SH7(TIb5*Y$`G5R>)M(lP#V8MiHlHDBtj8Vl8AV zJtDRwqjUl@pOBkE$N{bS`j(|Hsn5_eZCMlSFcj-8o9U<^_vU}p=OOKzhaAfY5 zotD9yBBvCV$oTN^ORdb;&x5+zzHk-iKb6ss>+wtAKaz09q$yTFdzNiDQ|D%V9hQ?T z0x}NHxv;(>SwrQ~3a`|quP1JDQudsb62+9Klz2>WfqI+6p0fuzBqKena@?vqJY-ur zlUt>-m+m=crZ|zKL}8K2-v!JHjW?uQ=g-q-7nm5fe?wtJ&dsYyJe+seB)1(?GgK}N zag2FbwZwKY|DJz5g+k4lB&HLonG1MW%YX3yE`ZtI4i@%J*93;aaDLO-qtf z>`L1dm`^XXQ(0rC__CAxXDH9-Hw^#Za?V&ER(s$6d&2L-1$Fu7o)$<%iI-Pb@3myS ztLu>=$z?G2>78Et`yYN9|I$)&3-OM<_51Ul2anj5tS;Mhit+!hZ+`5%By{=Dn=1Kx zUtOR5_2+&2+Pa#9{I#AL|FxWV%kC}yXIRf%xBtVm>Fe`u@4s2SUA*S|yS20ay`6r) z?)lW!`+mPtuYdhI?(ZL&hVQ&Oe^1Zf|LgR1{T&uFR_}|}n|E*f^2hi25_%r~ZTI*0 zujVY0J9Qv7Y07ECQ{wHbO^k5yQ!)Phx_(7)8$f4fRUgT=$91+u>@= zv~R_k|KeI@^d5bkzA=4zhgCw9x2tK!XE~G2mFJi&e>{x4&-_vMknW0k&v>#}1zyPS<`=I~(%FK*r{Xz@lUP`~BEnW));Z>B#s zJnqW(MCiO*d!kO<`F|DVKOzO#tlxYvjQF+6?QitLk{+v{vwMzgsIDoIa8SFq_`l@; z4cq_n$uqOK@%mdQ|IRy>u5fot>Q=@b2ctJ%>iM|e<;v`bn|H2$BpIl#W2$rJ++tbl zi8tOp+GU~miZ9C2N|3X)m`>7PGwu|`6%$a|G$6d>fUYKSX}qDX~p~q z9yT|H3!aPrImMJXT{+*LxKn4NjlqBkYwL~c%XjblT+|@TF{O6b_4t^cJv$}69a@|mB#%z>?h}!6Y1_ms zAl5LK&8BaG_TN6nrt<*~)&|_xT`PaQ+;sEd(1|8zy8bHhU3-$zZ^enpYiqZA5M{mU)=vJul{3u|L$wJ1JjNqwk`el>$e?muv@9( zcVN$}-I?`gW-y+U;6CrP@_pD+v2*g=W|F!`&I>={+V1~DL{RkV!st7>8}B|$JTO6k zYr|##>lM!raLwkEZdQxiu*Pg(KZjucQcu}CS{FDTS+uS`D4ter{5Q_zRe=uQvt1!) zx<7pDSutN&W{QF+$Gt@?S9#v(-o15KJoyV(GmBHMk-^1jJ2~6}luejA!nY^zT)V8B zepQ!A)G0$QllOb7sl!&w1e5)&#j&RvveH7lOQ$_}7p8q@?%t}CVOAcxJC$Y^D+Lvo z2cKqi)Y$UXI(r`9?~gy0RQ@r~3V5lK<9f|7&ijUe`=cXsI#^U(d@{ZsN{#8(Kb0R| zZhAT|Qa9Rm+NN;ZEX!GE)5;nDH~%aY})el-kwOyjKIQTrxKC&w3Pa1 zUo{pSvUNBbGJ)mfg$e1LG7mgnaEdO9I=QB|W1g?)ZPlp228Ly;R24eg54Q0o_WC~d z4-c6ktmZje^SA5X5RsdVSB@}Q7>Rz>xpGP=EqrO|?0|=@uxz=o7lBl2ZW1aW&l$Gn3q|cSUHNErE(krW`c0{FpQl0nB>u&9}(#w;C zzxQm?tGl`1`@_DS`*yV?-p*)OY~FO`SAR_=MIcXacYT8U%cOTKwWZ}t8# zGwy5ee?kA=`;NbTO_x<{o3eh@_3+#Kyx(RVk?__$ud(Ild;idX;{UejJ*#PoE4}+X zx%!#%n(M2-uIg7b+1&j6q3g=S7F*}tynD80@2lNF_ift5 z56`-OeA>KV2A2f3^7RKIvi|)jSP|n~bfx`nyNYbkNtL@2;j8a4?5ukI+2YBsO8Zrn znqM>b@1Dher0Z(=;Tfy1p3&PNq8cC39lmzwl+O;wL%cPY9!cBx!k|3-d(@V$PHCB? zS%Im>>+{Yod04`8C*aP!+nns}_qMFkHY`_4ZuBx>v0c<{kYBXJ&DW8GL0uV%_?$-=9#Or{!YIdbXQsTi~Q-h1Tp6otF~S%;Nmd&e%gBP z&y%K(y$8JwMY!YLWak(i@lfE)4n4Q}pXsp)-e-#y)tqbDX8nj<`1cOK`7~RWBF+1%vSb9+niyc&%*>8@Qu zB5O{^9On&Pdr_lf=7|=;gKp0vr!Gu*^Ua#!{fF|I%l*G;&s~siR}xSs3uo;c>dWxEw^;fUwoTp>-+h%cYD0n0;LE2Mdv?Ei`qBos_DBK z%z`qz9=`4~>-^QJrj+}T?OEN*z;wRPA)hv`StqoaL4r?`Rpw&8{?%Iv0*R#)mNQys zi`Xs9-s8y;y0@$CGn4l6D;i#Ja+dJ4tvJLTCY-n8XL7T{qDA>1FSjI%E>^9qy1DK1 z@=y-vh+Vqxgg-Tut8F@%?jX8V_S!Y>&IL?o?oH`oy}EZPqd;1-lV_-rfQrXDtKW$e zj+d<~|8d3Pi17^mmE!)qx+@qwWHxa8vObcQz00}g!g(c&`3HI5wH!$}B(pMj|GF=R z0;UC;N+O#5KUi8%d@3!wCFqp22hOO7 z822r%-N9LBlD203q1%l8zFQR!aGA5)7&#ri^VljO)G&H+g@WLdgLZc#o7OVQEP1Nw z<@PV&KNHJTrW4Uuvmd%oo%t!c()_{61mQlNV(FDzj;blxPUUC)8XHr1`gTOmZ%c)h zbDHG@o!&f6Tz;2N`N8#y@(b=$jUI4rSW>8a-T%j&uRo;c-CH{U;hp5t4Tsm>iT%9i z`|6&L8|wnDTJK?g;HpmEW>Vy_iXC9EafPAOgBj(IfmrxR~_{FUTg3pRNlye3d;8OY_q`G#YOSB_84 z9)_Jft~nd;Fx*mLP$*m7dFT!6yBw8sM{?G^a?MUOFD_Lt?pImYdgg%}-v;i&YlkZ; zP8>g6{NeWXS)V2Hga7*-3hf9#q@5HwEiT;tFT1{YTF^h&hpxG*f{}qY+$M!>-o5fw z#B0CPiSz8Q{rQr5`Q5UoJG76_Evb!)Z@ns2&=h*w$6$i<*_aGZwcr#(#kBG(S-PHQ z7Ak~1aS&=a)hqjuQ@Yt*L$RZvFu!JFkD>xASHamy%tDou*Wccp_o%g%QG1nxu=$dZ zikW>=yM#0u3;0)U*)U_8j47*zlIWAkXLDIQTp}W}c;-|@%KI@3Toazo$T(5pu9q^$ z3dM=+N4%yPnlHG0!g!g>niEsGwyqXbVM@DpDJQPjeedD9)1GrrG?ADh;%AVt@zsZ! z%esUF7+0FiHFegaAPbe(=&KU_~kqrM7&r6Jot4l0|5?qaYJoI#zRNvg-*H<&i%hgfB z&~0K<^5lt=E(FegckKEmxsoqtMWVL8f4=zcn7!xCt6+{zEtzIXy{DYsO;Gb&BW#lW(<6pSW&X)HKzDtDI*PUYp;ppd`~|A~SU#Z&chH!`h`mmRHYj zwDrAl{^+lhZgEk<_v~sW9$fz8*}G?-gzGB*FPyq^=aiUJ^7a1`&)qx|UR$pDHm>H@ zKV^pZ;fAlad8QXnuHQfB_oC=+#RqGzQyzVX7l6kyzx8p`C8bVwF{!z z|5_jW5x+Gn|J}#`&rbX8D4ARQ@6(RCja8x1>%+^pU!VT0(y+~Y|EpAeqxdtXZ|)0! ztE=?b|Ek}?e%hkPbYNfC9 z!MC42n00)7YI*#_vx>`UFB#fHxOX}`DEV-Sg*{%iZtKsQLk&r0BDn|MS<<35{gnuN zE;!-Ba&cn|1_k5QrqbJA=w&@+J^1s}GWDQv?V~CN%`?BIX#7}OeRSs6m~7FBb`v>n zdpx`(`RI|*hqiaB8j?yLGh}MQVrDo7%6J`f2uN9AmwjcS{?>bCdNumROd`7$U7f$H zzUqtm<@Rqefzfw*R;LFTgtTTmpGjPASLt|Vqr$7{&hNBywr^5=%>L(FL;og=`TK+N zZgU^`X}V}%#uZkhTaS|~|Fo~W-*QKq~sHvQkY^|t*ZdES31^6oZ!q?$!nU)52(=(NLL^m$sv zMoGP%0|yJc3Xaco{t&hJ+>zeu4K5t|8@_#3WPA~%{`rvf`d;x_zCW4Pzy3M7Y{&jX zZ|A?Uw|{WBdJp@P^gp(WdL6$ax&BpYJ&BK-zt15nV_J9Lu8-!A{_^clIm>-SVcn~q zfPfwyr2`HN3Qu{;D;>M=|L;4Cl)2|tm_F!|5-Ob}qTw1FpJ5_3<=vv&Rn>FvHT>Op z@B1#xr1_~0mze9iJ)(DJvbLCA+O2m@_|n6LH5N~jEuQ;&ZZ-(f-!>sBRBMd`{|||2 zmz?~kF;|Iw+_rh`1;MBfTcu<@I{29XEdHnEVOts}!?~*M(X7%hk+Y(NSFNj^{>u%CnF)yJ1d_Ovj}%s!O1bT;Ge;Qvn>73n_nRNC-kT?P`gqr?4?Rf*6SrL5&3Kq|_cew} zjro6CRxt0mRWkScuJiLhdf%S-&crdpdeV||T~;lIW-Cu7Lsv#6frjrtp1S`jY<=6o zyg(qU?7pDk$MPKh9d0 z`0Q50f}5ZD|DIpH<=^zX_3WxeW~D`(a`V3gyX-jpo&U$y1y&E%zKLsj@XkT&JHyO1 z6W>PgHElR{wzuB`iW`Kx{HtW)c3?mgOF{da5cNv=IFYFb}&I}pits;lo0_3yuVU+!bU^4GdL$(yI&%~96aCX!<%$*^T(xB17) z>Wu(l2JHh(3Pg4zGe*qh8*< zXR8#<+D&?`dgh99f6nk!yQ&&?SUO357MJMKFmnb@meO+Li(#!hHcRxZiQ+t~b?++g zlaioAvu9Zy+|tDG-Rf-n(r?#N5^-UpH_oKD}pKmJ8&SeT@(5!4_nwis!kM7N~mKUrAOuX~kON=#?rbGWYvbg9n|0)%W?nEO0nvm1S}GF_VU<(6Uwi#s|Qg|W$O!DVR1$UoEy7_Xaoo8Rs_K6Cy z5^-qhXydwnBJILm$N3B?Jc{>tQrDkg3JFijW9;^Ro3$d2zl#6FBBm>swykUY##+_< zdV$i7m6LS;Nu0G)#vb5ymbH0-I?=?WsbEUkF9<$U-$3flc)PUiszkW-N(4iTzyqK-`iIHi#ypXSm)J? z2G*xes5>@)`hmyXFMizDe`~h+Jiq_*p6?>g`(MqT|7X8ue|+8jfTfdP{cAn&uf=Hp zEY>;px-HMeV-~$u=Q>=R_w%8}5s7>IcCIZo{2)BhGCkO~e%7kq%U@nOpWD;C{_$M> z&DZ|>{yH2fDcE#n)%5lkk_G!Lm<-zr5>rmN7`BoDB1?)RO7x%O}0#9sfg;dN+z z?N(cU-*df-cAC$hx+u!&VUoy_w6iz4%s``&dJGO7+Te9(#fBeTk^{GPK&jX#Jr=T3_}XmyiaZ}O__(0ogl;`z%eFLO6HuL)<^Jil1q z|J={sjVY6TIXfed_-!hC_Ih>h)=&4p|1Ul}`~4gLb9IZpZ!x}m<;QcGQrp8X&)#og zOcaAQl{Mp!EBl@{b#H7rpx)WJt8mu(ZPLcGk5xVlGCQ(jp^+A z9GpKI1pA_Q2@47anqC$vluAezyyVWwxw^?SP(euY@xrXFd6QGB1D%;g&nfbj{Ya?) z+x%zwk-G=wqI61})?Z++;Srkm`h>9D-|Y|eRYd&aUYyPM=};|LnR;$=i-Z8*4;Bvx z0ha}H?A`Uu7y=Ie;XL(0y6@tTo8RREu4u>XJ=p*K$I*hAWLZxJl}Kidr3ZRc4H<=| z^DX~*U#oeC;>^N{<(FnwsEV^YF5F&myOEWj*FPzR$-k_k|G3{1r(#tv~oVs(<@^-%<2Zn;JZ$(pYJpEeAcQ-xGT(%0d zvUBm{4M)FD%jcaop-)0CH=*I+?nM1d&Rh%^X9TW1<0Q!Bcw=hqCcV}xX~L z-x!#hoWdhgLtI}lzOYqMbD6s5w0!fHCyre@yDP;_|8{P(4OCrG;hLEk#rcshzt7^$ z2Zk5rKV~YN@=9P7O)-~wE+{bZfrgIGgCnsnAxCTvb)3Im!~IOqU4W@(V>D}vLqv?Z zc;L~Hkc!^-by34Emc$au13OCqd<=ryw*hyZRcYh?AWc6;)ql23B$=2%a?DrxKPa-O3yoaPHN?h9l2SXS8;7&z}D$ zzj4I{4i*=8L4yaPQY#rUH*&XgT$&_SHt8C(#+`ktTq~5AJp@!!3*2KJGcTtpXUto=ubmpxkThgX@%@lMjbt_w9_hnU-#+Qh00TJU!SH3ACJGl#0&HF2w zv;uflA4Yj_Ol@g#o%L|f27~Yq3H-G_n|&{^o?2(ZD0HyT=n(57yR?4keO{gEwrQ;n z>kfV8^Iv3e#g?s6V$F$*3)XD963oTM;n}iailXo_jXhU)tK0D0Siezd$tk`mr8fdH zU)FjzOA0e+u8PiSUNa-nAhf-~bY0FP68)y;RaNQ}a( zx1A@7)^JUJy~Ntil}W|-`lqz^Y7MKKZU!l=%}at0I?FVf?EikOI^VusphcA>@!9IP z&vZ7{)!cep#-5foefhiMg)d#cF3PU@9%}emd)klA6+d>BuMIsv_j#)GRkJl;O>z(K z%PJ|qsWk86o_xhuN_+MSEY&wX{9bGOe%|Ka85)T^)C^(lk zTNReh*f?vZRND4Fjw7cHZU>x|*4!zp8E*f2TlU&#ehexpFDKg_UZhkrt#r%8%QwaU zGR=%WH1o`gl&`{{+*#a|jhL^V-&NA;Dj>M^{T#tYCq;{%&PW!YbBr6!wc41>HLP8y zM?^Fk3ktI?VK6m_uGdv~wo|l(jaj%yOj$rt(Nb9A_OmUAnp`|q1}>MrJNID+xA5~h z%>|aM)_@u%iIkaH4R?3(9I#l#@YN+^gBV9)TlC#oEtm5?xSh3sn143^ zj@aH`C%@kHahmcpaMv8g-5yIA)=V(S-_Q{f${T&p>PEuNW8C4zw(bkGZ++NQdQ9s~ zos%fb$CL7M9~2`*TD~>u7hn0?wC~=s*!Zg}ZZEo~SN*oMeyPDk){A@h-BO$C|NH*w zK=V~jLjFxG<-$q9Z~EKMZF^s^^H9TcfvfZ?CEI ze7uR<`qYs#uli;Pru(F?y24hwaZVy*X8QN=9o6mfI~RKUK6bM>zHPqF>+{Dau*vgI z+fjY?w}tiH>Jt@Gfef!oRGZcaABjG5E-atJtDYr8x~vjx{hSu{P@u3p1vw_+MYkmLK+E}0v*gv2HqCvIr#aylPhEB4>K z_MDg8!mW!=?|AoAdCG>CXADt{*&E-e@*NG|Irj<2+G7G+C;8h(8{e&0x3_(4^kx?;y}HUEvj=g9A_lJ|UY z=l8A`&(&*ka`!mPe<(a=m~gk`>-;a%C4(C@UZwvjI~}dTJ*h!^fyVr&^L)d0$$j_| zyM5m3_ot3;t!0;){rdOGp!ly(p5M;v;=W#+U}jv0=f-*#LSg9yv`~8 zRMTR%w^mW2Gn&McM++rxFp^z|h6gSmc+ zx88*Dii_6^^J~qN2sS#|{deKEmET(*oGwt?<7lno#_kZH9gx_v>VoPD=2gdT?Xh9> zyl`hvvU0@v{;O$8I}$#<4JtL-^la0Cvpl?=jvtr~bc?NA#KWk#;jjm5tkbGE$9syV zSIxCMzbpzfT*c%Rb1^s|!ci`3W{HtQ%$$%$#@}1-7-?+@)75Q0JNx;hQ(MOM~{j*|gFEedwtAKI%-ww7_8n&dyv#rbGXfa9m7p&jN$$x%F3&P-uXk9<)Q zZxir*e)r`>U)iwg`Yg#0OPy9&9-gwM^z7`twi;fG7aRUKv(Y%T@sHX5{l{{h%wJt# zyXk$COOdzK@;KLllE@;CKPzS}EPvIYFW&AHm?q08Z^*Z6|Ks=Uch^5%^-QX1%drR- zscln&W<8f_IwEnlXJ*UOa+|w%_c*kEooan6eB(9aIIT1BLaQWYl6UY-*W0?S{AtXc zy|05p-|qHnn!|f?R^^ZF5tkL+e#X{)z5e&1p5nXw8!!IqbMODEq&tV>@*MrohtD7R z@mYNCarU*>Yp1UHGl~7<`-yWe?-cpBiMd`+T)%@ee&3p>8@H}jUHaoy{PN6w^%XMv zPh7HIe$Y5@?bgPV;<9$T*8S$n`)}HJc)Q>I?MXkyuCKb!{kE7n%e6P@Orxb~wOP`; zZQSaO4{rTuLW^r7^O~t!8y4@h*eqre zyyQ(=(nebgm4n%){ZATpe@QrIW1ciS^!m07^P-Olzy3GHjekMl3NL|e+}Asg%?+&e zlgYlwkbB>M;dPtep&2X|b3~%j)C`_VOLPdn{d>}&=3u+nGqdUqb!W{D)Wpo$rKLY? zmHGNjrK96lPQ|&mUko-gIwbC5+RrHQjk`dAul!H4i-h6-u6ohQ-e0S;&xdalg0duAPw-eGOGPr2RW_%ZnpSH`rOBlni*DlR>4-Jld4^;d%FJ%9B94w=+ogFd6Q zf1m#?V7ggWG^5B%sOrf<(NKrPy-(J0ZSgX;?K*BV!(`SvhcC+i?j-)^tFO?nuRFF# zym#04Ju`haPvO`%&3yhBeUYY)nyQKyOBY`_wAfgY(}m^!r4{<=@o)IGJOe)4UV7`f zK|{dz2E!M*|93o3&Dg~I{)u|qnL_LBSVpmlEmxWC81K0} z*5AL2S9O>Y?a~j;_dw^dq(>R~@c8`S&u{f6N?y4G|aG+s@ zX2HCy7tI_ZkKO;cy1xGqT&isSx$wyxsRNh3Z1;Ytr{Zk6plyk}4gP%EeCG?te})Q?2k-9vE?uKP$4Q{W;)m3I>DA5E zyZn@@Yt{(2d&neSGxL@#YMtHU>uUEv^Ys_me>c`7xKygnUY5iDAgX!Ek-q_aN*Yo3 zO6pIP-I;n{!Q(~@qvYd5mmET8D6pP*{>i&_-IK0HmXsgMKb;P(+{|~~*iD0T;-Y(( zqWK~>*3Wv^yj0`hmQ_2LY=st|*02qm%%`^RVDF*T=Tw|Hj#eGWvN<=cd0Fk=@^#1d zU4JZl$#lhoR|>z`|KHfqIr;VP&##=76i&3Bs0a+?ns(!9`t|ZXOU(7tigFkF1vsug zTX`_?%2L4@57Zizgl;~v^eu>VH&h+wp2=f$XJpAwS!qS%qxRO4k{`aO@$4Y6rWWn(_gpwrAxx3zNODMXkBhT!uw#|%q7J^ z>d$w$G6sn+DGyyOeDzW31{;TTrWl{SOh;tGUhj`z`+v&k=N~_;zx-qR=^Zxu>YI#q;i3$I`=iio{)IaEP* z(jo1v%f@L_x*QoU-afUIcajWK8k75xp2l;{Dw7&^a0q!YNE|#U8QS1c;u4oEYbml; zP%&9+N$(k>ga9e6D;r;>#%^8KVz;ht_Pcc3oF{rpVzbmTCfs>_>&1l`rk&Sv6DFF* zpLChU*?54}(uqTN#y$hJ1>bL_1qcZ+tXd)Ym}~XcHJ;8>J+E+^On-T#)=~X+*R2>e zt4{Y?0R;uERZf#xLfN?#eP^%=9mp3@>+HAUvtWvNXti**fl(`iVR7rbk0Q-26N=6i zU)v!n!Xu&ZVvRVjLt>zY(X)nkB4UjympL|Ru7GeMBa;uvxZ{T^W znLnZ|-6B6Zgidw|w>hwi7z0LlqGW?U@RQdbRi@uJ6XVUejm)FWHzpmUV z-uLBBtjRrIp7o*n+b49~+q-wSie~(>HHM#$|85U@t@`uN(S^!M&sJ%DnpEzy{^r+P z0U1g)yvFsZ<>iY+ThhP1_Fi|y!0($Y-wWeceQxzd1qF+YEpOgg+PN|FV~en@pbEF+W2+q`Y2o%ITx(P@2q^B(THPa@0`9f3Qx>Bw!V zIPIEwZ`O?q&PN_bWtTD}UG-YIc3ava4;>#Lp4MxNIF4u>O^T5zj9Gi=>5;eqKb=HN z?X;y=gt~UlwbktCoXQx-z&y!v&91t+PA;>L?wIfKo_phUkuz^TsM?yhc`LJJS-zUS z>AhR+{G9ki1ECPMlbnJ1e^?mOb{$Jg4sSTj!5z8pWpCW>eZrp;oK;PKNblY|r&=)Z zzA6)M<&D?M!8d;BD40I&h{<+bd4f+dyY)>|$m*Y__r7fln$oF0!zf~5@6GoPF1w~# z8_qPGHI+Nv+E(*2t9VCMazb6`;<|rxXRe-QmSvZawmWeWN9y#nfNb^iQI}7z-z`&B zx~Fan|NmM4C6(&izv>=O3gBisTg-S;`nTYOGwt`^T%zsk+l z=x^Bb<3%ZNcs?!reX(KrugHn-KOS6j@6qoj&$TQ|n9D?;vi}ZdTD_y;{!RHli#H!F zBkGl6=Wl+Y>#*nUXZ6qnn*DEVY8!TktWwl_ zCw_VIwPMm6cFy^y*Ry1v%(`${dFnLw+0NHrm{-*vSm)7pspX(v%>CJxb5>cD=&qjX z=267s@bJ%u9U)$7PdOBiG~8Hb`To&v2koypA42b&n)M0<`B-H(u(YV(XwaYFmW3NBgDMwQd3}%JMw7r+!mD z#&&qipH?p0Gwr22Iz4A8Mc8a~YDw1>N}A_z>Bu*^3HKg77i1UyqyF>${m1Kff8w~( zRHgpVEC2hAt!EW3tV#arv|v?3v+%yl?*m`O$@BjxX`gTG*2=VR(pCkzFDDyy-!**^ zJb1-!aoCX-i{u^Gc7MMyaf!#oDXV=C-A*ahyX+{putTUS*7t^dVS!D_4$T>9lGm#~ zo9(rIaXD&R;(#V_mM@t#xAg7pBc{IO%_A z+p}Nq^bYvsdR#g2{MD+nvmIMnTVj}xzIx?7k7deJl@F}h%v)<8`fc5~Wio5H>Iy3+ zmUVTHmJ0@O7A=}p>u%5Gw#(8-j{G$}E^4tWe38WA zP&xaVd~a+6RXmke+x&6c+Q6#wWaZ4O*>gQyE}6ETTf6JjGALBrlox6iLF5TS` zcEk5k%IAyA*k^y+AgH327S^G%Z1}=I=wtgiVl4&wwgGAPrDTZYo4k9k;bJo;8P;eAz>51|$ zcCO_Np10%~#{|KrCf3W1rwb`a2ro(wGI80@DSrFGinW(t1*CR+q+J!C&t+jPDKleY zxW{t!5ZmZQS^_heFFjzNaENiz6T{%jq8fK;Cl20(?P3!n(x*&aZ_N8)Nyv?hf$}PT z5@+X5$hztm85`rxcxEfNl2H3El?darY-{6sWgI4nX605Xb}T&PvDeULBImB_v20c< z?c0{>ByJ6O5=TXy}d;U$k zXM1Y-BI^~aauVKbou#!nO>$wk$=@}B=~I>avXAOcO3cfVV{clg^l0)u=8g{YPkoMy zPIeogKT|lbSb4eGrBAJPm4WTLiyp5pt$+6O`PK8=Z~I=mb?>YG?)~$;&qkH*x8LS} zBk%h2if#IaH@qW$T)OBR>b+9wPR1q8qjS8qA3UEk|LGm^nXl5rcCNI`p6CDdcI4YX zOW*8yQj}-$d`{@w)m`5=zkI*?Y+Y*h|5BD8DPHwrTkMVJ#aYd-U3|7}!x4db-+nK9 z7Z$p8DOdi{tm~@(j@^#F^Xc&8cZXj1-QTx!=aG$P-{ctG4|@0g_u+j!&oAdcQ~JUs zU$=GhnIrKMEPH~snP+bgZ-3#WwO;pW-mGVz18wD>ghzFLUG+XF>t+1w(EC>F*_VC3 zX8Pfq;`@C^dp@)lFN-|Ci=oXjVckxi>bQ7j#scGZ%f`EVD>4H!&&nQ~?aVsuMA8iZ zomRzN7V6&~E^S#@_@LQCKuPSp#ya1cO%v|Qyt~5G*;!dwQ_j9|x%T;LZFlpa!#5=l zH0+Ez{3`q6ixqs#J&EtOuG(7J5~(i46m_nfUFW62vM@!3t+%$E$$J|e{X;~|Y5UG- z?p;}nS=%QEKe*7cJ?q_Bmcn_PckdV4xgx4ZKu}F~G_S@LB0!M;O9 z*?VIYUb$o(p7(8P?gpceE1rGHb+KjQ|F*DEqrApdFzVU;v z%io`?{&uizTwe!N)?R$M^clnRQ6WhOkJI|zeAkunw;F(tAH#(tJ zjVo5aGC9BWikPsRCsh_Vh*QUo_R3M;;Y-f^%_fLlD&lZp8ha+^K0MjylaXq z34Iwx4{p5v>Nh{B>#h~UjoJ6wte?lM$yw)Yo%j3v&sV+~O*aDhm@b^0vCHwDQU^y{ z)q!S@s&7K^MOXAEo7GIQ)x4v7zO_SvWrjiri`7yO`41n2eoPHp6qeEdHY~5qW&^L1 z`Zn%ereV!Ca)lpUcg2L?aQWiEy5ZSuyOUe$lOFD?pZ>YPZ5fN9!>g0le@%RNqt~dt zVLk8iZ*$}?WxKnHUzHa%8uhAAfBWrQ)q%f^{VekeX1w}nb!X>q#`d-OswK1P5;|@& ziwVajawsQ-vHn_pWzE*}ZJ#BNSO3{|=v(!lmkr08es%CS?KqY;#d&g{`Q_iD*UJBt zrTdGvwiuaBJeT6sCgN_pbIQr5;U?-krSGhrW@8+Dq@_?}xBrfKZ!75s<#*iu+KTM| zZ{schTNS$eqK)iJMX#1iyH~$Yx3;UX%Uc)D(Yu5RnOuNk%5GAAT6sB?H5++gJA zy|KRZ&&uDKqSyZ4|MTA~@$waxOx^rb<&$Og3eo~Y+!R{U7P&k)a8TFDg1O<&$Jw#P z{tYXp?R|5yLiADX-0M+wul|2YueE=mskFhxAVIOg=-5*!0g)eD_e5U)CjO`UAoH37 z+3P$PEzD}xyPIkl@SXkV3WcPTCjzDl%+6#vdD3#@-Bs%>GOx@PyZ_2;L*6bPl z`1M@Lx~C`Ho(b=4VxCy=V(IRSmG7;BUMv(=c$as%kzvx2Oy|37f7{kBI#af1`%lh* zg6rqR+pm7T`@eSY&)<(9&OiQm|DFybOPgzzFV^M#wmUF)&YWH!*%%A+9>yax)fP@z z$@K2Y#3V;A6|Q9W6s0KLEA9;)Ov}68oM_>5IJRO#Usyx9J|n}T!$+oAFvd;)XVbZI z6W0_$sly+haK7m)@(K@cpB`*;`{cjcLJ#4-1l7r+%T==(R&JPg%|WR}Rk=ve-I0Sw zn2S#$@^LkHSoS4>-$|lfT|qlkULEac6j<6dHQ4Wk(bRQoy;mu$NIxGVt8h}nbxNpE zQ+7yDBl{OgcV*kBVhb)sZ79C7Sgtimx`4SOf?L2~n?R^g+#~_RZH&hmcRPO1&T;qA zUL(zL@7$Z*s_&Znzc2lo>J;!qaC@zUzAT&jWWzXD&%NWk@z}OoF)H0%)na{;mzTIZgoH>Ovhg=&H#}?n ziA!5FmSeWkGJ}hL*TW5?Bm?;(oa_Tri`Kqfx$SDMo7#o2bs^aY7$3?y2&uFPIKK$8 zdVR8W=lfupCXWAovAe2fn&{7+eLMW%&+v#0mzbjSj(Jf9D|wcC=Y9Kre{bg5vb|R& z4$uGpb^5gx*Q4_D*Zm5uUsN+qU;9tcYavbd6!nS4*GuE04w^Q8FOYc_<5!vdMd-uw z)AG}pzE06DDeI{>6^SVO$(^`o)vnDmmx=`UsBhlBbot*fp1A6>4=q-_e15)vRaukL z&!FA=?&j6C|BEf!=~4KfYr~zW``!01YZvU=yZ!5*^KE6mIrlBX&%`ggW@_`QXR4X? zHhsZQLGv%4zNOZCImT;ie(|qt%~-GN;kPZH-+$;Ua54O=^s-Y5$}@hgi4s)a`fh@y zn}UXVf4JcDjn&=L?Bsi=y}Ep9XKm*44w-kWG&?Ro%GoQkZgRqJ8>`I4VnsziG#`8` zT0QrK^rEx7o;6X@B+0Wx?Z}A}E7IQ9t;t#%rn^<(%B(=Y)8`$s zOF0>SPSWRPRCnw=%W~C$>1BVvI;^@I zv-R4A(lc+~%`!gvuXW~~PxT%y92I%z z+4xIqxMl6a>|>wHiki*;eiCicn834g2FG6J$mLt6`EC!(xOsKj1WcJN^iARX$Si zB!=gknZnxgT*{>4k zAJqBwsbOziS*K6#ilv`(51bX-pU1E{N!`_DK%J$>hcl?ZOZnz&1GTXbRa|N%m z>WAig);5Ldlb@`aU1yqf-R;-)zh8BZKT5rEeVX9LDEGA1ldDag1Gn#xEX=WrvywgU zT2uRR)9oM5+m0F9ys=oVlS^Zc zVtK{s1A@EL&Oh6by+^X(jjijFwXyQ;mCExy&D?B!Hn*f+SRihCkkj>hOJ4VE<+AwO z9QzOMes8E1vvrf5)48smnr>@{{u`W!nBV0W9$s+rw7~oPm+vf|ADO43nl^XqCSf7%*E|}S`e(3tkO3BY1_ZiIZSh<36#R~r)0y$UD?`1LEy0R}R zDzK@eA!HA0OG);`3vvP>fpQ-*k8L=5_TKmEyQQ0gu2}EMjx2dT+cx0N)b^OK)9$@9 zFgsa0@AUIe8#}lQY?j({9CDO@dF8^pum76=z4iamd34`(j)m%<1eLWXZ`jsR_Dw((d@ zs5rZHPU6W89cTRKv8v~Y>`P&vSNHraugcj6o-FF#xj{_V%{JQat$Vpi>OOl(SNI{9i|@{%|U1=3z-MQwz7avpOm8*vi8v zyp6dEgS1^j&OYdtGCKeGM?wD2($t$@?eo_E&wg`n$JwoK9G-+N@G5lvTe{gHGJyr+ZL2Qd9dy%qi1xdc^G?=+RY5vtumfM8yilnnUMYB zUWV-=nKPSi`gk3h%e>CGkYUc@Q){DSIa~^R^R#cjx8<+;lW@FRU)a;cXW8PGuV-if zKc4lu{P^Afmj3%=f1T-_Z~r1~*RFq^9W!Ot-CQlqa@NqJ_wGVYP7dQ`3@ne>zL%=V zIJ4ZJUd_Om*_*w5QQWnN-J#7UpDdLbT{Ie8S_In<*lIZG76^m{gv8n0<6-S^5n)}L zww)*LnJu5D#Ch%<$G!(lYyypaA`!keVkwNxLWLQel2?ohE7oYpsxLG6rNB1LK#5&Z zLG)~k!Qz;czN1jd{X--wz{B|)kB8pAu9eejncuQ5bP9dl z!zkdsPPesq;=$7k+8bO>FI~-fw$~gSHBj`%BJ}E3pnr zH$!yG7VV1r~S(tp{~t21`2r>^`}@r33QKb>kK$+{CtV@!whk=pKA;oi_T@T zK5z+R|5mZJnuFL>R=@YgZmGG#^jl#sA-4e=E=2 zckVr-+xuD-A9w%xd|LjT?59V0N7FYP-}mA0?j`lgC!B=mE&n-5PtK_#Y=iLBm|G%` z=e(;c-kZDS+Pe2GJNJm(kmozDANuI|{PNwk+!gynKJCA;JOB5(xjM(cuF+T*pZw$h z?5h0o`de@Jr!t*C6Ms(X&fh!L^}(AT#Kq70xi!t#<;86y7U9HY;01;>6s?Ui6^+i=oR%h_bWG@S5})b%x3fyFtSZ&ZS;m$d==|=37{lSP+3d3H z(_9>1HLW&s@DbU;z0j6P%I@8UvQ$zA4WQrx0*Ng-dMbNTZ^ojM9$XEiOeNy zKlcYZZTlwn`=LRK5(cY__vphw7&l|?&;PIXJ768d+pTL@*j^i z>!0dngs=G#q`LW#+=qqEzfS%We4sp!??&veU0c_8&zYoR%pvU%sKh9o)-L&VUY*$8 zgR2F@mgpIAHYEe{!_TkkP+l(h2ajBX(?fL22 z6%H0>vW1wqCgzyGZkW)=C>eijvuW#@Rqv;X`sRLk{NQ-@WR|iix)Bj))+h-_2yB`v zxnF2eghlS`?W?n7++BOQXMeO|xYjMVVBLn)$--Z2JJXX}Iu2~R6JD^EAzL_I`74Qd=E=Y_sm)g4bGerf6;IGWIa-kx;wIDYP=~ z;cm{|g3j$r#0xXT{}cyKP&8ngA0@%)D`Ed|-`wS0Qep<%XD#Y-bG+vD;gQ@=pN$Pp z<`vN?>jHv{f9T%hs|d##CTw4+)92Wku-Q{KTKQ7ir#?mX2 zW)B=Zo-Z)92wQ3Y;&Na)hgdL{NmhB6;%w0+m?v3*+R{NHgbNgCBPAuq)8#j4h> zYAE|rqJ4jJ+vj5)Ez374uen~d{qo#^GnUT3BKg*da`ZeGzr1Kocx!%PdGA#z|3z_* z0befVe=Y0hU!tj$SMc^p|KWeRKjJRmVar{YHG#pRdUn6a)s77ZL)XvNVOF+#@s~@w z`R$X3v)=PPonXjR5*6knyDnjwoH^@vUfZlAUxQ_4>;3q7(ueQ-$DGr5-&GmKiKzAp z95}l2u3m4%f^|ws@1A=HR;)Y_bGP$~tk=DSs>lTu?Yieydlya9anOiJ^oz1><0?Hc z)#JonPEn3^i{7kR``kElQ!zhJ`n5o`ay zh|8{I;t1O-<`=#1=gH(pb=(1Rb;+JlZj(~t7?$lY4c8Vnv_6o?yE>}ujOV4b&(v2d zFV~*7qB8wy=MMG0hTs?we9f!L(!qj!$)%jocEnWRn@RZKQ zU~%*RXFo^G`Zc>w<=G|lO|INSrF`nQ5fBxQrw)4mD25W|Ix1YEqi&bR7 zGZzJk)K4NBnky%B2Bs)pcrn>PQ^;#2$H8A+o(BZfTsB%PSd+5WZnA27TF;lFAnlx2 zM{g=m($g?9^*2n~QO%LGMs%ye2`P1@?HcY4%saNNiS*m>=E4EbBeFbGS0(3eHc$#z zW#A|a+R`Yhm*f<%!)nPZo{pWWPT!1@_w!0miSgT*o290^+PCUk&Xk*~!GhBr1V8kJ zb;ZoRbFR0eue75QH*n)XaI4?65JFR^Khg^j6doQg!k*6X|n+pXpv zwh{W(l6NR3k-;iQ=u$^=@|vbEE9=x)7Th=~(xxDL&3M+t+c#$)aqr>sy`kdMd;ds6 zzuowq?AS);8C}spjUF>?-Yi{wn>5&6^I+`@osOBE@m=EUQzCsTpe|N3n5J z+>)RcfPq$cTCNxKHR2662r1kg)&y;g)SM*Qb{>OU# z?um@8uj+sN%gt7}f9LL+U;9|su6+6XzFqCst=dcHNqAtI4-t zN?JUvJXfsuT=SU8^O|4Tp?A$b{_&o_^IPf8DN$edh%26W`}XV44`xjNRS)^Q956oq zTW0c=IzB~@38(Y^cxy54J2B1rG~=#))v3G!eiQzEp1JD0WafIl2S-g354>M?tu(gE zG_~UEBYn=#@p+$9KipGIxOU-|_Z_=^dk&u2QF)c~^VzT0?zTQHka=fsAIJ1heLZ8N z^qvWpE!is_c-nS`DHc2u%XqrV>(Sq-iC?Uw_k;w8m)n00^ImbQ==u&`%grqpPTpL* zrqlG54$33|m$&Hhu3`P_X!RH%nTG2VdZ;nygoTmr5;+ zkBT&X)ShF*9f02Y3af3PA4N-$*E^J$U~kU1G}AGY1qNv)1qHNUz?|-}A%x{|S+fjxDSQ4r2)>r+2r%TzsVDE9HMNZHBFb^SZWq|KC<%D$L*h9yREw> z(>>Ao(-N*d2l-jQHng8Om{TewYcpH?K)@=kuFb0&1^oNF#r-P!Qs)&#ZJMdlYxYO< z|F7mBQcOW=6NBv)7z9GTFv^QOmHq$H{pZJy$3B%Z%hR+!oqqUtOR>|GUcNd9QI*D* z7Flb4oVDLM<6K01uyn(d$m7lH%+B^5_YZj`wtR)`wZJGvrSzb$rz_0fd~IxTIV5J@ z{3h>%SwTiw25aS!3yDjcx(a(|o940GJW8?d5&xLeVXY^nzfhSe2Nc`HI84X=Y84;-h4E4Na^IgZFlSMwCyZWud5Dim?X2y@43;L%>mub3Jy&g3Xctg6AZIB z%k_31wo-We_vNhjJZ1^(3H|y)P$uRvHW`|;<0J(MDsNmg}l zXR&(l^3z@>i9HXmMt>-OG1uAnbI#qkqjwB;@ffHCYD~J@yCRs;z*Og#*!|R=-H#Q& z2`SA#C&#MN^zK7t@9X(b_RAF7CrK3SakD)2v1aBL>6&AD%G)Y-#=KtjI3oEN&jm5_ z_%1;i%fzjRWDT^xIfOho_+C+Nf{21^3ghpXXTJLvdRc~-zInQ_nSq1TbamMKDwY=M zJ$IA0Pb+m5oXEJlVakQZfYcKjj3z7Zwe3#&IZ@7o!{>hdUSH=u-|w#oTo!exLu2mS z2TL9{ELt8tmE)c7hq`;p7sF-<3wub#uUWc@d&^GtZC%k~uPZLT+VbJ4)W^Mgi#=0W zk|aKQE?9T&$%kO^WQK{2uCso2iZ2zJ$)G<~*|J0N9B=aX)^lOKOHMwyv(@vG1Y_Ud zShdvDe)SJG|KF=|byxhG=F<*^NAv$pj;YO`*7-m++jdjQ1i3RRw(X*x?`PfB zQW0=}^dfGJT*6h8;$2LJ7DB8hbLUCNSgvz4&|csmv-cd!2A9if6$Uk%<;)~LH{%q%sCoj%=X8v5Ad0a^&Av}G%*vBPIS#s;vX}M@GX*~Y9Nj1UtqB)b& z0o^^mCu1i@Z%g>BUc%fFA@hobV@s2Ow%Q@hSq%;f?K9E?a$-ELYIdxCEyVXi;MvVj z#|p0`8h%q0Eor>QdN^v^-Zf>v7{0FAtiUmaGxpAuqMk`g*ZQmOU$kQEdUHYTc7d*+ zVFSmU*7Uh7n~p^VUfJNfCPpjd>MlROw>>--VkzyZwp$bz>+3k1H97THPS0>Xt5+(x za>1z;Cq638<6XdY{?Y8R^9N^zPIO?CVOnw`BTV?3VoTW##)peA1&Ceu`+UN`3;Mj19}VBM>%zwN?(b56cyeq6KbqKQ)2qZ92>>;vuqm{ zdClFr#B7QPW67oDDG^)tyuP2ClKP=LsX3`jX;RRf=arqA9gQ6kPjn1Zlgj-Kzr|ip zTYmejRq4w+Y4ukp{|U8}wJ`lKF_4+5Q9tyxe(UiQU^4 z`TcpWz(eonJMVIGmvKpM`w3@|H34g`QZQN{nPhm z9PL~YXRu_|@9?`Xql63Xq^6(!|MYx3Z{zQ>BeM0PSLYY++jr}jfYij>cjf1Qo9?_| z`Q3+e_zLWjZol0XyDoP6nKj-o3uO4(7y1gEt6%4*&+T;PNm}tmhOHg11Q<=SET2^Djr^(*WpZ1W`Aw3t_sZVwObP~*I~Lg?e& zWXUFII74dt^C96Ngt&5n0o zE@wj)U6XRMkm=j~cjHdEE2}Qsm^<1T`+r`Or8Fn$%4HjkV?TOUXj>$9l(GD{WyWcq zbM6MC=(Ecq(|pV}hwqZf4PMiIV{`b&9PU-w=YI-kmFD{^^Tqs3s|j9X@cHalF_jq{ zH$}Bh`(|7=;r_AW`1wzU3L)A}DdO?l_iVbFl>XezKju8m{+RRT+1Xvk z*47+|zw)Xpf8D?OC4XN|Ryn!pX}fHwsd0IKN6(oxtJb}Gw|nn*i+r(z0cjdDB+pEE z@T=YVM=R3=5e6nE1(qkXx8GSh{g#oj(IIi;;LtjQ{u66ny{dlC?7eQwlqbDiUp{br zH@IAU?)?eBCr&&L*0cBK>RTD}^d)`MQ9g3H;<3ne|;T)3hr-xdDN77@vSvMby$?1LMF#E-yNLy*Ww=X~FEH9oe%@v-(ech$isrJZW zw{LU#l;u9~sIkYCOfOaiPYKuOGy(|F&f} zbGDT=m+*-{XK|&XX3NJ__kx%j7R#;BwOKmTwtn3_UT(Q-rp3+G+M3g}WZA zyYF@Gq>Eaidzl$`uDH6$G5*o&uGC|OxhH%CEthRrBpK>?;6TJN{vU_eU$(b@G^3Pr z!rniZuE>3OH(8jE_r(#XV-?N$YPLu8^t|ojx9Ba zIO_IU?E`C{UrSiF?y9EAv>wf!JH?8dcTSj+YFYQgFZ<_}q+K`f|9f5cx%XP&TSkTR z>XZE9{srHxUwb0V?b*4lS+>U>PPLA-^}I9r%aOaM?rmgBnxxy2Gt)9o@+`xia}1t$ z*8cvyLDes+k5Appg(ZB^fxE{_H(E6{HFvaH%q!-&^UKNN>ybyZ7v)+dVADfs?b~t2|ugTu2*5bZ5-NEp{+n8l9#dY_tye4YgxZWfnB|P}W z0%pZ|9}<G#SPQ_l$ZBuuJPvtw*tJ2B3yybS+onJ-^A98K)ugX|BW3pV- ztt;mvmm3!_aG$#6TlH_&o-3PKdzKV%T;b*Lsy?wOlR2&=?_m3y%VzIh#~j?YZKCv@ zH!_P=>yqX^c>d$zyM3QNoH=(V^}oBrg_Dl2?;5&APJ8-2zn;S}B=^PcDUVX^KR;U? ze*Q~}y`2AZrVTYx%NOQuwl8HAn8+en?>3`)+5Ks|lqTGL+>*QtoXP*c%JZ7NY2$yN?=#vJYNyYOeEG5ELU!YS0X@U&;{RuUotxjbC|coH zu>Z&B?|%97HSe=A`k{RMk9*Je|2NoI->Tbxo2Rzhz~<&6`&$3$bENma`MJL0@2jK5 zg>m5~XT*20oipasDOfV?av3A{O)XQVER8V1#nV>pv(gMdwrc&~>s5WntPh{)eec)s$*h0mLG%1?)Qnay*mpI_rR&!qS0jRuWL-kDdk&%}6Tx*Ur+wbWwoLawA9 z&8C^jJZ;Mto}avkPq~qGUP8y7^w%A+Zv=e|rbOMk)xLktis}AZ7wp!j7Yhm&%+737 zIKy~y;b#BcudbGMEPQXpJXO_VgVrmi;3-kOb0(_KVM>~O+&(u(x%KYn{;OHXXYak+ z^f9B~SdO_p%hlqKkL~O=6YHO*8(3~L<~p}P*v@@pZqt`li+db1_jrCasonIKPQDcs%p49N=M0tN%`BMF;xj(7` zJ3|lOeJICJxvO)#yS0$UI;kJUfl_&V|BhHX>mE4xu$OVdyR6p-zU(+-B>&i*N%i(W zruxz+yW2kgUNdj)TrMS}3I)#>58d0D9UCMkGqK+K5uG0S&obG+xuc`t1FMG z_&nFD*eUa8^RnJ_VXqdwq-}TGFaJ}%r)$LNl>IKxMss@AA=?8Rj<2ayH@DrUd2B{d z!Q@NrbwX;*0;|Pr>~ykTm~Fh>@^MD2=GRSgHm&MfFwY>Sb7qJI=k~jq>W617uWgHG z__9*>%^vIZAHqMr7UZ9NRyb?QmU&*;HCF31%qzA|{p^9Fo@aUR=4wogKO)T@oLrukYiRdM% z+7(y0^@}kwSMTXuC>*zHZ@Zy`n%@IXb@yDx`HkC{Bx_}oZXNq)(06^dUX+6%ho=AM z#mDC`3Ap=zT~y9?3uvyI4ntV64xu{dv8Aq9eNwz_-Oa1yL|fJO5Pf+$`-G@*tsK? zIpbeKWcL+?gbtyNJ6=kvxx4V?&Dghmp47?L4|XrT*7Cp1_nCQg4IaPEuKT#Z>-zpJPXk}&^h}bUk^Ex+ zC8L8=Efjc;$XaCCMc5YJ@VaA_@YLf{@vcPyG2&dzYqIK=Y%n}~M5&Nn=<><69XTFs z0@0NwXODcmW9`j*@_pFdvv)1?9%_6(ZYGr=G(m!g?-8#?6VIwByC9ie@qgKWzlxu> z{r}#3|8-R&6Hf1x-`Dx^S;7DNx|hFCpKd>wFEqRSvnV4=`{5Heq)wlCu+`c9ieT{9 zQ!YMlqGa@ZGG5dN6kqeTn_euNUKe)$S^5#RYx4pV6Wk1^FIO`&4B}#`h*LOM_EBT2 z*cXP3*I%RehaX*d>g^|m4Q655!gJ?uj@r$+_NK$$2|A1hD`VGuzan&u(_-yq9VN9k z;glB=Gu_--k0dCFE^&Qc_WaC@vQ5%k#Y7ri_*#pcr=*@@Fbb2DcWBHjxU0x~b>U*W zZBu&A>&{S}HQhK?xK*Hh$J~NbGtQlCVp_mGd!?MnDUSycxoRCs#szMl z*4G_d-ZP#sNdy{PFrsKJYHoUs5iN}bFp`nS77r_XD&^fZ5lvtimgk=L)3y3Z_lrNi{_GQ0g>+ZksnA`G;i$?f-(JNxGM zNBK?ZyAwPdOIdqnTgPkNxy2c1k#O_b*WLH^Zr@OUS}0w5E|Iq$gte6Kkgd31yleAz-DCebH6PS; za&6w}y!FwlM_M4%!ak08?L{YVVCemK>g15 z7hO;0waFcxvzfCz?RoXC=WF_x?~x38Cm-v3b@rB;Lm%1JYkgsS|0Mm@O^&5g!((0U z^29V7XC0k-K(0_+fxSk7#od=9zCFHuL$Ueho3eY~tzW1vvuR;iN!(Q*V|~ljg+e9K zt6qgmFr12z`*7~T+X|s=41N_Aa*x>!UP&c}u6Uf8n*540^T0p3e0Ljz7m;u81iQ^W zu>bgX<|n;HwPr$DD{~Iyf1JuKGEE>+-@Ee0vPYZdACEjYDeeu&fto85T9oEAGPN68 z6tQ2}y3Zux(BF>3EzXWpw_fY(`&zK7U(_@sf?)%HZMn_dJys_~v*pjEcUd*|{h0A^ zy+E7GiJF_8Ay2xJR=CSBDLs(BWubH@@ztxQEl0oZ{-cifvOzatHnER?Fd*=x?14(&)0_? z3$A{(SUHLF;6l+Sp?7=l@H~6Rqi(F4l^~tE=wpHT-Fi8PyZ8UCe*12H=To;_tSc zf@qWZOK$IuKFj~^l+g^o2@Xa(BwNpwruA{0yz}7InJ}$L#g`g6vKhO}{laC+t?Er; zX9yQ=vTA#K-DT0wgJ+Zei?*Lzv!$$ZLdD$IUnX8M`}gVosejkMw*No&jQ!R6EXxgF zw#Wa?_iz7y{_)@X|0zjx+<$*?uK(P%w0K1|tE8p1pz!P?4KDAl#-0?tx`K^|+n43F ziebven~D>Ix2?Ir=v1 z;PMC*&la1y=0oYD6K4ZCV}lLE&5|xIKeks$@%7)6j6$WNn;tDwdB=05sp!&i-S|{p zzFP_AzhX^U7rPJJ`tSDny+_~XO^&jb4ilQK zlEWq$!`~(P=yJzX&cmMk=WZ+PeR(49mer++Rd;P4H8N+p)?HZFtTI83%}wUq4A~@? zqYL+*Wvyz>Zq>IiF$|a?B2lQZic`s>X^QFT%{{wTNkxRp-Vrih_2R&~ryZ6nQqFLC zPJDklv3d5s*G22xLf5XzHr;&w#M|`SJEumOKfk8u6tYwQA$vuV-oBdIpSK-c`+v?} zB?cXF%URq!7nZ#hp2%UY|8eHNlx<>`>jl4*miyMc^Z5Ts>~A{DtMd|}WmdiO_ue=? z|M26FKb4a?!gu>tcd%GiuYI}v_MxckwN>xT>blO_%f8wy@O%DZzwNz$r^~;oyLj0C zbZLFt;^OcAX6N78i=LUEP;vhITU#fN)qBd>?$#TY{X4m_R%*}x%F_p?$A8_NUbk)A z&&7@3=0`ExSvEdjba3JO-}`naY@BiJr7DDi4U86x9+aANb&*)K;SNpn zjwvf=u`&3qZ9C}7n{-UW^@{YHg2+uP@7}eyk@NS~Gi+(Qqr7P0`{k8v3tlaq7S#J{ z&+K}c-KlYFO{rUveMPt?!5 zH?$g5hkoAkLeRVV<(zdZZf9CbrV3biOnMM_CS-wAr9MHlSsX+u?<)bUW^;=_|5r;CXaCFxGT>*wJ<8 zMZZ3~p)PSi#qEsFS>8iNJkHH0uSPAetlXFXe{z@ALC5(=3|fBH-2cxxtM5j1p1K1#OKhh3R_#c^%HyPFb~Uvms292PKmIG0Jzk-YgzbY1?Br*rq(KiqIl zlEGEG;rgH17oV~3>6T%rS+zp=gU!#&uWl+S=q5NB7-hKz2%Py`{7bgo<*u5=@xvcK z)a~=GUdE6ixN!S&`3}Q3o(~l~xE4-%vnq9J#^)7WIonRgzM3QIA(8i0tckOqUn2kM z>YkS^iyQcU6)5Yc&taLkIe&y%!BViag9|hwM_3h7pN#yxQe!P#RSM!Eq~9tXL|VUvjsN2flTGUejmTz zed6D$e#Loy#R7h_6P9hSs6BGLB`@{yUzhz;OU+#>vKTYIELavW(fL;2OV)cFLJu@u z%B7cAZoj*wS3+=7OXSj3`)<|VefdnmUhIs(tE$2#vH9-r%eUOPQqb8}B{%JD>GVV-X8eSZu+^ICs&UOs|T#^VbS?a~Cl={+ecW^O4$OpNa{g z>V4mTF&`H5Gnk@M5Vdv2W_CF$=O?njcTa_TBNQaeHI;)|KvSG_&bXECQeb;<;u(n%X@x0HSQ)V8lG`+o1! zb5Fx67waBAt8(Rxgi=#Y-(qGdmLDO3$9Gz>G9FA`-Fo@+Tkl2|#emH#P3(gec6h9Q zk{$Q!+g(wIjWccaUX-zG8*Xp*Du{k;FCsOiVdvC34+EAS;|h{#f$8uCC_C7j2z;bzkLw{d+y>{hT>#s^+FDZ*CY&ojV)$kIyS&mZ zR!tomhk7lVnAco6{@y|4+kKDk^B@2If3B~7frW%_Uh4M$;)-$i_y5L7wK4_e=sO8LzuPl=e1cc@lutsW~ z5mkEIlz(@2khiH&gG9k3vnz4I?gETluUzU_g#?t@x>&qy=ESRcrih%oP?6Ug_4wM0 zrX!L4O^V9bzdr3?`2KbAYF>$1nttJt5n*$xvvaKZ?`@jaD(0p<9RjUUB86WA z{T!RU;x;E_yY1bW-7>`^s@^)n@ln;e74lyhUg*WC3zwT~%YXWs~(2{G+_=?{lS}>9apSzx_{bhW*bcM{m#h($Bs@f7jo1 z{|ZUN8or9(o<+0g&Ai(ACsQ^$YvokCx9aMPPBT0`eX4A2eT@6R$4u4L-+R9piT`+` zI_GE2|Bf3!)psvAFT`%o`SAA7^GA36fA~)E*bke98zg_-E&J@c;$Cpk!M(39tq$LA z*fL*+(|U%!c4@;|5wodM5-ZKib(i|3vUxo;d|oN(FW-N9Nzk;S{~siN9*CW}Y|GsH z`dy39JWEI_UcJ@m^G6do|K!!jt$Twtx3*eD{oDV3S7l_@Re>XSy$!c%op!uz5vJqK z*SM`#FY5WD;+6f}Q=4wA6jKvoYrJUOtjYT9fZ?~q&68PYhgU-Nx(I5%K+8TXVSBa#q~#SfUd9#ZU_17n`JpS{&bp5EL#f1SnqwEhbSp8Q<2=;Xo_IlUiMhqHdN&1LrZ zCi!}^ZT;-~cHaeUto`zxZ@Miojo4UZb@piS{iSEOt?uVH-Q8hU<9hD&GWiQVi@$OH z+dGR*#5A7$n8wwvMce-EWVYLI+WY^jKVr3~?%BV-RB3Bu`QUHM;kC8KbKJM2$6Ftc zimzl{Wq)|X_Wk=}>+Ua4Y~OFAED<~7`|ZE^{q^PB_x$I+$)Uk(IAhO>`13Q~e~jHw zk|y)Rzw2Jq7nh2s-{Oy4Ja7C;{1<~mmy^TRB%b!V-TVKF?zMSv_-|3p`L!4LX78;v z&9PFm@_qWETv>LK>qYP72Y(%W@xWMWZYz_k>&-8RgF>hCoT!MHp7fMYS?=ARg|8oP zP|V!mZr-lB@mJ24_Zy=ptbX3|cV3)@$T{VA;yyYC#)0=wudc4G@0S<;wRcZaL16YB zyBm}JcICCtXWgLhSSYN+pI^Uag)iTrE(_{6yXx6zqZ7sQ3`KYkdu+CgiN z`w0y#(FEPECVsWkTbioQp18b5&*)L&7xvaV+nVb4^2aJOyQ=mt&DwQcn(hAMSb@N+ z9SH@|Tdyrop8W0hpM2$;DZzRv7*Z)MH!NVN> z&+nu;W)?6$y;SbJGJM*l8YxbJse<2cnqG)pZEwEbi(!hG3~%XTeVL;R8I2w#HqRD3 z8X&5*K(hE+qyl4#oWh2zS8Tbr*-}@`ovowSRn_|Lqv&z%t>2e%H?Np@C6R6Q1>Rq? ze}o=0Dv@2aGWwNS(ABGqDf*?3camnsX8Y|`co7$W=g4jOH~hpZ z)XA4@C&0+Ow9XL;8L6nU{4?703)Kq*k0sX2W6 zsdw*GqZPMj6%?C_rp=gdx9{w6|6dhJyLMLoobP$#$saj^CxQw;f5o?k*XKSBd4 zIhbS>W!Ls^Y4zF1`~_1>)?Q{;n(+0VSetm_k5g+8bbZlx`o75T{hg^6g58;A;nTDe z5^qi@*>y*lb(+X8aT&9pALZ}(KH?LJm=v0x^k~6X4&5kqzP-=b%ai@yv2|3l9^AU) zy3y_>wh>!wq9mPfwLes~h^jM*b*fLE_WI%XO_h@WKmA`I|Ih9IKYoqP?faha$NvBI zboT$#e}Dh4UfaI;*Yd}|>|Hj}i zXXC+T8Jc2U|3>>tWmBiKxSi^f9cqe)QQ*O=<3l6{jTl)RNsTbM%`I1_e zp5t`Mo$D&FWYy%tx+QZAI+|p<{a)U=ouxbB{aJ~mgRBnA3S(ylc85JY<;fItH{q(d za)qI2$0fe&hg_Hf=NdVQ=-)b4$Mt>-caWRx_8UwQ1zGB8hkpn5Gcbx~C#G{rxY@+C zomid4D{yVLr!k+2#S-Q3yPAu6PwA}EIwgHarttlN1BX*4cjU0eawW{V*2>F}Q&N9q z@4}c#AJ}qd-rC08eaoOWjX^tqgY?%2FZ4X7PyMy7ihoal+K$&dk3If#-?a0tnr_Fk z7tRwD;slv2|18&z`#)*Y%{QC@Z2TWMj?dIcIlpb$eCf@1Yj?{A#TSQ?d(W7^QWNV&%58-ryFhKj(WfR_PzPRSN<WIqSN8Hm-&dA}bdT-{xm`%I*rQ8OeWnUAI-CeQQ z?o9uWx$~y*Uy@C()#+-rnJ`7@~%X?cD!7867T zJ8!GZPH?Q)ack$zZQ`MKRbNI_?s#bBHP?38K5l3AEfH%pcSt?)I6NmZ>w4=xe-VRj zZKtA_+20n0sl2WFefn+PY^9DVhi&e7ygGSy>aUKQE~1^6S19UlDTv>tSNPIWP@U88 zsLSnI&f80+&bBXoagZt6$MBn>=L_$d(^HS^^h;cuwYuu#Y=H~a^LwwBM!nkd#9y>j zElRbdY3|O}daS{y$T%_KSUPAJ8GVL`nkvIocx9e;h&uOts6V9Yxy;BUOCw&5XK-I82z~Gf-&PAzsDv&e>m24$TThO zp7OWhJAXmqo=x!@fB5pl{WW4{oH*d4!n^o4tLtCSuKZ*p<^$z_64R&XY@7c*)~GyG zm2Y!z|Bne4ovq7PIXsq)6FcLR^E~|GDq9J|6Bia~yfs$V5j4EX=5x^Cp~*)^wWcK( zlB#~rSX=jP+40AohxgYA%vyBs7NgA8!)^i#wsgk6TL16o>Yu9*uB!bV|L5Ji4I*kq z742I--c57RpLS>8&c9Zr`zvc~o<`jYdH?F@wKILoGpCt+e3<+1?&}}gE>-RK-~Ea9 zJRW??D(eb|VsN|x`@{v(49limc>ZIDOl-E0o2%_RJEy}YXLlwXd?>e^`(0FQ|Mkho zRykbb3T*AS2)#OYpT$YXDdwg84ku$~%5Y5=(CexS4sca^(50os%e{C(>eKfpc|q&eWe;!Td|? zAMcLIr{iAU481dLvq%3AhJ<6xQ?YB^e(@9_1)tyAJ)~0|5`ZRWPVoUsSW)i zDRx)#=Fgh2#I>|lZjSe!Bi}159~mTCdEXH~vyW|w_3sBK%s-uNDePXmV~53BcNK0v z&W~d2?H(O(_%d^m-wNAz4F!s7VGr0$7PHjE8lF4QlAL`@_(J^G`n!v@mooSXaU`v= z;1W7kdF#u@Z9*n&9t!sooewa+XA*K0*?0JCOLFt)m=*82*mpRsy;;GykRd_YJojnU zk>6L{3+Bybm?!Y=?PK0&juE1I9ugv#1Jx%=Tx=Gw$r9AO|9)NE{AocC9X(8xBD_v) z-j%f7KW)V-#x)m?9Qsu>jlJ3ak>YDjk1XQ_$Di^saVcr>@HMWQzguX&NZ~@`mH>uH z%e_P*ZwuH(n%FoB>tE%}PG4n`aPa3cfw^I`w#EFq>8Kqr>CRoH&Ycfpx=-;ibDUEM zP$2e?BDJ)o$|k|9gGA{(spM-jxal zpAKHO`}fO!`NLImb=jqEo7Hy~?|7B>uT!tQwCa9G9^dqTVowzBCj2^3$egtK%%x(9 zJ6HF3+zGt#;?u+jxrg;Xx=PkN*dBCLR8Wc7XjOD8+VW$l0wTvVceMr`w$yP{`ub}qghzf1qiNBIEt zmVHlt3zX~M|0Dn2?oW7SU3uZFXTQr!KFDATY%6N{?S1or(^VT+nHP6}C@i4Y9 z9o$+vVY}QBfvl^~q_fiYoNCDA-ne=#XTKL~w!VUcP@_X}?%UK5iIV}n(fjhhs-*g4 zTxZ*I)t{@=(gnnT|aQqffchWrsf8&)Y)?G zn?h{Us!0nzE*81Dw8X~8s?UN~LMX|N@yWhBt{sP>Sz;WNH!UhvKXfi@|3b@u;ZJ)T zSuB1v7(CjMQ1nU8aDlOsnA_G_rll*EH0QX=oenYe zRE1=y8@_fDXx&uMd_Gm@qt2(z9V_R_HeSgSJ1<(gV#9$2fxjKMWob2?dgXidA7)^PX;Iv|vGr&HrbbzZe&kw(iI^Km2t5^B}+FFPCS(e4AEbA+vwQ(_O#k z?+pL4`RY8q2>J4~YuzG(Pu}qU-#qX0=Eph5)Dqvd%cZY<=70Rh-DIBg-skGsjK3du zV?OU#sFd`r)#qR(&&SEh75kR6evp*m3)uRc?c4n3)qg)4e|>fIZ0j-;zsv3X-_N&m z$6M&S7`B}E5uR_zRQE3TSIX(X#{G#(zZXyZ!5#9Rn_0XqbdU8j{^RbQVP75Y259$I z2C_SS^;>Ou=J`^i{tE08 z??23r^G@59a4xYaG*ZogyZlgN#R{90a|M#cGrf8{EnBiU?g`CvYkBTdS6o~!7`8QN z)v8sm_UQPCwAhDUIXl~U!df?-h~GP6A1n<~s`+RX9Ng;uKy+W)V#&|df9~;0-?{T( z^)tif#ZIj|I18Qr2;JXueo5V~6XyL6Tm9YpkGxqurMA*}qQFrXN6DS;^)mz71b+QB z@Ku-H%CNn{IHBN1_3w9cepFqWd42De8LPC;EZFz$nEBPOll$!jj~cK0aM}1mh{azM zfxhDYU0cNYHo3f%{>%O7`TAQj0i}X-pQ)DaHE`!EPPMmY(qo?^!1^g?)54rd0*tG| z+-$$hF1h>up4^eEQYp`8YP>!FLdv(x#J!QRgm_8V! zeQsslUj6Ii#Pu`F3ipTVrS1JLed~)M->uJL)_Z&=^wjk8OTW(Buy!5KoSx+ue=(>S ze60ST7aY&t6+5dw`rkB_F0=Lpr7xH-yZvfNq2VcR~Q`8Q(i!>`=#+OufNR_BDhSC~8kjh;#G z@p^H)E9ar%`P8*BEpZQT743cgeSP`;GiGW*iWMSF;m$L+?w_=D&Mk>-UFWvBf(-uc zf~ssSfydqNh)&s;bJQle@JI7L`3;hWcHY;MpGqBmwR2&G?CDI88#0D#H~3x=EY@F{ zBKlJFN!~Ki34FhI|Jf$Yn7nx@TN1+#1^LtO_wJfG zGg>vd{b1pb4~J{4YLs|$>x=(Bx<9LL8MkF+-ICS$hxPvjbM5t&KREY`wJy8B@w3;{ zmg;;jaHyJV-||-9BJY7;;e(RXVQbIHINhn!zME_L_rW?5voLji<>lIEzbi#gI#;st zsmB9@8dkBh?%TPxZ>W6x;aT{0BPKqf2*th)OnwTRcFmZ&<@eI7EA`}6-o367|26&2 z{x3iG?iS)N-8Z?S^}@}f0)em971xR$ZDB4CQx~Y&$0q1LBU2Lt2 z3uhbHJzCG=i?*!}n{v(cUdp$ys0S;aFrHXzXHa?T;N2d*9BzD$(Fs$oMAX~`#h!`g9+K2XE2ORMc_{LBd=0Y2I#X>%u~u>HwN z_;_{s0oh+Ij2BNQsd-Nb2;iB@an!VFb>+ERyq*oOw6ZxJigsvAp4t@?z!0?H`VHw1 z9p9U_ykKeJ-FCLE;q%`QLG$xg^^`~|-*;YrfHUP)YV8ND<@!eYRZJIt)~%d;>5W>@ zV=jKN%xAYxi|H*@iC-1H+vTSI*4)?2xYwi~ojqT4?N7_svF7(6FTJP6 z;v04veq7GK`uzN#!qZzS1LcnkrM5&*@#U0`HF}^VvPk4J$F6r4e9=x{vyPe`efR5W z_Qx~o&m~f;FF&t-mT}vbL9J7hb=izb6GhV7^mlGCICSBA?DQlK%@2B!CnfpT?R55g zq_lfobz;`mkoB>p_MR&W-4lD%G;^D#tLCh4+I%&*@JivPyhxoXCqwv)YtL*ESs-%x z)Ul0z>(pEJd0luDyw0HD{Cm025?uzmvd!6Ueu_sn1Xh@7hc;h}6b~tVHp$dX-z?hj zN>ac$M~wJYrVtDgihuuKnHwQBBq_LgJS>S|_628MSm zJ~p12%cyLy^6KTs63Xu@E@uh74R)1vJ2K)E&b z($~`bh;V1e;*{O98gHJR^7W^U*3uhpqRTIDi3{xX!Pf0=NXi0zVCEBWuT+}Ca2a{E_>$h~E(64en1 ze5Krzc*N&ez}8o%Rr;qI&$Ka?Vz{tD%+`dxso*ioh8Q;Cw+}cQ!{S}5Lz%u!w&n6z zGWmq(hr7u?-f6ffwsAhwbw29TFyG<+)xWQE>n3(Ji9ffIPdJ_AY7=r`;{Lmj)BbOg z-#2%8>dDqZ+hyWnWjx$U55C?${o#6T?TvHwt3|AjmGl4lUc1Y$XVI%0iN+1AaiYIx zUvJP{6!iXr?7B($zwDj`+1-)-mRM|lHbG(W`^T%V>))GS_vh{(e$Nd*WOA0z5Sv%N zCEicHmFXi_c*hA7xi1?YCN{s0`Mdq;gxXZT|GZO6RDTBUUc1ATEAa8N55GUyvUV(( zo$qeD#s3N0?f|m~$~>9MK|k-^%ddP?yVFtM~3%Gt0ME`Tm=4F<n3J_y<~mG$PU$#CWUdRCkMo=oK{!d{@(LiOv_<~J5CQ( zav$n&#p<#gDNwp}jlH5Sbb8X)V^=B`{YnfR)?-<2v9&hj78{IdSel~XTvdF(24xOA`d;?I4mOMCs| z(j!uD%?_>oV|!dTX01;`*>|4Ht z_ptQ#S7BRaBH14Gy;^cww*K$@mXGG&9@oDX(pD?nTl*DU-!8vwA2V&{>ATi-KYcv} z8LPn6d-{AQrsK~(hiNet9xHaS3A<&pQXrJGMss_BL`$Hf)02d1qsb|1 zog97)JB}*|$?|yyFfU|$xMkZf{)GzNeGP_*N3I+>9p%+Mt$>H+6Vn83u7$Oiw>yOJ z$I7%i2yP50T(D}A$CM1e0_G)EtdUnsOI39yMyz~xb?^SXZQ(X7`K#rGcbqlVIu&KC z>8Q9S-L8MpPATc&y>tI4?0d0jMQGQwJ%@FbL(YHy$;=|exl{$wPtYHh~ zQZ%e)PvM*0f&+NP5Kf~|q6m9J<&R3_a zMHVcVikf!tdI_7M%EQMGbFY7QWYH4!zJ0Sz;rXq6F2`BLM#m~v3jbHx`^P6GYw_uA z?{>K?i#Dj3^=#`j&$VmM&Hue6dS%6LOToE*!99sDcAua0#D40&tACE3{ra`;`nPZ1 zdfs~C`hTxpJs0v(c=G4Z>DBY(_U9DV=ehqn%J;#3QsDoa^1JHQSpI!E*nj-8YnI)- zW50hNK700G_vT6SyUwg#{buK%eYg3}r@r;B`*-PeUYgJo|F*08=kHrw7Pp++`AaW% z`|O&$`2WH?zbv20*SEZI)^z=ItS>6}-@PeYz`4bKa{Aqn<&}}&v){!ZX^gMav;X|9 z^TO-bieL2w*|fXfo?dPD?yNJl-Dm`8EC{rs$JUZfa(bv#TnpR@YlG8aqxMpgtc$yL?aC*_zp5*J<-QTtD zBx;;m%CI6(d1q+TMiJ&!hS&1s)Cx_F*RB$nac$KD-Gc{wbaJcQp2cOG&R8$npT6|U zj=jpOS0ygXTI;kvQaVgT<)X#esMi)}S6Mw3>bly*%GMl{m>HIm+m-#WqNu_bNwYpC7wdPkvTd5{Cc_zK5+D2b^V*y%r84T}>_0n{8yXCLBvky8 z5VSFxw|UL-OwObzJIzPe(k7d6-0+{W)HF0^&z&+mC+5Slm-iT)>iK9}_nj#|zwqgl=c>soFP_%o zzj)N~g5J*43ZdsqU%U!1?v52X5xSS}n&p-ms$b%d@Bd>_|NZWta_O5LHtBb_Z{B!s zZg*h+<%aJY;zjoeFh8E_d^&ye_RzrX^S`a=HW1$FtWYewe|7zT-TzWkgT>eFsz$o;#w%VlPq5SZAn!RHrTk-&dfXxF{> z&!*R%I+D4U*N^q9@{IO{*8H0z4b$~^#z-bSNIbrOQtD~fXE9xzY)>TxJZG=#SdjGj z*MJ;aXEVioRhf3os*uCs>>THN5*uVbcQ`j#U1+`W@YS~cY`ev^bHX=C zf0a;r*U#@=yZ`0yrPF#{uQFY7w(}`%Dtx)=e87y^LW>xtuASlLP%-<0;=EPUsvXvb zJ>b8P@4SQeA@i&EJKa1CQaLu;+}K@wm^pVtrTyO7rA?2t-Zp(Wet6*=#RozWt8|jZ z8hH%UrTu?CTb6z#@_0juzP-caruO*O`dv37uH+XMw+E{3Ui5bF>J^|K7rq(fa<0*a6Re_C5C>S{2-{ zIe7KT`H4mL-O*o{V`eH_>iTz)Z?(7^MNN>_b$DA6SH)Z^r}NAALiwLe6TL->QxE0qN3%;+xbs_ ze5fAvYyZlx7xy}eXDl=SpI!GcJWg(J8W*!eocbfbUD}(Ip0b>lDSx0)nEv(2x>@oL zXYOvjdh(QK+x4u;&m|N5s$<_d`Y7;8zgib7e)*;;vsBylccIVuANXC8b9u*=)Np6h zOQpou8!p7}4xQBN_kOKi+qmvQTYj@8)ry{#td;6=@ zM_Zm>*sZ;>;_Q)$m*bxEm+qXrLsh%%@6_x2mwm5iE@Te)`|Rod*4rlk-hEQFs{fPt z`%`q+qGt_V!bcXmJUVLBawd1ezR7meCm;6NVYSeViO+w|?AO0IpEAncWX$pXAaW$} zfD*gK-08vx_pVtzYgy>2Q^`4RiWE+tGCqH1g~s%C3;eddQ;jzMz|w9##Yg1biMM_W z*@RYHcXE|+I&rQ)<$7+x1H;376*rQYSu_ldS2aCwy{gEd{?dS9MlH`2r42uLm;@wD zlN21hIT>V>dzmJFcFMfCG(YW+aiQ#oH)_b=f6FrXP07JU1Jkl zckk8Qe`na%i&@s~du>+TmZkhF$@pih-}gWIifg$3On&!$a@tq>q}uS9<{`SZW}%=($9V|>_`UJ+=TnQP|!yFU7VjCG@7 z%f9zghyUy>XE^`s`TY6qU*nH{c=-6QiLLCajy*MnpJM{ejT8IQx&A(_@w@!xj%mS9 z*FVNzi{k(MoV+NHbjyXM;8m*O%pyzC z=A9dq938^$hMdgWFsWN_#oWq`F|k!T7eYzRuMN6Sm1* zz4oAQji8RvW_P;-J<8^aw-Qe7IwH$s8($taCv)53Rr}Vh+G?_TbN8giL#gMz-+h(4 z-@I|-Yrfjow$C=Fraq0Eb6J0I;ptZ`IB( z`4k#xuYFj1Qte&AO)I5i61RW4D)rtlIO3Sn1GZNRN{MFMyH#ICy2T179XGF2JTO=L zc=UcVVZn$4N)sJ&6E~FYc^Ou*Q0?vn6T6zlLH1kwir0qS3O4?nXTGCZejn@MHAXjX zh_EVg&YoKO|FdLi0mG!3*^7@!UeP!&Jn@4n@6m?B->)|MuXwNGcx}CKTANBk=GMf> z*D)-jx(q=+X;NKBcF0ZI$Y*Ava8|aSe{%eT^&rz;;ZiQy<0hfnzI+w1c` z*-Mnisk~;6iB{S7_xsUq>89AJ@1L*8+IFSOXxYQ?lG@9syG4SwJ$;rn-|5$f0`~u^ zGKWm`H-CK^x`%Vu&Ne=igN5?<-+0zUUU{3mw|eHjq@<&tF6N8fFVop?{pTA0L*MVS z+f(OEe>ywdy!~+UwiTxmPw_ZKGx`lxqzeoj&nO5zASZmE82-y!|`k9PNeI^4TYCuqvY7wvMtdp8&u zKH|Hc5ORip2RFymS8nSThAw$MLCD=)MUpMY-(teCt0pT}`>hrT+!5k*ST|8jqh@_# zo0}tx2j?T-WxF5dGo5Z&yDRSd@7+(oeEaZQ@lENqHwjKNKm7gl_@C>38Lzb}oU!h8 z+f;Kl!uzVo*{K&WET~;h_Dx_AK zZu`b&^>Cu2ZJF$rSz?z9=Uvh9YEjbfTPIiW;_!y2ON_1_lVeIxneHW^QC4J{yytlP zg{TP(8L3CM%@>?nmM`&iNtD~a2DZ}h1y?s{gj^7szi9QRrP>jRrCTP?dB0+pT21&0 z+nH7_VY>WlXKz?AL*3?TWI%S~M+O0p2ZAn}q?S$Hd-l-E$Y-0~mz@&Gb&J)ji@Q;^ zHf#=4PHErH$(D?M1!tBYuK4kzCh~HA9cUc-%BL3rQzY~Lto|>1|BqXL{e`EFyTpCo zs9bs5*>reKp9uT2y_H*c2|OshXw=BuU{&uCzW?FFrPZx7zpP(llTp{8!pH33a`4s3 zt!ud7RV1gH%#GnLE-`;}V3XIUySjlq_G9WEVR9lud!z4ux9h+Px%4jg>d`Bj-u}CLepm$*nPt0nC9OJo^dv)KsqAY{O>RSG z!wWIp#jy@a3VnS=H~sZa9Nge2zcXns%M#J$md9FBo*Z0!NN>$-*KRH$=iBQ2vzgAH ztyRm2uiTk*3M^SBkDrc33q%BisI$amWp;O&lwptaU%nDQ4TaAY|3j zsI8VFeZdnV=IRvWXzg{7czt)n;k<*M6VG>jv6>sdfq^}|JjC(U2Fb!5I?rx38=ENj zeRwAmEWUw(?QVjw>9nYAvl-h8EzGVjXI3h3UHd}mibLeOZNGQUG20>gG|gEQ8`r)w ztSh^Hy6*g4Z@u|vrRKe_-Y<|HdN;>BwrlGv|Lv{QkK1$}v){Py^K_{_Khv}J`hM+y z^D6w@wVU&D>sOtAyE%UOttn^h9ryos`SLGh^8S!(Tjk4Rt3Ez@Rx~fV^7iiU^7EgI zta|ly`_`xBUMn?^P0#pM9P~GJwODdRTJDcccmB@&{WmlEpK8KC@ox{$`#n2#p#Iwi ziKgu`?7|9pT%}6ZK2i+JtW?8}IvD?|<-haz)G_NRf!VrY#dGhZ=_+r#yNp}MdE11y zQ@mxTnyW0#(lyP#7!+}6QgQWO?~}1!XVyfmHB;K<#us-yYC@D@|E%3^X)O#*8$#AH zx;d;ny1KAsWmxXvSoIYSoO!Fl8mfYHqe7kwzg{t|)7N!+(<0*@*OWwI&&G^K(J5T4 ztIyf6+8W9)&uLfXc0POJ)Teo+La{wF?e8&a%+X?D)15rC@%ZVTy^34>-!F|Rm|L^c zJmH|~i$v}1_1C{@#$=s*E4(V&Hdu6qLf0?3`U&Yg3%dSr?U%YBc}nz~ZsokfUq*?J zOT*^cZr^n%t&-oP!Pu!-Bks)?@yxyNg#@pzTBYT5=F6WSEDu|l6YA~>i0@gwDDV2; zJ;qaBY1z;HV|&})etMtmweNSQRkzmZe9v2*Y+Q8flHc7X53h}3XMcY2l=xc{!X;br ze`;-P#K8h%ChMLVce`yxFV24T=FOd;?F~q{|^7)@49r7A!F*NHKq-s$I{&Q9nH#oP!@kL zJ~z76z31TF=aq?z9F8rM33&L#>&S7VDIfQV&Rk>sTjF;eYkt}7lxL|cW4*SndOkBK z<-?XaK|5ZhSnBRoy7TV$o|NV(Hh1Uysq!A3@%Lf3^1c5RQR|+tSXg#4#~doOdbMa( z?ft1iof(nSxK)C$?%VhCRe{9sw*{L|&ULlOw#=zDydL!Cw#fgN`ixf?>~5{EU!QO0 zv-|D8I_tXXrTg_%wRin1>6!V_=`8mIrst`WNiv!0A0Dmm5q>aBEdIy;uiy5Id0XG> ztyt~Q{x*BhPuqWa4-(r~2kSLf&JIXq3e@BFeg8~aWcBHnF6DPCrxv#U{rc>-W%vwU z*>#8Vbf*b0{E@n!GV}ds#cWR*=ku78#CyZYk18*Ll72e<2w9{_NZr?jLKiVZLZ+vN7ICc6;j>WfKS9(s7*kqID zlJjll{Ugbh2@e?pA2hVDu9&O){e|(grlSiR_-cw>cf0?cU?5Xye)13ZpU~X}QEK&v z9&#V9dmuN>;r8Z5>n5=5b^2L7hv^63ea=PUyLbNEdhTBFcZctr{StH!6xRH%`n~G< z?ETh%&kD>wYr22Z`g9-Rd)-H5*BLEqD0`#p?%w}v;i`KlUiSzq3q9$m?pVBT{l^&2 z10`Zs^8=k`KM$FwP~`j2=KAKJKh16bzi)M4(6hm6+IdsXwLNpCZb>Q#u}qk;!&%~L zn8BWA=8h|KqH`x1OENjiKHGZr@}^MZT;I=WS;FgppKQNf^{4sjNyfAM9@Q_`-;ovBGtc2k+}VU#Yx}JBKi}{$FaM`t-lhEoFZb9S z;_i6$$>EI1r0J3gTyvkXf6w~as8Y16Y{%73i%$it!Rc)7J9dRy+*y^ge`AfoS=~o@ zPdz5xQM}tbb+NU>#KRAc$gEEZ?zkE2X|?ECX5Y3ug*U$D|LfH&HO(q56fjmz?vEJI0B5T%|*}X z7O?xmispu@HkQuG=azilCjHE0<+Gz(ggrg9*xYnyO=9^X>v7$CQ|mj|h0-_0&bApn zGDx_eeE31nBtypSe>*{ zf2`*!C^${5x<|1jc2~}%R{c$tzkQu8TvuE#EoyF70v2)6|MOW@_iHj3F z$NEO+PSLHjcX>NQ8Uxo_`K?SVUthZN^U5v}q1nq9vC8fcYpFT1=yS!1cg2>4A5Jc1 zE47?zW$k-QS4>TI7t`ezs~#jQ)L!5f?eSvCe&ZI`B>{$p=M1+U+Ve^8s6v=c#P`s( z%PtwRWVM;F?BsITf3To?(*fg0YGs-R<%UNNg!1;i-6k%kl(g)!#azGS&oMdM51$s_ zo*%dWPF~xTpdQ8tA}hRkrcAJ?alOxwGGVQM8|Q~4YuAM0gNie_4r%`pDlj`WLtUm? z+Q^T81)J;}Eu|a66*;mWXJtM;CU7Zc^ObV}*VPJ-H}N=IUB2t}y42Y>J=6BsjVYHd zTWsu@x!v?8>m@aX84QWdwh5bUt+Woj`ub__y7>=I&AV1D-nrvzVCdP8T6vEH)|~m8 zc!(poHTL3H-Mi;b^oGxzJ}I4b=Al~^>sePt$44FeB4%$=ZgFA9LWU1Udwhh7_4tze z^`Bn9807e|^uT>7z4o4k>Pib1ew}@7-?mK)zihkzFv?K7M)mVy{t9n3K9wb1Z`yi% zRxgdOi#nfnUZUz-=G&PEq8*t6?3z!CMn6n!5IOCT^`j|hb?EB2kSh-q^p-N^E_}1r z_q`-7f7BQd=$h>W5*6v-jDh@ALD`{^*zOz4mEt`POPy?d{>e55F{t`Cn~*BV~Od zcf_NGhc_IzS1mYD;LP87y8XgkhDQ|^a_aK?@7-ZKaby4Gd@<=+=1Pg*(*oj`@%;R2 zDEm%+e)|!>`dO3MshuK8)=-blLsd5EhPn5f_ zB#%}O~T!K$D#~-e}!&;o%(-;Jfq7*_I|3?8Tl}l!cl&w! z+yfq}H2CefZWz!vpYLwWU$*t)mZ7=(KD1p8I+!|DZ{4#Ge(VjAoQ5&C%Z^lst4{oO z|Fd1p0uCi@llr4?7nfFCN%wF#{wrU9UgNj=wV#$r`5ftZ@Z9tCE^~FqP;qzF71yWk z%MFVD_`YjSaoK8)4^4~sBsOlB^D1X|zP3&(*nGyy!*{dh-rxVf<>S4*yJ~;V&$M0F zSI++T>3_-H*Xw@Fea&S2NaHTAXL?v<)mrz6r8`5Jv3tJjp7@P67O6( zo${XwZaZ+xT={UC{#1*Dy)JVktX7`;`09F?T$CjA z-U98gzDswTcF6qeYbf}dU2*lJ>~ZbFRVxqAu0Lu2&)>m}sbp8}*Z1~+e$?y!%!~c2 zfBDOY$KPJ{E^c_3_lPm2AlxaTJlu$#vpi(gZb?PX)Z@2W3r^OuxICS(aZ9L&%d81c z-)vbC)XbjnOd!CMS5~QWXKE>D;2uLcAJIsQ5T7It0jKzlzm}eN-nE5^~dW%SDvj`m=;`$ELRI%ks#pC zWAu{K;M0{o+Y2oFpFNo)Uc%^S=iW8>T-UD2vEQEB-48v>6dBU^CLoEku=Y>WrA`0V z9$Yx{`Sv}v7nn|%<_TLh8}3NR+3~}B#XGNW-F`~PCQN=1Wq3%={}$uq*CKMRDQ9-R z(n(UxH(eFwy-Xv5#j3?F$^MneGovNCYuI>?L>TaW<1*AQxe~$ln*Gi8*&@I7Zf<Ar91v`VpzCa>wg&K^t5|8n)@p~T*e7u+WW zI-4mu_`mo4ESfKM;LV;w(6K?f9Ofn z%$w)G&aw6E!=Ie5rL1FbCcFF% z=9D+y`l7VrSSkO*m6@;aZ1uCB98%VnGyI*s5&|KDM>mW_sOPugu#AKi`rlFKfPiw6k|^tYpcP zYNG?QzRqdby!CF_q90kc+tpw1j=K9vFEQHm_LKG+7KWvKhv(&=wb>!E>h0vcFWcH* z{!`w&ciMGMP9FQ|&KBZZSD2;GV=QAnIIHx|*?UXO1C!3N7A&mjp3Zf48mrExLzd@# zT?@Wn&TLBl$WxNAzi1e5skthm8wjK8NQ#6bdR>;QpU&|GSIUJnueFQts+_EqUpph+wv^ z#)m^$ZcPa*&ZVXOUacs;GuU4&|3lHUmo4swjCumrI}e0Nm&WMCo?dJEWqYOE@_ny= z+5IWux*caExN~O9gdm-zVd)bUqW(upZF;!BFv=iR$9zX^UGP2|&&5ebpZDna+}SsE zw(Syri?h4u-OCN0_xWntZuXCxCKwzyvpO(ceJ;zKPYIb10`7a&8HQTV37IleKRG|t z_V)+Xr53)YuU?(ju~qN&N`aD&_D$?3-(J0Xf6w~Bq$8I;7ar+r6kZm@r6G3U^CYvo z3b$*1R3B(%s+N`9xyMS*f8B|K^ZW(#m%IArw~HIh&%F6~R;!%9yX(75gYAj8H}du= z{B)LX?|LrF|DrtdtMB>votqLvF{IK@Vw&b;KvE??)k{%?_OFonK#pfQGWi( ztq=AxPD(GGD*y4{vfZ^as~X>YbP{lv5x3&pFtOI<)C`U(7tTkR-SAeCI3#r=bBRFc zkI+4)o7rtRl8xphte+AYa_*>a=qtA0wsD6hsJz{F*M82v)6>jNdNy+@7=}fy>aU&7 zCtp-9x^y!8Uj|2qmRmaQw)=N%?q8)fiDB)l>Yc2g=kDc|;{Rm#GIXL=+q~^v-&HnA zJ=mDHNB`0M6J3*n6#^z7nyMF8msz;*>--Mo30I1nn;5z`ILx&5u-PuQYPYb6S3?-n zPVTk)XD*WHRNh{#=s5RsSF?J0c~S6=;8N8YyTkJXHPp?Oixg$<|NkJfNGq}W_bdA? zi;N-~t?NF%-*f-Rl`uLpg z^vnEtp}(e2lV95>V=lvX_PfHhKOPMEanW-htorJ5r_o{4$4@IxyRSE9@C#?S`qb}I z-(fk1)_JEgPBSmJJwCU(v_FqEZt3pA)w}MrKQ!$qsbe?iljf41eVG7&5OdRc6alQ2HXevufYV{LW;P$SJNZwyCFi=lOXlxTqZ9UCKLa)3c5% zmj$*krzx-}<*R0u1YVh$VY;6?KtnvGvE&BV=jQoh^A{Rg2eqGSjK3Y04@c{VB_i{}X=t+4$ln)y&Y+$WLGYy<*Do*4!4jFVZL6@@ckk4U4KOGj*hNlbC8~WzTU3c*G0V7_LzXm7MY2wJTo4%TA81`C~kV2``(`S zK^zaCi-f1`lX$s*NA>!pCZRu1cHJnQ9`WPz9@mw8j_y#n`t zcvZJfuy1DXmHAf%Y6I`=Q_q}sA4Ua9hsU|J{9&7gSENek=?bkUa27Q-5@#u$p z?QFR*fAN0%>g&fc|K%|5l>D{sy=tmFqhl!iXkOy&w`sfUFD^gwGvzdg z!iJ)K+#MIEo!eX*zH9F-^?mE){}fn={yCpqHUIGSx&OM~e(Bbh%UgGRb@)s(^QX$6 z*aIBuxr^T1Pxvj$bf9O8|OO@-S!>(m?ZBw zaIVgt%$Uon5~?k{I@0@;!RL=9QALqOanp({WJ0gXZoL-6^&r;gsK6cD*O#U&&6vf& z&R)%7ai}KucZ=pNwHYF-E+j=NFm?uf^i;pOs&j|r;@b~*ZHo$Cp8hT0AjALqSwlPT zw$>Jz0+XvZYI_Y1e65g)p5WF}yLau4=~@?>OoLziYVfx^pOqlcDEUR$RhEzY+S`KA zrJFWi-FDclHSg`KKf3R8d9IblPJH@;pQS5fBICV>vlR5-L_2UTTWotma(PuA)Ah`_ zsGpK^M4sB#z1Vm^vHaX7`_*>}*e6Wa;b3Lm6tf5>&bqC}ba^9LL9h2~U(66>YcHP-89XvICek)h?etn|B6?)~` zOSLn)aaYx5r|FiSk2<|3(#476owmbO*>`)c_6z<}akWiN|HeJ(o?+dvgda7I}X(@``|gK6l5=757;n?aflX|&ekbS8POBkQ`?mHM0dN!a& zOYLQJgP%!X#h#g$*lTN|d3X+85)fH+tMujWr@De)Ys-JSF&gMc&M?31qPVH#mqG0o zK@q03B_9f;*68tX{^FTlTmSFUgZ~gTPFTkpCWW}b+$-nYVQ`X)(DQEIU z{D*|W0`bbgmnkxNZw2z-rs{v1P%L?1U0Cpi`nL6Rb_IX;I&iVcrEXz$=gTO$Q;#3+ zcp@Po@_BZA5X*@~mCogbnwuO1FRC8l4DK%w_OpEb|Htl2TKrSIO7||D{&Q7`70ZTQ z`xaimeReDJXPb|DS9TgH=P*h9ttiefx6OZ%wTElA^Y?=dd-o?L-F_Ky{J_TJZ_ga( z|Jppy!qM;U-6EUN&w~5a0}R`BPJc_`axnIrEc4Ev-NjY#ql0r~pR4}T*V7ma?Uu&f zc_aVKGHkWOMycH$AkyW_kYy? zbm2L`s=~3UnuDj^ws!aXx6z^LXV0AI_;B`2NT|z?1<&N>Ri0b9O7XFxfaJ@EdAqw# zY}ak6xt{j*McmnkzI&eJdA(!Xe7xU2W1mUQq}}2FxdpQC9v0d9OaGttd$#Iddynv1 zI!wGIdc5~#&esnV0pP<@zcGpJqi!s-Fj7!!NT+ZCEeYjT-wt-o+fOA|2s6gk~wDY4*!4l@9zJ~e#-qHKOR)0Va38--xOm{pADS56`DM9l zc^uE>+k~qaup%L-*?u7oCJ2>37a(s*Gp2>PG>UD^AgNT*a!_IBD&dl*^+!Iwg zt62EUv><)o>waQp=T{pu-#imjS@}t)VQFB1`^7b3jTh$3cl~3MoHdPu%ffKWubnP? zzPyWD`>WXB+vUC8o%ye)Ri2C8zTrrI{<>t2=Y{pxU9D3eEs>DwG&?6ay+8;YKgmX1JZ>KD*)nCe2TfQ}Je&KtCBAdRb z(v6`;+Dqk*zbuNncxa3LVw*Yjm;LUm-+ON_|L5D}E8ox8<(=5SLE?A6>AGb%&XiyI??_PnYRY@Zp` zv+(_{%MHrgrPEEnTo0eU@O|Wt*Fp`U_rErZcYm~Umv2AYyW#ca-M^1NF8`NQZt<5Qgr&jeq2_|Xds*%8#8yw=&~>*(@%H5}jEAR$>0kOG9?rq+ zydh~#)v=I*6AGsDxAcTDr>ikCtFANZ%VX0|o%PIev(+6&YnkWAzq`LUeeTMuDXb>4 zX`2_T8qY2|x%^gKsQ1bZL)TWDb_b~mqw?F)IOG$3|(koHf$tw+yp0SJ;W9wP>Q&n2X-OO&o zF42N4YqjMuCZ(shHaQ$&vrU?Autj~ss$Z?X8QB(hW!)O>JKeg^%D>z4G3(R=>ph~= zZXVOU-ySSw_~vH*d-caRlbS1gmBJ$!nZ;Ocyyc(YBP4Bl@=~9@TyA_I%R}aR>52PZ z)L+byW|GYOuJQePmGuYF6Eja-6tB3yheOO|?#!i8X-A~@{XF>1J%=&!bL`@eyCZjZ zajh~vJWp8m!Jj*7&!Vd@@9}O;)|8rg`^sAHl~=eNzA(SwlKFjh1=p=|d&fIXd-q7? zRsH(0xAygCuDklb-x)M3xOs2uI;bc3^>o}^j&T=raX%L@x{hm zbVJevNsZ*0x7DO~l{Ff-XU=Nf`F{7~!kjv@XLf6@O;j!~w3cg=TK4m0(cA#n9vi>U zH{-u-Ie6%d<@9H_+oK+6@jQ$$@%i$qtn}~4hwT!(_4?W`_TSi|T`?<-Gj`?Gv%&J8 zTl8uL6{0K8SmnxD-F=(j=aXmmW$XJS=^b+-WO;bj&3L->?}xvWYg+=PZyO%J`s!@& zX}`F9C;ssKr+qtiz2pBLHgD6yb8N3tzH<4!Ik+HB!>dDT_rC7A5;eKn4)@#kTG^Eq z`c@=L#I-5bA@SM}zj!)@y{TG)S2C=AKi^e^}OX@P*q&XjrY7ran;XBa_ZQ#G`|+*we(8>VNA=b3?Vo)w)O$t6iCd1XAFV%KX+Qo~DCYRF%{e=r z9geU}6_5&|J1l!C`F06@naBr$!{IPq!(zV$-JGVaRem^Dt zPbbF$p8&@TU2Us_PnRuKtvq%6#1s}6gJoVa38zm?dHBlVHP32h?@2Np54p~(uPS0K zuzAz}+rNe}FYeEE#klK#SeRZtiT@)Vy?4X%`#V=Jk#1?U2$b;I{M2?yvgDmr*Dk(h z5$g+AxODjYJGIr@H$)w;){dTZ?$w*PiQAV67plBmBX>9C(W$#9WaIAk?iRmuSExfH z+fs;ohjQ0}7o|qJDz^M9-|aj0!?EqU-=UYsa<+Vw6Vc>j$dr7@7w+*suVC`SwdbNM zCYC(Dva>gDs)t5wv-Mi{>pY$7EYJI=Onv0``JQIMj8M8{Qc3hcl)b;{rF$Qms4GHfB!$`O>h-M^U}h3fWSwQepfDk9s)AAh%UU*(jxrOA5L52?rym^sKS7~S=YV?VTNzQY zi3~4RuIQaG?eUCRYq&kGT)ixFh@)han@G?CCTG{grwSGc-7NYR*PK5WD?2)f#0X|A z+&7tlK#j4PCTPjmS{&wVmehspK@r&hP>8^omezGE2-q^L(TK-Z4z1x zs}oXKHo8vtK4T*rm?aZ--ThwK@twY$Gd#b#3#?mddfP_p;_6u)D~jxuUD-CD*=$tI zaj7PkK_c4dulJJ3)w@@}oKvhB>U)0uPoAYEhcB0z^VA-m>~C0&~**9Vxu6BEi z*KW#H`lR`L?!OgBW&h4h%(r*1{r~2<#G$hC8}In$YbsCEKDGD5UA4Sx8_t>*?!VR* z!tZ#%aAJW(Uh83llElwUhCcqkm%lx3m@RQFoN@n)XSc=6AAaM$#JSaFYj?-LznzWETco@*UT>XqZ)57$T}iEN+s z-0Q2w^}mnbIk3l^eX;)d@BGi-m@Q7c3honu) zn+l6r)zc?&w4dyge7tpU){Bj^nOU0ah0dof4>1;Y*{1SSLr`NKHGjw(B*#8 zTsvD}{o7JU&uMWF!_QaO-kNuU@yU6UJ#5S~=7;>fcF9^Q@BXH*5=m*|!W$y&Ly zP3K=Oxt8%+)seCTyUgSF%o5i$zf-&acE6p_86W0LEpN-q)~x7VzI7{q!-t36tPFXF z8`I4%rQ0`cv*rA&dFY(4-Ex5$jwMEs#s^NG;rP8&J8$!@XLS$W1@6*Nykuy}*~mJ( z;HafSLOMS~{Kd1Xi|T&bKHqd-ez%_KUBTczFYQJ4uQHkucuuv2p<`xuqtew?iMqPh zg?po>M&0^RnRq<)cfpMP+{y9bYg}($)jc{R=ueKFl+%o5J0lC{@c&!HlWAl5=-DFH z&1%>3t*o9^*z{G+TInnr#$6Eo>hy*GaoLma$A8$`;~`aaQatMKu?xFvPEAkhy`vUX zee!ryOqaD9C&L}B8T{?af~+Ce*Q8xGWEOcGy2Rk8-t09ZuP)!5=&*HmnnCgXr9oMR z$>k256|;EDgkS%Ad7@l@e}Jy*$1U16`5SjS@7?atcIIKh6C3{PufP8OH~03JPk)cU zUVT=z|Kgdhsc-pz?|Z$hGH!iX`;H>r6Y>2I_#O0&pFC}^Vtwxu+`_TvkbOwz%cFc@ z+l=ZK>s^q!9)B=i^ZN9=&QYbUVLav$uMXYktDjx7f7ZLiJ+CiaeYMK^+1)=u798zK z`E#D$s8xL6z}cX5=jdXY`U(p%_hYZLkA%MX-|A<&c2_^Y{QtY=?}HYIh6#TwKYVxU z{i{pXU;1>T^3dOg-Ba?)E7%LR7D>N2 z`s+8V?0)!_Pq8a$ocHB#q0Z4$XJ%E;?PI+ldu&Z)Tj7hNQzSHIy#E?^WB#(m)7yfy zbvIc4QPKRiY;8N!^8Zm)&xIH9?Z~)%&Ai$&Jk)b~eNj|!&(yn~UuV8wb1%SL!{mpt z@U1JdegWT~it;tB$~I*;+P!1;tXYwz><8~8CanJa_;~sB?5n?C<+q>BKUKq@bgH`g z*7fe2-@LmN%ee0HJ>B3Jo=Jxn&Gl%zu2U4El!!VZt5UFimpa#_{SV&pv8)&NY%eM}EpqM1#l2Osb9*9v)FbN^|H=lg zcx*k1(Qkr_2t&lp;&S1z{M@=NRS)u<6EA$+%PbQ%Yx}(U7I9l8t&Xog8r|1)-Q9NI za*KeN*?ltA(_U2RwC6uOmFT*KYxQ(1`&S>WyTW}Qt^e1x=hEN%)iGE9Y`OO*^`w0N z@A{Z?$-2kiwwIJL@7QQ~P^8iEiKwZ?mF~`+nGVaB9MiS?W;ySySUJm_Phr!&7bv*< z-Q=3V60|z3$5@nM^Q*2UyR@7+B25FCA{H|0OsM1(wyB?RtyRg1E0?#ZMPiP^+x<+d z6B;H@JQ(UZDSMGwLaukcWa7jnhhME|cecxFGZXCI-r@SyAj)--fJ*1FSDOP*yS~2C zZ`{kw=2Rkou_h@Sspm@Pg@z|`IEyW5xbtw?qLL0C zGdHhJg$|SBd!5$*`a1h{IKwrQ>zVee7w`Y4wC|aiYxc5dEU%oE7B=>MPnKQYEV*O< zI+GQJN^AyNp|9qtWz5U93(+`W`PsymWkQJE%e`lNB6&94F@AQlef8OA7NQYr*VSmP zjnd5E;G5AZGbc+hmE+FdwWo7W{n4ygp}=t}X7;q%YXVJu&Q~Re=KirZHvjxDc>0c3 zj}vzulx$wwcwfGc`{6mA=aZ6bNVBHU zy#1;2@8-f1`{I_p-XG^^z3(fW^=}Wu&VrrgS!U;r?QGY6d}R7gPpSBv>U4Q+Yo3D5 z)0_Xie*I~->8{(W51-Xvaz%gHdxf5VY_W&luD|eAz_Gq{ht}@D`~QFc|M30(pBt*a zzM8Jz9)J4s+tt#0Dj3%XG_h<@SmWSqYIT2!ufm7^|6hJTwpbfhdv0my?Ba7#+1}0X zUfFF(eP!W4t;X)zuYxPH_$_)aoA^Hd-T&+R|CjTZJvy}T^4i|rJELay|89T(aK-14 zV{>!7_886OyZr6r`GU!zYd3z5;W=kGQ*rvDtk^EamZ;gX zQ`d(bKD|3yu&(0Hxyv=P?iO?uD(+cw;t1mz_S@;_j(*@WDhLU#kvd=Iv`SHiZ(H@F z;~Z<=Jk$N}=BwOY+qI&(^~}M7if9|>lbt;8hvJ6ly_*7j^X136`wyn zE9O68;0=uTdGhtxy>&bEmqpjDfA)Nap)re}@Hb<=JcEcwDQZW#cqT5DU}>B;HP7$C zmKk%q3=V267I^XG@41z>(Qltj|NmIP@6hJ1KIcPyr-LK@PI}Chw)^kFyEAvDUf+KF zsAtGOPq$eTBHtMAf4vib`*g#JGc0YL|6d$U@3k@v{oM5C4)dmz4RiXghSf3bp7lNA z?g#Jo@7rE(l>GTJ;rFxU!jcA7$3t{@l_D$-NI5#RyxfzMvF^0k#u*0#jKgP`?6!D% zXzqVG5`?m;F7hBIChKuec{k&CmXI`g^NaR&E=kK$# zqf7O#tb9_-ay7=|&9AKFXJ_v0D!KZcb4tgC&mAw>XYcQ*xVm>}u4t zm)Y=JXn)GI^Pi%kwmdGWnqng5Cv7LKaAIlH=6%NR5(Mlj4;(45EJ$ry?7c6)lZ+D2ZpfS!Y+teZ zWY8H==L*$d&9hfuG+}I;xqjp9#v0FQ=R9V{cemKo?zvy@tj4F-WAi7e)#&lkRI`YK zFZewE*VX5L=6|II+v3kme)sv+|IZ6sRHS3>F8dLb zU**}5`9xZxfqCAwqTsMpe|gcLr_A$zoBOr(C7AgZOD(d>I`RFhT)+C%cS^>`lOEL> za@i=pVsUW{yx5jA|KBTq)-+kkrH@iY!U2LCyK@77~*wXs9rE5OB zv&$U&JNNKjxwBeRnU9Lz@k!qiBymU7O8SA!qi5n9X376!7CildNoAR`Mc^*o-S>mv zcv@NPsLr&y`qOaDJppxH>&u+GWB1kx_sLi8`ED^~*WR7K^}YW^e0qGS+Q#P8hDlch zW#0KRt8jF^Su(Telb=SD#D^vwmJ3cZ1r{*LE@2j%^zr2D2X|JpvxJ^DldH{=5uPFQ zdWN6SAs43}F@2@;k)7u|A8|S#eBp4+XTsz+0ckzwvnnSs=$)M~&xJw#CbPos#)bt4 z6#jB$r`nUs$)%WO*Wjdl$UhAe^b8Lw%BBSy;XI8?Wb4E-~PMw z{LR0IAMZTcS65kHUR|3vHP~yq_0}h)TU9?@?)$sB@7?G6IF(+`iGM?~e;rSGKHpD% zmgP73w_9EYpMLVce*eESal5weSQ_@bxY@`Fk3kt~wqc|GV4t!pB|w9&LwHw7QbdDH{~XI6ct`UKkf} z^0UP`=|dBOO6&el&+?C;E6a1V6x-j_o#+?1DOYgPcox*+WvHcsl|DVgFH=a5Zc05xm(DdiW zf*&&kP13cGZ|J+!J#}s$|Fh8U^IHt%zD?WzUH3?2)tm1xl$X8SA%9k74cqL#)$5!L z7Qb$5IkkYnT!1$_lJ~G&P)Lu5N=fXag~`H3uCEqrcAwZ_H~ndBSGw_`%5zmoXZO_p z{bauX*Q-}qlaop+1Qs3>?^6_Djk|E-(xj|}g+Cv2`xR$@4(3lV2wrh}Mx$un(LIRMp2?fDi|65+~K6rsaHa)<*Bs6zQ+2R@45NG_ruzk?VS0>Rfew%`G1JM+g7&z1Z&_~5m$vK1F3noH*^J_oaxV~ zYGOF}C_3AK*}Y-bo+lA+OZde9ES`1mk!wG10{^#Wy?u6E5)XLtGApMiRmX8DTX-2y zh_1-HbgO8-&3eNbw&je^&Lw3U`|sx4usP>V$)~e-e%l>#mCagr-YhWv<&=}G8TVB- zzOGX^^w6THhx$(5*-!N>GrwXU#ihs2KM$~H2=L776=)vPG5*!69nmrn^w=DG7=dfLqMDQ%@X2Ob)9->`||4%kvS=gi#1S6tz1 z8&)m3-EJrU>PM0Hsqb$YzG#}wbz+XVQnc8Z^R>x(rEj~FckcSz)nIe&ccJ{R)!k<_ z86->7raqHdo+{>j|KA6`1MHi(ZR)!m;ngYB9Tv0J?orvieK)7i%~~L^UyhyaV9ZXY z8?$8(9@*=h^XJ+pW&wpgkzX~{&bF8C|9AWTzpf9`Y|>8Qb|tCYCAS;jG2Ju_-^(_m z;^~%|mwGQPyO=%qGWWjo{_pudznC5Gz29v0|KIh$?B`wT3m&L*1fJ(-{PrR<@y`z&R?6B#s=&T6d(Qqf`=H0D&y>|NNIZNaBWS?EVTU*n5 z_Vy_^oC=DzwJn;v=HA^0EHY*C&olREWFLCl{r^@@S@5Ns&hc^oZoQs(*x{v=!uC~{ z-QTV2oZK8?wfW5~&2=6(o-H@BZL;BsZ%DrRPk2ugv(E0_yRB?Dez{QOw3#FEX+z{U zqd*CZyAw*5NzS?(y5T&HQF|L#pN&3k{W5P(gO&XL-}{?_CE|95PiL5L&?45^ z>bhlqz5As|%Z7+HbA?Yu8>*h{C^OF}oU71zXu(&G&DR7DgdXR=vtKFgtbgy5^M%d{ zXZ`Qo7xK2O;Xb>-A@Ravu@zt6i6_>j=<=DbGcW(Yncx1;_UBvkt19Y0|1uY4J$L0C z$IQcyDtFo*p3U-gT&J)w-DNK;Cz0ym2Lyu~F(5z6Tbf50xCkI21YZ+NU=d2?uHHOo^vuPO4GU-5zer~BIm!0NYo@jP${+U9P33!a<;_#$b%y<=buyiICMJnj1hhJ;pJCW@ zW!0J@Q~&mojCJ-~miW({dDG^3P>b(F#pDd*>ZAGfMsD^{9Yo-tU$cJC~Tv+{&16Rj1SC($BkJjwt6=eLXchVe7_hsrWZ6_OH{c zavIi!hkq}f6SF!?Z|lj6x5G8Ju9{V?w>0eR(y* zlpi0tZ@Q-@yXkg`aUH)hZ(6s1^vbM&@5@9!Je$+#Z#3b5irvriFAw~mvuESy`~TK- z{7t{}!2WUL{HU|S*-meT?DHZ@oSBk1&erzYPn|WdVZyHa^5=Tjt%{9bo7^*f%h}`0 z-#)D?^=`YSzWcJ?q|Jg3udnKR&zcf+d*11t$L90zpa1w~-J_%Pqj+w(PXFhA>+9#; zf1hOE2{EM7A2$exK{OBf44RUPys@W6=&NZLh1#k1x;EvENfKA#b|GPei-QDF`<-BE<$22sRE{?&O2GX162wys$>64!lR^I4`)yk3lxLs@uW}LPEP@a^oP<7$jcCIMa z&Jhpiu|BU_-`Dbaa$*nnw-bll(tE2?-n=b5-)|tQw)NKmdy~DpqF9@Idvlv#Pt9#P z+`PB`L3z@lvtR!@EsndnzOXp;`Z>14)8sexK45AtNGe|R;|-(VADx98Fa2o~a^JYc z=Dx(uw5)wu&yJiv?62o($->BSf4?vH@3PeYS6#bTt1OkC z(wb=`Afb?E$a%oEVa0;fq|E{n0(r>_98Wl-+13?a{^LEHIbHs8%ggA$&yM|{))}h1 zbVkygXO=8>x6W4RDE5N*t({*SD|5|%Cntw z=0A46Uwg@tSs?Z5r9ILbCVE~QY(IG%e*N#62Y;;P5r2_X7ybM=AK`xwAD%q%?g!VG zaNAwwzwVyC+AaRG+cBTzkkfyVRxH&|axhFT+7J_CVDapq z#oasW-rvjJ$N&3o6z?0&qjOFhd`W-Ypn2ZF+GnTKqE)NjpDx&3SN*>vMwREr`;U{> zuG+ndG3E-}rW4IwPEU7de!i>I8+PKUrFz*Gjylecx<6&{C0jJSJ}K#xGrPab_t-q$wjt8-qVJ`{S}KadF?;147&b;bN-U{9SeR>7ZQC&U%X*Kd z>ldH7H!Rt|_Hp&QI8OG}uE(oi#ut=x&+WMJTIO25=9kp(YoFSBDmA^^Us%zzKIF*0 zKiB_!-Os%7nzP6D@a_MzzW=yCKW62r#P5o6cQ;5jim>P%dh*0LwNSu8Y{G(7ZjO61 z_piKY)-`|0Nu|x1#>OHZ+XQu%TrF1PC~Q$)a>~!^pWX`0c*Y0I-1u%4lEVBP;(`@x4PHi zY?iWzSJsb?%6YF%6M5Ha{1geh^=R>~9_@;U6?0#6NYCWVdFvTrvZ|<~B#py7@$K@G z&0%4cha%Rt^C)*LxMGp9iq~~Z)czN{`Fqyp3CHO;`piF-Cmp@}?)s|%ukU#E%LR3vXT`@YoQD@>?)@BbfP>MOtVnuvGQaAXzL>{)+I zri|z9ue;aS+$PMpveIhNi4A@7_I5S<>mPk|+_^bXT9-K_D&?s`#>%DF^p?&@(p{>l zmbcE_u%-QY?%JuYC)T`9dY$deBQs--!WTxvZ_@qB-}iQ%nRc3F zfs5s}D2}PemhX%1_ATAN^`X??&=0o{#<@(XocHhYy50IOOSS5Yx8BS#6Ka>*J@L{i zKK-|Od*uo~uR1q5e(#gj)0KLr2vju$o<83G@%8<`m+fo*8{GaHU-R?n`Zn?H|0@1I z(~aBfA`-#Mex)ml(Y>BIrnbziec$}zegA(bEZS}!zJ2?3d%MTS|Gs!%&%u2zCs${$ z!uIJMyJqW67Im0?VXNnbjYsC)>^FIJ%g5fY@V+RQz2my{{8i^g1rLc&;}JLCc{YqA zXu8eG^sQ#fY{INpmU++hJQ(83=_pVmc;k0}>b-^sO9JzwOLNQHcK3hNeX#!RbiN6b zFCDsTWNyLI*fZPBR%zb`eW|vS6BfUf{}{XB%S^Ywy#Ip)&c5xj)+*WL>DUu=C3))s zw$*jtPPUlv-16l%V#wWHzw_e-|CNl-H@@!w{^iW1tc)Vo7zIIp+nC-IO$(l5vSEEm zVdtIx2dA~_87DXB$*`Y^J@?ti<){On#GStdGCxyZr(Y|Y`%wHI>vGORi_V_Xxc}IG z^2RwuSE6N|5}3rFG_td~%|4>4v|`mRqaDuYPtOxOFr&#-QR{?uO034!j{<()O=snn zq)P7Cw0z%tm&)DN85elX-qu-o@0sVB?OnYs;Ih8ow4)r0(q{)J?s>6W_K(5yQ1x8L z#AS~yYjBkLBiEe9*B? zdE-82BLj`R@Go0@r304yYI(D1-`i`G7q8xI@}s%a>#E&R@$0kmw+XYHtB{QAD?L@T z=0eLoeU~I&W6iBy9zBZB7cJ}IdKEU&?p^mT@8?1@m|R;V4)41r^7tkP!-2B$AI^8! zPw3t~I_HwsQah8l^ObY0pD)&t*UfI7^iHhuvu993RCMZvxce44s^>57+5M?Xcvt=N z@6t!2_Zi*b=2vm5kd?o0o@ysDtMHh#sK)8|h|Gdy~yr6L+yEBV&&&QuMZ zL*?NP0xZk;%uBO0)%a4IXU{e_U9!4)wfW@~!(!ku3onJx~-4q8jdAD8B3qNoA&6~>5X~y&s|hj+6I-> zF->gYUDM!jCon>|JdCd)SfXQx;Kf+=h@}P%)@h8L*^SyurS*6J`ZC`#uJC))lFcs} z1vbz0@UUW^(-LtfO<$Pf$i&~)O4lmAjRaV=*)MfonmToHG+Re%!R)nf6jwIB&6}CR z!0NYm>U)bdG2QO%pn=#;tqLm+XGHU_;PqL1j&+zmHy(J-T=BQSpXcQ5EhJ z4?K?Lt4rM~`fnk(z1Y>(Ow>$pYS8SJR~22@A}g6XWLtt(+0UNJ9Jo%suX0vUVaL|Y zWxMC>?sGV1*Jyrh;f*hm9pSH|j@o8(UfSVwMJ4w6oi}Nw{5uw_7M)hTmX+3UxNraY zgP&ZAheKC92 zT|Kkq)8z$>AR zL;cYOL+R!Zk9PgP|7Uu(hJV!aozJ4G7R2*+i5z|CD$x>f_W$+&M^^`FFZNdAKYsU5 zD7V$aAcZ8w1!2E;Y@TW%<38E>T9OUVW#3P}O_MrAk6aXxd!=Tyd|maNey^CTO$I?N zx0;p;oL|0R`82Wrl7H&bZf}jRo2LCu(&Ep|{Hy*-uk1S&|NH;SI`K{3d6j+X?D$z9 zR5@hr_lf`Gh6R0R1uWD$Cj8p9`0Ul*N_Vqm z25Z8kch((_n(62?*I<2~E5`zfrWS$Z8P97hWTfANM{Cc%o*q7XYlna$OJ7f)V&(It z3*Jx8e!;f-=ZA;8`~8o9e|fgOT>kNn|DU>7-~V@Xef<5k7H$=d=^6_}`R_L|+CSP5 z{zX1_N4Hpzm5ao+b72RLJ>2XS~u^BSy=v~=)u&e z+WveV(KiM?7HM{2Q?jc!A3U4c|HA1*O{Th>_@$n?2B$7>J8NV1d=I1T1Kz;!OPuW& zrWG$cRwu3#cblO@dH$vS`d_Z{>{uZ$!u0P9?lM7 zdA@D?Lni${(|4j(``B~+9cKdd!_s`hV>ktb9a_a#HZdM5N?))2O#TP|bFsfdf&Dru z^KH!Lv-d{n#x}p7bYz?1^TSLHmS3)kKAE}vx!>HUl@kt}bCZq{KPS2L>5A>#OA^9f z?5nHM-XAo@{(*dWS#fy0|CH^&Yc*Eu|Iinll_eO-YYt{ zU)dj*%*a@0B1@8&z-&2@D0bSct2l#|!%?f)o}-88N2K!@Ku zo_jxXgsx|8eR0KhJ^K{}^OhYKByTlcEDrBtRy)MxibzTZ@FVW~nL9&MB^U`Yew*xHKvpSa#c?a?{F? zca^DO`sx>W6_WDeX6QdyoFJCAG5O<(8D}k2tmM4BR+?`4JcqkN_o>+B{kCUTPrcL1 z*f42Z@yBGoqnfGVu@_&+-M(w6x@K`kPg;imp{KVPoF<$+rLV}ZbN^xc_Ag~GuGQ*4 z&lQ`fx}86PZ6za@pN~(<^ovGkla-!7ddH^58L!m$VW-UQE7OCV@6U~o&*D`y*%;8X z#xm}B?A(4$zF^7DPUh&X-m5qmPwl(=mftFMu5`;jmiXqvmkWB`qT~2>%_)-e6wzAO z^|?sFYyOhd`2kgtCJS?yi50L3b8P82b25_S)s<&wF9j)yKJo5ZqHQTU<1p_dgRt0^ zR}51tW%zfWc6#;dUHoj(^5s^M<&&@L8|*D-Z=L#--JCys|I>fxUS0fju2kuzMchFz zU3R5qC1(p4Keo25c`w7E(8Y1`)W)w5mkBp2*dFZM@$t4J+q*kcPkEf3HFNP(+tTtQ zbFT{KH7xF%aBl4`(MgO(Cj?&o*>I)TR9M@ae|=ut4xyK&e-dVKK8f+>29G$euqLc1S@shc*slm=gV5#({#+hKlFi=C11nVGGfRtE*LzMPaz+;RK* zdKRk_9Zi|1B1=nab+6c-v-&N=)SvZ4danJU?$**P3~BCaO{;FToVgzPeBU`2?)2q1 zr(9dbc&g=8c;&3$44Ylqt0yUV_Rq)-@pw`&i#dzqkWT{B4Tlw3X@(t*4EcWxf|nJ# zD(yY~qw9X#kDi?Q3rioLIGeP7;lveNdjp$xSkJINx1v$OljWAUh^y}$y%XE-J$a=0 z)p^F8_jznh9-79?E;ZNYY(IW3({O2&XcGh19wny^-X$DoEIxjAYB$sRrW zD#hm7oF=w%{VG}Km%kp{IC^hpT%@p$Z|S0C3c-dJ0lv0INeM|2hFe#qr7bxYsu-lv zVc_i4Gne_Xj{eej_{>P0@H^<$-x-Q-L_y?{PsoxDwKFgeQX6>rH z?YrfdY4*r_@vq+9{QlRU4c@Wy+~&I*o8MmR6XskmbNtxP^Go0GKYo09n`ZI$+jBYA zhaa|Fzb?$wUDH&+^vute74tXQU0J2$l^AxJqo(ylz_tyo{}^5EI9u|%4yfgQm%Gbd z^OT$699!v*IYn3M&$RqJsaKmhQ>u`32F~_{ZU0YqB0CetBzqkF`U`@a1WjTW469 zDz*v<+8x{_{?$~zFr~mT&tdBW6JbmCH9ESrvbjt<60)RkJZhQD+hmb!az*~=lE=o@ zz4OlmnhQkF*!aI8zbSUD;AbW4a7Gb1LATlRAMdi2o0lbj=wO*)Ud^P@uvk_y@k3+h zx!koj{{POL3Eg@1;@az3JU`Ec<$pL`{ik4yyW45yl8w)|Jk3gNeW+XaIYr*?!Rz9_ zT@G>|_cF;YS|z%|xiyyQ^ri!|_1X8l`ry66dG$o&M&ap4asqutw;FE06Z?QCnbWHf}VNxI34Ee&^BALw9hc_GLq)VEr0ck8BmZ_eIoQE!@GR{80p`TmMRi@tSic?%Rz z&Rosi)OqvLjVrFto_V{!^=dj1bC7Ru_j>kMykZtCryi!Yb@pnrwdL8KeQa@7)0H_Q zb3#9Byro!jrNoU$m57XKEd}h$)!d7B92UHxG$Cou!9$O(N}Alf(cajzH6(3b4*#8q ztzN1*B{ln-u2f{*3p>2ww*0N1cW>sFy{vq>QmTFFtnIeTmuTcvovID6cs9w~M_=n= zjMwyZ6|Y-A53SrhO;aHGgq8Mmk=>gQ?Z2|fa^+3?w>lSYC|uD~SXwuW|FK2+3mTkG!{CJz#y*?MwA#y*q2KO*z$L z9P)HktbNQ^=Gs5CQ%Yp>igoWl{4Txk?vH1i(_=NKcDnAEA=ta1V)~PNSLgByByoDI zJiXIoP0r1KG5_E0sTGa8b3*jMytK{#jITFH_^)Fu(GA$9X}#s{j?*!dqo&{6vXt|2 zU3gsH4uO3~`S*W_ckk?FDiBLuBEjhM`Oeak_;v4`+Ft%E@M%=cc`Do%@_6qZ2e(Ip z)s9(BpIz^3w>{gT)o?wE?)}%|NC?&cin%zW3O%MRm=m{ z^nG&sR2v>isaV+u-D&x^^_+apF9)N7 zU1hU%?I(RPlS`>^mHGN(@0xoptK+Z#y?#$F^=_{}zg6vm5^n7W0%rRcUZ|YQ5g`5c z>iv@Ya%Zzvwoi#$v;P0L4S#>LhRi>1bpGzH@_at#cX21XcL%&sW>x24RaQ#Z)8G)bVD*ISD{tARO1^NP`~RUY!YLix|jIWlrrYHdp2A^G%yTvt(wn}SqV z$=SzhEF}_C*NQp&$Xq+q+s0-bZmt-!-b^Pb>*^XV=45->{>yTICT(Vavx>9lV3O&s z>AFtEtgKSJa#;qi96KwGr_Vj?6102I^IP8OqE^1g?MgSk&$@cP!FikD3$38t@1vLb zc$xSYi`N`eR8Z-jUJ>hMm~Og3lTox!;#Pra%K^*TySWQC%#oa{uD(`b+fmL>cUEuq zEil?CHuvn*(ns%D=kI996Kf90;!@!B&J>y_m@m+rGt*AwNLS%h|Fp5m9~GIBb1VNO@hgCy5;lF z$5&>a4z&^uKjM^U;IZ!L%}Lffv%Px*mU%7nILDT~B_MQ#RBIaNVb`$Tb+TDcgTAP| znYmhX$11_b6EkgE&Xwr39r^IeRd&Lzmd(K(otzP8$}Em@=xt3}$8pGD=GGPA>$A>0 zo20U|qv*`eTU^I{c#K@rGHFgrq;a13 z+5Pv*^|$Ti|8M5if1Dlv_mlZct1q0}wL93(w=qhO%6lw#y!vb)A-VwORuSy^xEXun@0b7c~R=(qk>f4-P62Z+~7HL<0f;( z%Na*5@$OHXJh9x}T4$q_+ZC1@J_a{Gi1y6+n$Z3%#D9&i=XssUvOTHNw;>T*zDQjEbG7$w?dktJHYXo{X}t08&D^7Qf1i3T=1c4?Q(=ov^y}FyeDKi+ z{$mHrrf$7uQB;$>-yp30_N0PyYkMjlRd3v|EPm$hRokZg{(Ru+WX6=>O_Ihe0#E;> zd{^c>H)rk7%R7r~&&I##dY@3#Up_V6Y1fIs>%8wc9=&nDE&WGoebp;ffzKx&|6SAk z{@aOcg)KFc?A)UG6*2<*f9kweRgzG7us-F2msdkm>w2@-cmFMTyDiX3N|3uuKk-rT zj0DEKhPk}lvz{cp*ks`~FPTH9L44ILl`lp!&M|BZ`5-dY)hf+uUeOi*j$Kn?uCD#r zyS8l}^Yy6T>)3m{H5UptRaS{~uPk#&HDFoi$jfyvXV>9G6NQEMzHND5^*PzO{8E~q zLOA0@LAleXY!9wtdAZd3(TWXQ@5QYTe{8dL={u?Q=UO<_j+8bE3vw=svg~^->HhYo zhFe$SPTv_UI-PSQyzcJunb~TPHe+`Kr^m$Ws~2{Jgmy0aaOU%h-=<+_Yjvk@@;aRH z*nRhZ*T1FS^K_QFY-`?Y(Q|wEiZhq;;tKVaa(Le_Uz?E8QKhYv%;A)h7d&%jq=M5~ zF$tmWX)C(ER%LK!Zp*3eJnJ4A+EpYggW8SiN-`^MQ9^do*$mmkYjB zc@e$u*NxuPs)}3d+U%BHef#Qn@?O8fzelS5`iIy7P!cv7XW#6<(b~HLcdA ze}Dbhy+7uEuCn!(^~V$<4!vF5Ebt`FxBVpZW=D5FXI|Fh7fORxr)|{_E8D;R|2DaO zUHa=!b1$!YQqy`i_3T9NPDO*_mD~6>2iy&p=6}3;x2dB`)Akn?O{)Y0!+5Vgxbdmo zy5{Dr59b9+4=UMR7hV{9!KXVm>auRPdClYoQdnUi^@o6*);b@I^`8rcLVa z6r5yq@-<6D*v?xQJSbsR$~UU!TsPB#iG{sYlrhbHgMfN=SlOjUmbkgAu3tK{Fr^~5 z^7*wn*H0`|P)wR@7j&nAWm;@{UpCj?r|LUA|6eLCIm^L(TwCD!OWPd2+4m=ulzy+) zu3qn+#mr{xs~sq*wf{=ak2`d_&l5V1Qv!cbf0y5pkwzsqFm zIOP8+|B7Dw`|DMEY5w@xdjB~1zu({c>Kenu%C%X`8kv_>toOsooIIWU>20@zj{t zhvMG_1$kV2lqO_(tiZ;N>bacp5o%_$t;W zJPImKVO74)RPe;?UxdUb$(yT>UoiE$(Na41Q`+UE5{|TU+oHZMy<#k>^?Xv0l3nq1 zCztzHn?*Q}@yz^mWtX7Q%MG(XDlN4*+d89Npt$4Oo$NWOp>H`t*5xfOPuKW-rgxW) zS;$rC&dwQ}#aAtx&uTbE3WYLmIAD_`BKGVWrEJ(oEAYcb1gdC)?eB_Nn%MuG#9-=Zeq$EMgIG>M)FH`uy1@{!#05 zyFWQk{_R}ww}|;pebmRgy0iVW=GxDb`|sZJ+TQKT|BiycpK@JO6xHiqohh#rsC)PP z^X+d3CntpU?6%pJzT=I&b>FS$olESGtbX0gtQvd$aNO2&X>6AlMSa@l_j|dO|I=;z zf{)BqySvZWz2<6cZ0@v15KZM>!4v1`Ul&NT5YmP=`vqKK3$N+xrB?uAhqS* zI_+l{`51)w_r`XgzBx^TTY0bMndy7^Iu_@0Z;IG;boE}_larRzU0Y$kzACcmeEpJR z>6;n98=NXIW|Uc@9$ufD{>^q@v!{cI^3z>2+MV@GUnLz8_xm<~qoUp8)iZ9&RXg)e z$hpjSoAI^zx6`)szEv=&=vd~?5vZy*e)NO+-^$H)N;h7{XxAL@?e{yo@2I_6N&Ex- zhqvDFob5fxIME?Zz$i*KLE~A&;hDDuY>VVCJbP3tuiX?jIqQFDOzN@!hgU90Idgqq zpmCbtM(^h4J6gpujRH&dJo0YOGw9x=W8oO&w2# z%9k>)+0`ncbUf>L@7*_1Es|c%&sY zty0@?BYWjZoeJf;?hM)NDeK-ptK`}2r8FaXq7kccw~*TW?OXP8g$qBGa6TElZgtGt z`B{F9hd2&BNSzroX|Lbp_i9V#^{Z-dKjvHC>N>-FxOPfBuO=Qzo`t~*?JYL%7pE5(+>4=n_jiiA7gPY&7rD^&b` zYeqohae*1Pqe>5b5xM{5=eFIqWa2x77SwV1^WlTIt)%fBQfDQh!dSOD>}O*uuE|oTW^gqaXfD-SJa*=j$KyoYen%pY~rSd`kGF zL!&0oy3J2z1!epSd#$d0l>Mx*T7Qqss~_{0Jb$?Lxw`EAiE9#$&W`U)?aJPs6UOnt zpkeV;<`_oxmvfq5r?71D`6#t&-Ob1oS{4O+*j&#>e?N6>0$<{VpBL8_LR3+3=>z!U;S&&#~)sw_%y=qAAiH{&>g||u0P&g@Ku}X<+ppE_VFH# zovPAct$gTsfYDZ<1qj^Zw=i{&-*adf?f| z{|Y1S?_3%DcoP?6sP>%@<&7NHet{8D2V*Xjyw$mR;6l~HJ=UjRTku?1d2VashUa1% zOHagwdd{(4;KbHx_*^jRMpAP^cBpH?_fspvl2zST?_v@=HS_5#am_oa3~k|#W?sA# zZNtsY7pU8-Ma5dooZK-*>seC}v(AwVCfSza<;Cp+DlG>VMm0}=tHP65{OIV~g+&XD z;|(@Mv@2?TTDHoJ|Ag7`O|ODwgYUOGOibp=ep0J@>-(hQy(zv%#ostPx&>2$PIFC` zXpPJYWjlLq=pgGNL28)QQ$^HJU&Y{mTT~5JwXk_g z*j{aPI>&wgEzg--{YTC}I8f`g`>~1q>kUcf)8`zm4LK|6BPyEL80?`r%`<&&=)=a8 zc~+*Z@-ggcod=JvJd^t75LctavZD!2d_rqx-C7fRd=fj$#Do>ExC9huZH{j1m^EWJ zV_u%rmR5%2B{RHOuJ$d^SUO9&MZ1D^fr4_IPNY=e66an?#tUK+0Wx*lq>_qDiz-wT zr_J~^ga7kCgKfJX%t`*Av#q?gG*3CZ)~_~8=i~9^Z_i)66pL)3&-Y@jGk7n*FZ6`@Z-|KEK%7+Z->yy;NKpS2atqxvu7xEJIiLvP!Eb zHUBFL7Cia+`u%<1A00a!cW_sRE4uT9aC>SUzu4)s_G-@Y$6Id8N7dHJ9G{ZZ)7Vp_ zmll`ZshQ9+ZI-j;v#xWozM&82c^hudiracEM>Vp6UBTMH&8L%jg;_?zJj1I-E)z6Q z@fesWYb!{w%xZiP^XW~_v_?5Lg{}tKx=kSl<}0>4v#|e3S<%Rb}EIj}F^~1Ba zD~{+m8Hh-J6UjQe@Qro3LGr8|Svln|k0t)^E=XD4a8}UrF-QI7c}bc_w@B{hD+sik zRjhL7RMnna(Jxu_izJuZ2nWtTRJOnEJ&S0kcW7|8Y_EB?thM!otvMp88w@^Y9w?GJ zJ#qhn5Bqz~f8YAMFemtm=v|rRi#xt(&N;`=+*aZ8(jb3BrHci_>(4XqmasQ`J8sGl z;d&=zXY0La2mLqBD^xQymx;0zzYkDeTyi(>PsM6A4&Bvi(}gTKZroutcr37Hw^mxu zV_p-7zP89kOYZSiZ@zTuAxFXS=m%$K20dHHV`LtEDAneeg!786J$m0uJC>bQ*l=>I z=Y4C1zPAa|9uv$nnbvx(JSXBPw5a*ugbm#4WcE+q`-eR=e$*wzk!#3uiCK9Z)+GT*;K% za=7A*qcXSOx8Ao51qz-*-Jxmcma>`l>d)Bu-r_tv-w*_oRjs}V+c2{ma^(u$=fl)~FOs?cR@>}e(|0h*lTwfn~y6;xXzpM6- zOPZ~$8+YH{@b8r6l(+9YPApS?(Yf1N-+K0Tn>{R@Pv=f;cD~E_GIsNH*~0rfJ}M}7 z`9|}-)LgS;X83g5R>nCx`J6tNienfZWDoMNbS&7&;KZ-p*HKksacI`9mRUFU29&#( z=1iQqZgMso>x5vL2MQB-^h>LldM@|A4t#!mLzVRVf5(=&pN;kqf5a_&|M~I#)BmX+ z&wj)(`=?bE*heE9$9%dYqT z{Nn%h{`vR6=f?F-(QE5W-~C%SWqH{d{@dRV|9Yolqgwvm#ht{gY6 zx0s#U=lFWfaSiXLWxJ&{Zk$LFoyg$!Y)XJ;`hm5c7qWE?ymgz^7}|uiByY@UjQO)d zEwRf%AS^fGl#n4S8++!`MzfQhAzwb6nLgFwa-Fq+W9Lqzm5xn9+IyyU@Ktmy%wnCe z!?;+`ps<5SaOD~WV}FyhB@%~Yv~IL=FtHt;7y9wMWKbGU*^YxN+Z(p1o!~jOko)S{ zqSa=m3A2_lSXfkcI0Q!U+?hQ^Gj(>4Y(bmR<>;=urif5qy&%gd9SLJ5fh2X2tD700 zXeD10;ae5?br<)+E>YJtn#)$j8TKuWWADhEx#kE@e`VWNw}uJXjN9&LGcRwwUN zm{d~er#V-o)503Kx_BZ|FG~eKKep=0vcNeWS&b__1tu)$v++-E)r?SV+%Ol_Sz1jXYGB7*+ zaBSMTRW2o+mutNqiZO(K?N@L7UotsAZQG9@F>F&`wkCX@G_9V?=*JQJ*Pm;4`13xW zHT~fEz>ls!-o0y^>8<(Lq+R0nd*`0DjQbw&UYT?{U9j9`e)8AYUopq$c?WJSZvP)V zGdFKpZ(;GtE&KOHHUHaU8GqWO?7@MrC$>7Xeov0wc`dqtMcQJ5K=HY$784GKdT+Fv zx++R`YnZ{7FvazvNAIrS+S8q=G;8^-32kW-HFn}#vx3{Y3fA6g)AtEEqb0rDId-e% zw@BkD4=-Elt}9rv>tHnNi)TV7v_mTlgB`Y-<@R6i5@-rgXcK6>!=U)6A?eJMww+NW z9$a|^!CCIVb0%!|m=PN)n)WQqzOuL?IVD?h*0O!3&swHKbvxx3f( z=axAOiUdVjHcXLG`*uG{XHCwE38%E&F7_Q*cyWVX?^C~;_vx>8&*tuquxxLvl{(M1 zk>N|F7W+KWTR$Y;)tPO2$l7m(f+mBVyCp_di}|6+h@;6$~p^4v6NH=HLHzL6mGoX>N9Nx?N8hgY(&I zdn|8VR!=K>&iwE4#~ksuJ1!bUHFjK`Y|_X2iK{BbCQ7+du=#ec*O|N>hk6zCqHhS; zKl{A+!}2NIdOi=Ia30q=IHB;zy#43Q%-%oKuB+T~m+w30VbS~B3)hd11E7Eai6?~>q#h>~}y_5nXV z^#3>a&uxzvGM=m;v8T24X-u-)jR2K<;fCQ1r_4O2o62nxY?oMj%}T6vz0f6v1wPF!vdaCUj~&8GbJrrEtkGcOlm*4r`zfCvC90pSz8yr*J6r@Yq*~Hm${Gs z=iTtT`iorI>~EG``}jdvSVF^6rJ!&DNE(<96>f z_u;abuych*KXT?0VOX)6X*1V@Lk6u(JGVsY?6iyw_{O0$ zZBfp$y<&Za6ZZd?@%wMRgV`zpT&nuM+;g-lM7V;$wGp2cejQCu2RWxTG6$ z?_IO9oOPr`K>yLX*XBNt;y8NpnpbZxh-bPQeV}#b;m#wzjOvf38M15Cl8P8Dl8dua(sr&j3`Mgg-rT+Q`pCyydn=+Y zPXAy0h{M9;4wqTHz{Koj7j`}q-W$X5uC9+swSskT;8s=Wum-dzA}ez z&RIE8?cCIowOUMU8(WVZOEzmsxOrz!^1V%E5$RLr9ong*y|cuTaoL1~$U{MEjf@4m z6%-FFw)&+IWvY@`u$E2X_9>48`#!KRPngLOwON2UNkg(Yf%ApHgiMy;NBcZm#nn$c za4zHXR7|hUa}sc1if~C_DHUa8a*Qd~zG`#9BvX9ShOGG&lV+UOS$M4PwU6W?u_?!+ z&h^w8Xn3ixx;q|Rx@hKGg%?2_29r-5Y${y1B-hb?@+EEqos+I*^gx zoi}SDvk$TGvl(kWGd+-)bJWYG?{dzvb*oCxl}as?MYpChXs@KucM2+vclVu|{^ioCVHdtX@2_YJ-J z=D5L+bfs^_`fuzXP5UqN_3zB~+i?%}MXryMI+V^^kj-5$v@Gmu;jHs#cN+L#zVmD8 z=j&N}cO5xnKKtrY{yV9Q<(hKBv;}X<_Z92${>#*#QJi<{$1xB6op1P$pRP6B{Cf4- z)DO}P>)zYl{yxox;ex#575jyCe?sDZrT_YQo= z@R{{0%NXB>zTs?-CdWC)ZjEi-8uD7!Ve6^x*DUkb%f!wpjryT?g~x?wMdZn6oGvU* z>$Zk)cen05zqG6%N+@}jW_j?`Ns=yBt52t1J08KMU14&uOPj4@al^rzUm_On@k!e~ z!)fN7kj^c;4TUaBY3|70Ca^@(IKXi3^QhQtL*7@{emr>9+maflAvt}`&FoUGvo#Cf zmhv=qoH={p41FmL61CIOR3<3EZf>*xC~zLk*i{Ln&%ja#Q^YbeY= zGSBqm!JySU7tBi2KI^|ubYX6XQ1XwQUB!NTt%48wKg{}Md^1_ejjwuBIh(iHnhlvD z-vrxl{9EOj>wCy}=^r1F_@ZY#*X$l2yT@?j_TGlH`TH;Di<`>sQzxMg$I)TH&ho?0CdqYQS*G5@qTggJ5l*-GO`Jy)LIM~bFKP@}? z`{A!m-Tx-D|50D@er-W?&r_La4!Sjur*L%qTCt+#v(e2NcaJ4RDxLN2KihDSmu+h? z-^>s4bF+V?_RMi`>)F6OOIkiFeRabZ!TUE)emfsH>F%1s>H}p*d>|QFcB< zWAHVp6}+xd)A-L=l>D~3yO8Bof4A>G;b(rUZ@)U(pB)k7XPLb9l~`Wu;WMFH!f(GF zzwveU>gW^A(P3p4*Zy=#w#{AXSW>(F@~o26eD7W#K6sk(dgR-;1%CpP+8TOPb{iRf zHt=dm)jzjL_iXOWXIf3^d!B!6|My{OE}Lj_am4kd+ltn_>uZ?3-Er!vvw2e7E2^HB zHD3KNOJ)tb!b6Fx6#;v9w*i6o{Zkfrr_4lJL#e>Hlo%Go8 zda`Wwk&Cg<=lou`x~=o|dev1ie`{-ZM6cb^Hs^HKi|ezSQo=+9S4UL8GfYW2w{%DK zt3@GFPV+t$Zrv&BZPw4bl8GZk@SD*49RY>b)(7Rpnhg&ao-3aB`fIHCZ_m2{VH&c# zWUoB7IzKg0r$L+Z=?Uwrrqy#F%~o937(1QA@RkIlKxl|;Z}pmMh5rj=yw9!d?|pb> z-5CjH9vRKAJs*o?G;iPCv9t0e+iKAsflD_tLS2G-bSw9*jb1g&LgQiScQ+Lk&jUAd zk9=$VZ8+=YzMW=^o*xc0uf6_!6MGGZ;OdktgTA?ojvY?9!gN>0v!aFVh;;4KgEJ#E zQmlQ38E!?DgsTc0DtR0gl77W!oU)?e1b4GXhbpU* z__JHP4Oklw_o}StnRxX~T3P4}>y2;AZ=ZgoQ@TFr|E_=aD}PUqxVbIYm;YCE_TKM? zZZ2o8M`f@7Yw_FWy>hM_te(b znY-B}{LKtOWhJ*S7o)TP|Nr}y!FZV&8m*=l_ANA<=2PxLWYy}vEq{?6VP z?b^}hS}}Kon|oJ_Jyh7@b>o39Uogi_)`V$S#a1XTymljMQQ9;If%6~uHf{=AAJ}X! zboyz`jMfC92|@a&&T=U(UzRo7i__b&wPoR@M@x-4);~5fU^y8OZSgEq{!Rp+Q@{p8 z*J2AtrtZiujIZu(TEl!L#HVQL%{e>D-t$QN-eD+P|Jq^Bm4If|Nv3a_17$CsjcR0B z$-$WWTueY+u92w9<9C*>NmL*tB1i)qrqvo{4>5BfHxHmo)}@7d~1{m$8uA zT5&E)B+aXUhG@0gKwv;Jdgpv~T0UAA%iE0e{_ zu3MX~j1u_r?%lpHzQ&Z@lF#?f4(M<*IyXU>6d%9 z|Cydz2I{3oezGh#W6@lyvvUsn#WPKt4b-VfzWA~vAXGHgS z$n(B?m*uc|P36j{El+lelnV50j9`~dKFM}2ZQi^I`M0mn8x8suEHT6~E9adW3;MO|0wIqq|_?K5Sp85%FQ)NYg9Q1)DR9@7@T zD~b))&TF@A5jHf@*lTmj`}f&LH^bJ{T0gs$kdfv$_oM1=#`2;*=}POs=@Po-6JFD9MWzC#ibw!Sf*~ab(@;Mbv{kJ<-IP$hU z7R?izEyK3tb3op+kAE`KVpe%6EHi%4I&`6bc%WFhMcmwR>!1{w~B6i%DSq&N?5bj#^#UK{9Ee^1vM94OgNx)ux5`z zFN3)5Z4;)Co4vlb^|m}Gbf)g6g^iPrn*R@Bu3B0-7lo5Ra!+y+_ul5SbH{ygiwIE)!(15_k@18 z|GMWv!Af(kR<`XdMjc6;-8(h*=DwdXTXQ9oV%MQ%$$kYVSeqG&1#f+c)nv?@C-%KH ztCvUEOhQKUnCf#kXGfc;n;Tdqx?V2UVk?k*(R=;1fc~w0b(h$I`3=G?>+w6ef8s(jQw|W`Yv;P5J)yG%YU&z^rZLl3|>LKt}3>l z1_}A>Z+@CYs57jaee$SXW_#72O|K43+9qVh>BGWzZtl+A=Pq1ayCeL;{8_4> z=6{p_wY(rFS3yj_^XA%9&y+JZ{!|TAdAXyW>1=6*lezt~-{);&8!Dc?e|U3n z?aXg?3s&aomigb6eD{n+JToAgZ&P0$L%Y)BDZw19>|D>j?h!8Kh&VZS7OPJRYty!@ zJ(1gI?b6jgm>cRcuR>lC_!yW@mnOYk}N3$tE7 z5y;N8mC9NxvByykGq6IAZ;ili~oFC^rezH}gQqWpQ#`HI<&y3ZX znjVJ^GVz?6dB9NZfTWL;2(#%u8{xwV$sVyRjx!vDxLsH_DQ%YsXlC+BV3pn?tdZ5e zfOiVF(;_433&Kj1R~g)>@{D_Zsbr@8qJPs0H?*sNVmayj{=uTTamN@6E*lF?j50}T zSRI+*^t_3Qzu~f$4ZB0|IjL2OVVZ}8CVK>_t!R_rc3Jz?RH7?;oAsRDdMyKw_>1QR zR?eDy`OurCQBRmW*9YG535{NMOtQA&phX{e*8X-(}|a3-QU^&|K+;U zTU_7wba?&0=ZYowcl+ecn7&EE{m}fspNdQONwo;|f8J!TcTo3jnZEq7i)RGH4(q?; zKW)>@Ze0EG*}jmwe9Xqn4m&@Um^5Xv?zG4!H#g5#D017n_v`}IRjF4?ua{rhlfKfs zCZ@@*Y=imT3eBl()lU z>%tRylnVNdlDM?BI^;ORpU5m)R={YT5Sz@b@uwhxRFkN3aZv;BI7bkELDE$O$zeYnb}q#dm5)xY1j z_4XezuRTTI{)mKZGrMr`t(9K?qR02@wa>|a*}eU+%S7W(nSc6p^Zfj4``jba_43>% zuesWd4IaF0&_gWQ3Hnk%^Gc7VtYmFBc{e=98=E^N-)niELHwCSGDnpnSe?6 z94C%+>65qp-%3|by*26oz0c?EyH;e_e}55u*iNywWqwF>U8B9_{?E^Km8Iv#Z8cu( z?`wXkfNxh^P4O$ez6Du1aZ1Y^%i2SqC7iU~{`m9@o`>watt|Xo_p`7F*=|{R^2(f{ z+(gjILV;W0>yE{~lT{q}r%zzL-F#Pl#|lNZXIJ)L`0dSNDtas^%Kbv<)f=BCtmwm!H~V{_+UatTABPTK=3j~Jc?5raF68?Wj{ zh@G+ezCM}H>E!J1E!$@=6l6?3?%TAkcbYd7m*LS#-Yb7vobH=>_I*EpT2*DK<;!Ke zYl5fy8Hbloo4RG;s&^JL&*vZQRA|{SMf00HC%5O+C8dhbm=wOPc^PrNc80>A1GPf6 ziL1}C`Uq<{a&leWYJKey^RIQgPk*g$d0zP4z3jh_`JU72pkH6-H3hXX4**N9%m215x-+W(Mt^Dcp)33boPkI)<-uT5s z*o=SKgP%-)U&P!={`{_N()6G+A*wG6-flg`CU|%c|Epskk~z&|@BeyqQuUaDK6h}5 z$SVO|J)Kw2eEl*EJ~Wsw%?b8eA>1k8v~=;8Q@e~;rybKtJNJ%vT4?Rp!kzn+&lQ_; zOx9)yf3n8u?3Yyy4I$E+>r~fPA3l@ldVFpb<7_*=*^1w6N;-v^iW3w{7)9BhZk5&! zyQ&)gey34@wCUx&s(WYdo;7nW zIXjEBUGjI-yIl&rF{#CB@4CENnQe!Bp4{1464qDsXXiQnz3&!Ij$D`@WcTN;*U$g` z|0m4em8PDv?|Z!3&a&AhyHeA*tf$S}r9C(Q?T^;->0b`UFZ>;U=xoWGwtnY5D^h#c z+uQBw``fxoXvUrJTAs>m7VnbleXYKCV{e~WzSPWMjpqrMHCoNS-1B?#Sc-DP)UHlh zv?}zBPUhZelIg7uGgT)t#BJE6dS33U0PETB@8U#PmOGX9tMIjMl+;#FG&yoBYklVt z!>G1NyTmsrvYq2z@a9onljW8pIcjDTW_tE;nQ7;)N^O|+b>U+HhF^aI{vWgd{&D}G z-RJX;Gi~0-?44u1YUiq%`Bgu^*Zq1{|NZ^nSL)}t9WwRr_Q>hY>tr%YH(T&>@eHlm z(Os?!xLRHNmPULw&QVxU)!weXd&SjBh7}i&oY;G!WwY7ZzYc7gQ64VN3 zaX(4ZEF^{5#bSc@kx8!?_~=+p2-8~SkXy7U%A4b2;+%-**96MS!#||X($0@NxV<3A zVo%uJ(t{5*RFW=jj%+x(Gpb+p@ap_^yPuZ&&NG@(@@B{R?3Q(IpFh^{ZD!e+qg(WC zjkFcx$*|XU2L7l27A@Dgwy~fpw=yyDiuvVD^Ec_K7@f)be!Ng`R_IKfV>iBaSuYMM zy(rduSYh3~T(p1slg_@0`| zhRvac2g{#{#{++Q2?TyW#=?HeEdy0>cI^pvuqL;qMez5QzZYulbJ-%IY^uWCQO zZ)-M}Ow2{+ks=`~cqr znHN`YbsM(r^S*!Z#P7cC88XYKIqd#?_~PdFyfX|nFBnS8*PnAWGqRTunJC_>`C!G? zYpZ;l*4=p~_l(W?tbwZmA2avP%_lq${6Dwy64RMk-cbvAW@(B97lp6&J-PVZBj3z- zTURy927G7eXgRRrj%Y_jUIt5K@o}DaXM`kT{2hwpt}c+VXj`FkZIYPOgATP|MeAZ8S~&g`2pFuP=>yy?%3o!Q@pdqpqtO&wl>q zT}p*@VESYRULAv{U)^d_ml{mE*e-L&WJCJx(0iN@%H%8b-KPJE@tKpGzy4qzTbgFr z_L`lCL$@+11pcp3Pg4JXGFkDs`Qw{2Zt}>wn+2V#FrRn!Uey&(9TmUc?RVrR3qF38 z|NWb};XQBDZXV~eVN(>h^=+EkE%oT6Mx7M<)*1e44wB~IbP9tmN7p@E_WH$-6cPLS z%lmzjig0??6-0-c9^f@2=Vu8ElsRx5M(@U7ZapW;R5-MLlEE{?se}>F&pz zG=pOo10h~;Tez!8r3RaaiVT^y<;!1ZY(=dqB3 zT*=J4*{yn2XKr$GGDFqii6uZW&?dXH(K zr&6~21JQ)N$J8FR#HTWuGV60sSz;OOsE{>{Nw)k!=XJ*ZmEBJnIeK2XZ2xmdVZMjl zir~1)rKz&%&JD^bPv)#G$OVtj8 zzsKX+HIB>*oS(aHwd7J&wQEVwP92%8a+*y;ap{Uxu8|XzPT#F~vpeEz)VAXf4D}yz z)C5Uv>^OINe%>(^@wN90OLr!x?PzfOaOg(Dw98XEGkw>KtECxnF6i!JX5=_z$P?A4 z6v&c%wqA9Ke&hPLSC@YhJaD^vf}Z$S8BLq+MTaN2uiwfSTW%l!?#I?^Su#liZ-wN~ zxFj<@H^>$gWS>{OhdJ)y*RXXjjvP{%^vsFfC3fXhXO)5p8XJ=C$cwMecCA|!Hh1;4 zu+!03FXZOyZ5MKqSKeZ9IpE8qzAsA>hdKKrH*o>gTIxPP|ogbIRZ6h1=)r<<|XL zfA;d0eVzT&lj^1$d8t=%t#y`;y8KkWw@}W$X}{{RU)Dbl^uPPFW2=M1Tf1K`p1o3f zey8MD%eM8>7C8!YpITpkv`~4&Te~Nx-(UWB-Zs|gWZL(~f8XAZ-kyDWMQLK+l{@)Q z8rb=K&TP6B-#O>V)g}u?lf)?FD0cSd9u}syjhi%`98?3CB|FwslnG7P%+XnJRp_YV zCcUk?4>%N}wk|uwG{I$&6XU|fMM}(^ZcH-*cp90U&)zt3g=w*agqz5Ti9)G|a}=XF z10{5n8ke{kw4@uc{1CD*CBOype$dfmCfgVyeRtJet&mG=^2K%zizy7 zkNY`c^X~YPKbQ6U*8Z-`{p7UlRHCriq*VnsyEX*!C4HC>G*3Ca)nUJ^+^eCBM|bv-Tarf$7RUrxvqdJb756xbT3&Q( z2|dOU^3?IWXiUxLEwk?>FgS!VOpVx5VmOWADD%<ng#WcVgr$^UQba zEzP~Tsb!(`fm=+>$r2Zof0@Xw65@}&-=VhnRCy*y``~?Tyg=tD?)>^y1qYC54x@mpS|avvgy4Sx>Mr-U*v8)R5Fzuq}A=3>B5! zU^Z2Io`LkElJ6CP@i`PD-{d?2d&CG`$S8Q{aE7f=w{%z+$_S-R! zG<)O2L{d(e? zO;O2)Ofv1wdeRmd#S8%tijEz!dnN4qImzs=%wgW^QP-y{pIJ36DWNksRkXr1-J17b zxWCF)|3`1+OwUi9y{hc$=WSCbcn7CC9c{c6*KWqJp81%|)eXHj-hP;n?aE%dVfNzw zKgZ16Q)iU^N%^GDGP~ooQ~cq{v$xC6t_qxSaJL>?pJCVTLuV`n(sSf^K2$OXZrH@? zoW;52>LTOsQ-mLMB}_cnruK--@#aj1j_~q0$F=X$bQg-($<2-sw)wGBlcktt?c>%m z+h2?4zGhKBEq&lYv76N=*28s5wXXk+<*Ry+ewy>uGeS*bx_Y1Wvjj0!PmWCtdH0wM z*Y7s3{5io>wSZyuqH}XStd(c4XYNtsU7!)x$mi3$`|h$YIfvHt?Ng4w;NT~ITQB^m z$ZM0hkKJ9LF0GqqPj@Y{g#dPqUmnvt;9mud&y-SjvN|P6cdcjk|hAw_@SLG{r9qsxJB#>2JMt zxuX8W%grxVcFS&b*}3Nhk86v>lBnv1GEp_{S2u~YZjv}vF-KqDtm9g|&S#x>S$%HK zGoK%teS&2xPwv^YrB?&~ZTnEhdNQhbz39C8`PV;RDo8piJ=fx0HdFNOomF~zE7+3O zwgv3vnrQN9=jxr3tUGq?*G<2E;EU<$=@LusUFnvy2+?gne0cGLlM7$lhs}NP>`2vL z<`+(h?w59+GuzKu*gegBnx0~(LsofOjpmhwt9Xt$@B5#*^SAP*!c!j$E-3uF*e)(_ zzkA2dd%5N-UkBa)u9LLNdmTT2drVP}=hkI2IyCi9U*KQqU68(P-Rq8%S2B9<1q%8p zZkzY?=0}O9DM5GV&Tw%$XUQTM*P7;ge0EYu{tdaJ@5k1j-03kPZEewxm4?QKTLVLN zMO#FYHMdq?JnJ(f$jc>W>nzi0p?7)y?*G62-#_{PH^pzCj=gQLYpSS3Q2OlqJI-zT zJ7Z^acz<-wpZQ1s&SwrQebwgw_975CYVFSByak`;A6zm$!M$9qZQrOL+v8)i@T;EAcZ`@{0_l4B)t`n^r7u7ygc z#1)8UM7A567KATkGg@1#%eOLj*;nNy*9BfIJv2$dv7ceek3G4pGb?XjJ37OO$84rn zBhTMQ1<_U>x4T+4zj?p--uY19*(-i8Zu}6HWl*;J?&j4;CYs0IDE*k!ED)@6&zFJs z_^aD2{)b}>W%=^9%Kv(M;ciZ_`I4g-)JyEHA3gT!+@;*#>s)k27RBWKDzvj|%Z%D3 z@%!zDZn4V_HqR6H?YXl`&w17Hoy(5we#<73TL0(4%gJ_ujK8Mj>ypj|t)roH~Ge*5lISDndn`IW_z3(r>-%q%+;!Eg z+S`3Kt82REv2}~*E^p3eus>vYsl0t!tHjfkX(`Ws|E^8mzq?vs=Z+hDjceq7P5Ba& zTo(KNUW3*1J>PC|9Di)$dwRX|5#!4yXWu8@-uHiR|M|{ChHdKR;r;O^ey5n$w!eFp z8zQ6CBe#3k*-MXgEPwI-c=i72Qc;Dr4;AirU4Fbj{6ucz&!~qV;uc+tHr@BTe$$7Y zZByC|&oq1RBrFmiyYpT?&iAtuyH4%dSdHI#b(`NiyIM`;xR&rtZ|P%;bKYr-R{bilc=w&%IQx0} ziKDRw>^9Gy8{EY`rY?$8L$CsR#FJkhL zML9iK`_wtHYh9CC&&&;7m^ydG^73Ck{T+SJS3g@U_HXB0$JHNhbVPp0Uh)2n;Ijv- zo}Jp-TDn70;K0^3C#r0|)fpFW$;h#ed0)W0*m}>59}QFG_Om@Oy{mZ_8%_GkzG3am{kIZ-&)NSyp}f3e>a5PQm*lor?`yrt zJCAwCbMZ9O#OmhsGritVzvS-wvj4*3a|w^a-kBK6r7}jL6yfnYWu89TUo$GGE7Dy3MA^aYRM8X6nS_)i*>NJG0A6mj^$0 z{iPl+yjhP?fn#y+qM5<=cNR9c?QZ|bc`8UpRjjDk-_U7ClQ6@FG6AN;erFpdmpFDX zwly(#1fF8PSoYR_@@BysS9f-HZde?!PJ@^(%KL`wQfG-dcES)hYM+W`?era*XLO)sJvC^hLi)Qxs8d==7M&#%W*4 zwD!+w>m;FuIxXvm3ckO(9_I-JK%m;OeZI{lv&iBv@S$LkKoVQU?vqj+Z7CqkEdH186q9dxq1o(M^ zEm(Y5+NJE@_Wk{kGEFfhS&`v*=D#n`nvy#ebUg%xlGK^FcbyPhA3E)J)#tTZRNOyB2_|4-!Wcts34)iw!~EpB}4 zr}rU=#lPh3vt^;$ ztJks~6Q1z=i#h&2Vy@}gs&(spm{`6}ZjoCp`i3W2InC(lDSxuu5CeiEB)$zuFTJ?*tvYm!YzuvbGEHBKJLwRKG1#sJ|pvf zaq+Jc&X~km)ckvuf9uP0(b>DL_wD*n|8)P4==vYk^Zz}#%s=0D_sgK`<$u}F&#(XR z=V#jPUc;ZY+J2L{vJKL&U>S`;nE{Mnr=J1IUH43OPF|ia&ocQ8J3P)=Zpoaa++uKk^UR|nGb3L| zugv0bOk$sLby1d~n*sC3{r~NMnE!kC|GE9H9s68XPqjEz;MnloHYT;gP3OYIc6E_$ z|K60p{H0#^J^fx`$MwI@em{PA-mmr7zWcV!8V9Gw#GaIV_r&K44>uFr%EJOT4z6P} zaF%!?BOxZW@`$900I%{@g~pH>7fV8vu5`4nV%xB?V}-I3SA+x$n|hw;Y9Fb?Ii+ms z$CBb!+3IM;%3kS+T&tpH*IPGjQu*z5{j!J7DES=8aqengxxwRHF-PX@uioL`ZFrrf zCY1~IXsU{b$z;i-SQi|B;1R2!Vke@Z{KZhM#33=PeBIQj&!S<;T#CFcCOK^-k5w6V z?S3&mmC>kS_UeT!{~zn~@3u4Pxf8$P)r?k&WRA3NVT|wco}Do$POk6qjQ*ay>b|c| zUBBh~e{Vfso>`Q&^h&<0W-WWb+ud`Y75rM}|I%FZ-2SUJ|L-0AGui6i{m0Me>*fCM zlvIAl)_il;dkzVv#jC=MpDn8|U%M+JE#x)VmT7aQuTfan7If;7DNAe-uVuJL=glnb z+g^+Nr$>3OT=uKLqVMt=^|ubni@w*|pa0BdcOgox*X-q%`OiXl_w1kj>5Escji2{6 zt>ks%#2&C(%lsB3H1VYc`3ee3VvIs180n&z>@7iX{V=(xBr`nlwL>oba! zEZc2@2O^Wt8d)VJmC6*iLBB)ezN3(HI5FHkTTtJ$&~&8U9$_k!JL;S(5!m9b;ZWZSV8# z-<~r5UgofOqrb$(JLjFs|C=%Wk>p;lUH|Ou?Ee`b3%4>&Xkbicf_Hr#%(Gq`*`#Q4X3PA9ug?rLcF&iZ?$?&6l-QjwtH8q z>rCXiXw%h@e0Y~l+>70MH-G**wdHM6ijslEIt$}o>2nNcu1*i?tNqUtby1#YwpIS+ zXO7Y@KgJe^U$nZuAn)j%n6nng4xHhdbUAUIWm8_3iim?o-l1Q=cC_-;x|?j=EgA8M zCrH-Z<9FFB!85Xl)_BPCIXFh%H}!KeHY;ac**&**%FoRW+BaB2%vly@J>w}gxOgEq zP{LrfpSXLU<^6b1`?)oR0b3vaSaNuI^zWD2?FYMDk4Z}}%Kks&)PwwwJj)bB8(Jnw z?vAvJ^O$~ekrK1rdBf*^KONp3`q#elf0fhKVV#)+Hl>(sE@yQ;4iM}NBW;KQwbZ~W4@9{&AT zxX|zJhmALR8t!~Py4*CTYvVK9o1G{x0`zX zck1ltALD;y-~S)}e`)=*$Nlrpdxt*WUCZ`V;`#67oz_qK?fwM#+W-G_^?Cfhs^aqP zxATtgob~eT+0vUA9`497clMdsdFPDp%*q=%xAfjTdbH?$dT8n9vuTGn=`S@X=2-aj zdy7EFk9Y63aUON%IAe7F^234-!)sAz*M#MMdiJgR?DrYP+;d|cd31B1>}g!LbyxCc zNrp3@7exho8M-MMU6ucTjeq)`u-}^%Os{R4;CJNYWrlYPr?&Oa@yP$rGyQ)5b$hPu z^+|l+m)@@_(z|jtYo6S$e_FE}|IEpJ7r@RbX*hAF5|gK?^PW|0Umeaea$SvH)g`*# zRQgVKXvrjvDQQ!@6FOFUPxe{m9MU~a!H2QHl->H8kEFG|NGVdTQBNqdid?PN7=F%7H+&gzrJP0hT6W&{_FFZE5(1eAKHI7;mxLFjNaRp ztUk?U5bo??+Q(ufeD%c51AOk1i+?J8$aP3RJXd%j*Y4~HTdjNRcJ#jQP;qQH+AtyV z%CmI~7tQXqa9H?SVCgJ*OOx&Me&3wGUvFmcU6s6**>}EK*PKd7-Z8WFfU({_nT@WD zvm6-}S(-PR_=;}lo%)bv3j5BhLC+3YFZT&FQGe%dy;h?C@7}8sTX#;mzW;ho)>Y%5 zI@2FrTN^1hE#eVlo&O&mrJbw1IuzR&-t}udW6}>$^JCjzZt*d6?d2)cmrhv}*}IAJ zdQ`yCHTph!KFiLo&hHLg5OGm;_BF2HhE+Qmz1J?4@H_05m^A5xv=Xz?9Iel?I=kG} zrmxsH!?BaUVYZ)T_`Bt)^Smsh=5A6GTo=WM-W9u5>3IFz;{k=Buj$H7H`^yg6mhE59QBudh{hNOX|Nhgx+06Re%eBk1<(dBY zgxPibU(fw~DHWcnVLLaz-1Nl#_RH-_AHONwKX7aBFJ0E7NkFOJV9vi->&7en}u2VVX5`|!z$M{Y0FcXgdH z)9d0Yw%Bby*L^#_(qVb+%3UA!h^!8-EWEIOSK&KFPEw^Z0oxQi)_5Mfc^*>ki&ij3$>rbrZD+5J~j(Xdeg4RuClVWD1^cck>EtMcu^oKVWxwK?uzd&Tj#Z%sovo)nYyV#K z@IgYv0{M^X=RZtbSl1eV@I~^!i4J`wKceiJOAgE1JnXNkocKhvp|)~bEX$w6Z}ZP? z`}5|Z|1Gt^HD@hu2W}POl7Gz3^mBrJT8rdy{WtRLOAh#6X*=_2;YHT=tQj+|<~-<~ zW$eA};?AF+kL)~T@PUEjumWS==Cl3x3zAd2Iahv43OhI{_gSfkJex}@OL8iwhwKzy z9$p>UCTW(XS2yR}`6wn}?jvzKc>C#yr#{bYOYi5%Ut6_2Pqne*;jyn%Yc(4bZ07wD zk3On(cJV)%`BJB}|EWf@ct{AI4+ zvNQM2xse-?6PM-hCgPxAmN-p=VcMGB)BNdA7#V^YrO&j=Xf8gXsoO07p3}plC77!+ z(c1e2i=b`yv9?po=OnqSq_9XRvpqT)8+MmvwY2fds$Xy?EPgPt9FNVzQ0T8y6d@#kC8)-Vi-wq0(BVY^MN@Yd4sc znr^>MoT-zW`0QG_;js&4KMy_0J;%5#B6^XTVQ_Wp5f&k()Rj-K{hg%274GR;EWeF? zuCUG=hQ|`SEIxgn|Msoqs_h;6Mv3o&SiC+Pw@I)(@nH~_HZ)~kGl9!n^LU5M;ozHZ z1J|ALIrOWjz%Nv@bftyX?DgjBo?pB9@zN7976Ycu8#-|8HxVGE-;O>=n!jEUbpcOFZpoeLZpgObDA`#n(a9M!K_~{(; z*}mtz*FHTtS>5MJSn&PXGdqqboVE$SntQ)gb7rJh=H{C^mhKZz&z_w*@BRDt+i&N3 zy)3ZU@jGpErt#;>c{457zT08hqM)?(+BVA@Ic#Zma{al&cRc=`Q4uQNeRq@2=}k*S ziqH8r-mIwRy&jXzYSZ%Jhv8kngS*~N=1a7@Cl|lZ#iJwq1LwZu&xC!d&*(EAc20XU zk57I>-zu5QXCKemwn=8&?$x{MV)(u*xIAi@tiiOgTS9o+wy7S1>Z>=cf0tpa-@HyY zCE8<#E7RtSyP8&RaAY{^rJ|ELcb`%Bp&kW8Cu56BHcOWUPBKbP52AW!%x&zF@Q^s8 z!)S1D5ld!oZPUidQ4B>Kv02j9Tl*yEEGl8;Ul_5!$0c;4-?_D?&Q0TVaz4;*(~-CN z=?lg?9v)lXEn3F2PDAv{rBEJAWy9kWl!6|sa>`%e?#H2t%X>o{$#xub% zN3}yfj56!3_nb&Lpm|2k`j$)MLIKGLMIBbJT${6+nvJD$7U!M``mALxJfY|4x$DId z9idyLqG@U@6E`?Lbxyvx_yyBFgP0|fNi$-lBCkpXv8&(ur|HwgXvkKWWGJ@6XvWQF zt90!a=w4=FcU*Gl$w!yHZw;fSwb~@PzUIK1^cUx{zcVEM&s*}xaL?b5^HryR4~V-z(K9ae{H^M8bHRpboZoId zf3te?FYCltU*e8G{PVeU&hhf{-`~AHmS1{S*L(QSvtO^Gt+_v~3wlssvG4f0_d#!d zy|X>>=eylcgVq_@|DMjT``EwFYFR>FdEyPtLc2SMxbr8Ku9>zvNKdk;L907taZpbC z(}ETIHCQ7qP1tRv8SqoFOi@68se`Z5d2c=MmsX5B&YnD$nb*rB_-=PdSL4N{9zLtm zmcHYiaXe{>)C^~#eH}&{88s#-$;{B@%P#R*;87&pcRSC3`%b>N-)WtQRTEg-Zk|hM zj8x#0U_TOT9~1jYsFXv)G`8-Xxo}kmn~ciDgI6}$W=(#V`^anQ^( zc&3Z))%Ss zdIiCaC9_t{-~Tmt(YmN$k(;>^zX~k;mToH88nrM*YeN3{!1jZ0uTRXmo2Yi%pmR?m z*ZSwPnT2NFec&yid5k+YUuVD5eX(+p-OZ0{Cs)*Ftvey?CLyxc;Bv}r zv&H9TM6datCDqPxOXgM$>(3v8?1i&xKL+niGT8iWmWG25)8Q*VYt3Bm+-$n%=5Arn zDy^VxcQbm+59@VB?XyK25@Zzp-zqyv{!#i~)q`FZ6}kGXAD#cp?dLw0kyKP{-o8LpxI9a5b<4xI2lh({ z{W5!TZW`km3#J=4eH*o&2{XyF=$J`Iy)zQpCF$PAvnOy0XR(vak4UDaB_c7KkJatI zUc;L#)FD!GH1<*2nzpA8Cv9~3asAio-njUL^aK0EI1_L1B-`2av@iVf?%u3*E=#sf zDf4}__1>jTD`s!toWaR>;>Bk10|y%YcsblQXkAE&sVMgrJM>NCUDcf_hVv|!o>`L@ zvTCyT1Bv7~KgPAWi{3dez1|@4s_>sxrkCL3Ia^zd^H+bB+48FK>=_3G-=FV<{_g9R z^xK#8De=Ijl(^=S4ey?G9pvB=PJY(r7#6y@O(Er>V#};O%G-02QzRyIZ@+tL<2BLc zozs+V91)(&EYi)mN`>v~)nzfOFKelrn)NF+tym@GFA{6s<+>*5yHvu%knDA*GU8rL zU6YyWnm0>&b@Qb)T#Y#!m?zv%;|+N@HD5CM#N!gCSthy+<~vJXRXtYjGRu4OW!j6M z8oy0ai*uu-Y-^ix5B{vNpDC44tDk)*J7LG#9bJ0DGIxR|T=a>^_0)Xom+QR8^KbL> z-kfhcIkIwIHq|OTUT$ywiC2C`a1T$#{qlecRzWBAD^9AFSIeiE^_fMVa`w5dXUfyw zaWDCY=Z(Mo>;IohlRmvf=GIlU@+G??ICWmHsfm4k=RwPfjZ0U(Ih}T+%#cm`V2;Yw zDF?3xa2jkES?Vvc(|66nw|@_3`7ICqd2i2)^dIise^X*@UzJU(4N1&+y+mSfal2@A z)8yzWhfhwuBgAy?(D_+eLT5sxLKsV^==>Tp8oIQ<}dmGdh2sv{I>r! z`8?zOJTspV|J^6e?#E4Ib&1jz%q_nyt{*q+=H%JZ=Bj==kKWzBtL9O0kH>-_7>s`>PZ+J^ZPBp~qT7AxN*NO7w29=GodFSi=RiFKPfBqfA)^-11#H@J!RAWV_^STu*<-$Sv9gg`jOkoNR z3(qxrT#bCN>7517J06ixNukbb?EO)$lZ>?1Wb1nH#XJny!V<3Lz$12dMw8H)=u9Sm zhe-y{jg(idmYC5xgOQtKo6_Z|?W?ywGTpjt>%mYSuIFw!ds8+?awN28w(Rf-2wkAY z$9*ElcUk`H(g_z{II@2GWKiVcv!{vkuH&DZ;`-e`^W)=_GKYUMA9g3#5s zPBzUc{`l%xfV0249r?Ku4XT}nmn3iw5yH1b{woKPLxxXnk=3?qxD7azFXTi zB#1e!n^4A;vSRkyOPbBxlV@&!X12f~Hz3=>*t2_sNYw3#2I66DR_;lHii{_Kbl!^KF;e^L>)5GzBAeJh^7#DkAo9(&>1M!2Iwtrgm>l9`GtXHwtoz z+VQ3GL$RChf2+V>lGSoeckbQWe&{z(*ZzIG{^W`N`u9*^-@=x98|M1;2mU>{Ir(#U zm*O3JnfQM-JRj$}SA2W($UJb>`r?p9s?V=)|Nco%W&R7>OTXhSUj0{}#P&z6|Jl2% zGOMO*9&ylF>M~73ymtPsT!+|Q?@aT>?jPFf8?fZ80f$V5wAB3O)zJ;crGYIOxhXoX zJc*lcwkQN&+xTMA>sc=}FDQu z^7e_%ILOQ`T)X5U*X@o?GiuB}y|p>AY3Bi(j?NN29^*|$+OjL-Q#N;gntb}5T5QwD zgx&qS`%P6otvXlh;8?J^*MD7NSC zpWUW^Y)O*DjX`p5F2^$s=}2Va%D6$8HDkX&y27B_r6%tRrk}x9ZhwVS49z1KhwsrkM_T9UWXD0?|r#4Fe3-B>IrCoDO{j|vrts>o5kJ7c)E!wkc ze^$_`h6N}3g``eg-R=8og>|UW8x2tfli&m9htK?!eWvFpA#PVLw(ZnL2{oDAT!Y;Q z*MFQ?I5Q_pICIml2kkkVnTzdqBy}%8W_tUITb}If3 z*Uf?tEwKo!KNz?u@NAe=zk-6@r*F;@x$l1LSf6>|LV4}=yYKif=a_$)q~iZ$VL?uG z50`41Aw!U*{npjI>lpses-Ky=>!`TkVf8=O?zi_J(m(j8 zs`(Coh1LVLeQ68VoHjY@cCXF#=%0$1GujK5_rw@aVK7R29M%0awdR30H|tW7t9RZf z1TCD&XS*n?{f?R4n{$VcUAkJRcRkEWBswQk=bc?y&XnxC^6odRV#Oy4JPFuu<-bTW zVw%laVd0>ZXpbE`e_Wl}ad4}VakjuV6P>p%lk}XmT+eu?OSL?@cW=q2sT}6*9qRR8 zKO7aVESsCxa#KTSm6LNq8lQ@TX3=qpEL)wOJjL-_b(YNzUXt|sd-hS5rEWh=UhD~$ zygWfwEJ;+8_28MJ*feSOTSmpE9UYezZB==l(fVoeecy;3BBrNTzJ922w1eXai>R1j zoS3Ouk`edQhg!=#%f4UJHR}0V!6`kfzi(zJx1h|FpfhWm`GoJWrM={1d}Aek^+W)3 zW6#eS+L5aFLYWWGczC*jr7zTN8ArzEz_3tZzSkzri=Swue^qmA6XL#bcVZ#O!icX- zN@Cj_&WL_mh3^gS{SG z2wgPb`E1kwSVQUU2fv2mEvuv*tRL)qe_rxX?(C~s6>0b0mbsYpC2K#9W^X(Z`c^j2 zhEw5!WFouzixw-TxHb1De`>gQtb6~So&VXNWKG-sulevl`MC>xx1G8CE{S`Rn&2wG z#uKp|7uOVdTbDPA6bN{fekw?5#eh2 z{9~1Af@8vxjzun^EgXj&6bx=i)vzQy+w$uC3PH~nfdgFJ4mtTg4PwCpf**Na-d?G3 z?$AwVC-Dd)X~EsX%`*-Pz0>0HzTD)tOy!7DLYvf`s2PlHe6x9fd6x($F&tc0^nKHw zeJ`Ap?1ML*u-M2Tdc>!z^wo(gQ@U33d9`>mo;f&g(V2UCKQsj@4MHA^NimoeU+}u$ z>|T*<#;K5C6#Rqp*QIOQ4g@UNtXF7u#*$xiYyXU$lib;!A3b0oe6}-nS%}4sbAKxr zt$WqL{$bN5W>zl;&BS#_g_bY#O;wxIyl>5>PL~25?RU{vng!Ra5=c4Ze277<^`o-n zx0_PW-es{VwH_4Vc*dtz^ghJ<&9~U;PZCcTJqWqJ(K(f^IwfsFX%^pXXY;58JAq9e z5=`q&c-cA@bC~EQDJqr9FzdTY`l?^@TOGj}c!8CLO~R+KQD8@RlKQFW-}4tRCEZnv zGMTV7ROHwLDP_Sxk%&c>-7^_)Un_VxRsM{E12em6@{Bdv>-AkvF1vr{+n+ec9|t$9 zr_Z^)GpRDa=g;d^U%q#J_?~n=`u4}&{puYxpY1+n*S+5BwZqcT(rROE_Jf=88VmF1 z{yX;I4rK-2HMVcTwSs`A@j6Hh;ML!aI^fv7|QO zbw_xTo-wz1lf>#-yTdzX>3r~9o%-}pXY!8i)0>Vg6FnuOlrO=SoFDM+v&-Dro(@g@ z(ipxLtwV-v#=AFYH_Z7v_3x_4iPu>+3BP(X+w#?W-z%&ZJDx5Tc-d0x%JSQ{=GcO+ zDvff6)l-$fu}wH>I#1|wPO@{W`JdS{ndVuj?auS&Idd}Q*u@CB#pIKb$4$|{J~|M{gdUI(C3IdESGm^B&?X|yshe>|H&g2Gjpx>zY11%U{pSF za&Hc+u>r^W2USUH%PeoVF5YuQes`A3stervejTZMJ+*f0@oaaVX)F&9#=VPXIOohK zQKzxr{Px4_8nxHI_dHv@mYYla+Tjl;Oir{lntyv0e)_!q6o36K<(ZO=_X_TE@w@yj zGuFRsqR(WJz18wY!MAUl85bOvE4B7*xLTwAbkY3o*woK^@9mOunR$3=>}*|kA10Q^ zlF7N9FTxmwR2tsBS-Ob9LySGf-y&_wiIn9H+smTD*R6l!`pr|JEh@l+(KG+cS!wC# z^Xu(jF*04BTrfLubxcm5|Mu_t>A&~L)#=w-MOwbNvte!Ao4h;hujVaFSGd0FcYE{9 zvy)e{s?8E-_vv7quEMiTrfB`qW9{u9XT3<;zISgt-|>z2>vBFE`dwYMxAN=lV_G?O zxeV*M4g_^t&u00zx#rF?^)RCYT=R=3Dn`wJUbtsTcjd{)CPxKMD_;#VYh_mEw(OTYF0@4N^p{@``Q#jUXZd`$GZuF4oAP|+F|(`$kIj`#7X*F_ znXzj*ok=Wj5Z-m@b8n>4s_kE&`sFd4i8Q}!R?gpk`*3Q#tdl?z$FrbYE1xNtmR^x) zPhECo#)h*in%qtpNAxgeN<4IV^N6WJqUc;;)~Xd}-TF2h`_Ae5Rf%WQl8P07nha8G zuN<{r;_5r&smTrA#;vd3sCEkM){}0$SoY{ar~2wi#V5APyeV)!?{!u0MAPArSDQch zs7P%5G}mRGc&lmfuBSpXbGG?D)yhfE<+I;>Yhl@zyTvOKi^DfMKT~^m;_;FtfA!)P z+?IcL*5`t{T*hxx{ci^Q?%m+a6)3s%XLjVeyn^66I@ACENo!BPE@39|-teCCw*XgN z2iN;7`d zzuW(>{eN-Uf8PJK|G(eA`F30E^mp&yA1{2fYh7mOEj!Op`ES3|&(AYW2))mLxn}#B z&u1sU)>BpTGQ1Xa_ia-7#+x}lr851s_FFhB?|)Y~W%<3f{+fTAKu5;B=Vl)HkKcXs zFJHfPoAm9wxAtu~d-Xi?=i<=&WxL*PzqNi#(4M_4p|zi$czV68uvxZ+?eMqS`*C-z zt7F-1yssyIHq1HriA_i&mZ_j?gHvKd2#@&EYNf9k(^O+xN@7226-z8n-hOjOmhtQz zlV_T%_e@@Uv*_BSF0JjEW|_=?=O>r#xy97Krje;7lz~awa{pzI{Km$&8y2tGd_;_C z3D=Q;11D~|xM=Np#}(CkFKv6qAB(iGrtrlK!foot>n?=)?Oi`lLWtS8+xm{mR@q5L z3EMQ7)ElM<+!dN2t+L(!V&l?ZcF)(p>(?=2aTK4pYjtAGs%!CeJC{tipB)$5`}A`A zMEz~o?Qg~Y?>=mH`k#0kW13jC@`{c_NAKK>3FtQMP`_=&VRVTp>OOB=a-QV1v_F$J zR?I!Ccxso};@1+Z4;^@8$$09pPnPNTr3%w6kFGl)SYUB4{Cqtgf;1iQ;Py&OO;# z#Wg*uTj~3anHzlt1I{fG>{L5kxGwT!b8gq#`VimT&=uPzZ7Tg6qHWy5IkidPutboi z?9T2p0V^U6xKH(*kU8R%BftHis-6;eMuLEj(073*!PY+K>APmI>3C1MbX;KPL++lN zGo7aI@!q&fBFo2+6*-%!PIe}yG5em zIN#gy;w$?lE&ur*{c*GD-;bBGrv0;icj4Q*Pr3cy)-sjWvJ~6sCEKTwxx4``@ESe>PtKbEV-o_vOd?BbUzKS^M54e_rS}A)XjZ#=Qsny7y^WsztF$ zHI*93Z=1cKIwx1_2-DhiJKoxNmF}Cj&qZ=|&gExL=HcAs$AtUVyREg)Y8jKEU zPr6jUuJkU52)z>JeKlEk%~6Nob7>;kI|YKCaVV)N1|*c0yJ^l0QTA?J_rN=0=JTkp zYxA7ePSZNHHZD3w@|@D54>s*5R?eJrRq40VuTvZs#TQgwk?cqpt)9)ouI5%zt+w{t zSJ8DpBs}V7=~{MhzWFija?}jgPpK>S$9$7@otFIj>;3zG93SOvd-?Bd_QWZba&KoC z)*rFov!$P5&jEe)Jb~R`Uv^z(oc;XAiUQ@?GYV=_=gUuIeb^XpW;sV;Yt$~K_Z8Kd z{vxXTWNR;1>$3>By%3mrlR<6PsYzEk%n}7Z`2Oi|bhnWym|sz0ZW$AHjK?9Rr=q!E zH1&{TQATB;@wch!#x)Z5t{e8=H9PuGyCyNm{F`l~&z^AYLn|#mvv$tBcO(4Vty$Oh z?6UTs{C}$Y_OjdDucUuV_4VBGlvnL?`Lt~If;1lC3A^R{7H4IKeQkNjX&3inMcBE> zr$3&#)c$>TT8I7Wb$6S!6RxN?XIrY2od5Vzc7IKBzSWnBhcA9RyFwu1$Py)HtB>oC zvkC>-F-eHW!wu;=6zL zPYmvC+SmVmZ`~Au7FH&SQ$anecg>pB!Bczu(7SI15+5}_U%XqH)_1`!K4JT7{p1d% zHF=GXx25Yw=G;%-H}_+J+?6cphufBZxGLR!gJ<{ph-2s7f2Yq|W|<>Zm9k!S?Wwz= z4D*ULm)^UpS9x|_#9CROpp7x{(-r4&o?mi0+x^TJ&Nr{me+@r;`03w?z1>&c>YWY$ z9Blu&YxVzt_iQ!4oc@(KPd{=$Shwj%=6~Vlx5wMNyHCq3i~jeiZQZfV&WVg?irc>? z25eyxv5au65B&c4;j-^1Le4BZQ}}sG(VR znLqak*yL?lXLomc$>zkgzV60`HK$j_ei%TTxFw;8X*9P)4bf3p59|151zWH{U9~8x`_g^=s_O%9+m} zzVd47@Zd?Dbxh>((Wj+d6FZ!Ge0BBD?C$0cUAr_UE^gH;i$33X_qyIjz23R&oTd5x zUpD7`chr5GEPSzScdpcrb+2#5+`ZeL7;rT;@|M%7GrrcJZ+KVk>!0~_&h3ag8@b^7 zg64&{RyHnnO{)LdEN{$Fx<%GuN6_#mg zRS9ZRTuqx1F{`lhh%9GI-SiV`Bzp@nN^SXV( z$zr04hN`F)0x``Q0fVW7*$7FFjZga*}Oxa_!p0>lRj&*nNl`U6REt7ROR~GP@vEu!T*AZKlj1DhoUHh?rB1_M#2`)^T6(w#kN!{i{BfCp=acd1@_q^DTR5>LO#NwV`jGZ+bLa@c#R=z30E@G?$p3-q}&0 z*=r^QFC}i_C*L54iNs{aLZ=-qI)0|HFFzls}oQZ#jRd zZ)gj z-`wRhw#le05!rh!L95{WRz1tNIsyya@+L+$gir(4X*wPV?$sOcB`m3?eJ70p>zIqMkbfxEtl(?AX6AdD&wN z#xv6vI-JcfR%Ug~5$S1QDx7L{Tkc5q2cnJmR~VNo9Z&P*)_jQdzc9gZr!mz&yPv~2p1Q~qx0OzCIdK2V-i z`*;S=x|lm}&hfcPYFqZo6-zUDZ+m?G$0N;CsR9Q&ewr*z-dfbXo*^)r>3DI>jh})o zkpj2v6cufbzH7M6zPnfEfB^S*3zLce<#YZuH2yujmLcn_sonl#?dLysGPQkb;90G{ zq}ph8^&YhiCXI6QpQJzEeqV0lpS61qPG{feoH-0&(mZjwu9 zt=#m}FKXi#w=Q1YRIC@+6OpiDRsTL+pL5@<{%`0BRQ_@CQd&x!+>_UK|5Vy6bc;EH zitqlO>p1z;g7qg)m9U)uy(l#F_9B^N1Lj}9e;YQf-*??tbN{~J#uExhM2gPV?oCuI zz0LDit55gz-M{~2e|9c<(`Wm6@|P{|6HYg;X)x$rN)oCie33lhYz$Drqs_mb3pHm z<@Dd?r)_+1N(Y@~aNE?q`suFS0(>7P1)UM@+t*#{U-#$NL%sT=-gED^hA|!a@a5A( z14v%tZJziao_8qeKbwCo(ytzVbBXJ7v0y5X?l`(IZ?|6ZQ| z@9~f5-F9zxRmtD#oSk4@V-|ipVf*pd{2v`xeh!%5d16aX34_3uq_~hMFBZ-zM|0-& zFP*Zm`>~Dx$3us{^~Ej!Eh~M(N}wU{wZ$#{cQrda!yjy#Vf-WXXVUT6A1wqI+Q`f} z5~VUXUCpcK@5dQi$~@(c$(Z@CPyJl~FaK|W^z#1|^}oOWm;b;0|10@FTkronnE&vp z|Ht-vN&fr~U!U*z$(Z!N{{Qa(_n+JT{l~xVfA8t^|Ly-D^soD6TGDY~?v29_<@R{a zJ?nk%&AhaujqzWf?i4+?sr1lNdAs>}&kKrYR%yE**eA7Lrl8R2_|}5V%z_eu$?^IOj!d#Cm9+;_V_=JEFb`{V!qsVNLhWb6=V zS(s5^DI`|Pap&f(cfAa%?G@TrqB3^u+-WYg=P>JxW#td7HdbeaC8{Ofcpi1%CShL2 zubo#f9-nCZGjT}?r&8>()n^5kWbM!@|IblX{r%?;{Xg3O@6G<^K6m zhCB*OORB4He|fTY-MioW-v2&)*tznT$+D17Yps)d6jr%PR+`AR?R!0G=C!C~p51qs z?v9U(n>L64@eawhL%Sa~|NU<>|NJ{8{&@zzKR$8)dvVx%dHv6? zdAoMk|Em9)b8qVZtNvg1-+iq7z5eg!v+L!i&0lUX!~b*79j#r<)E7kEop>VKL2FLp zlw&uP#Mxxh8Pv|q2~^YDyF61%ho!T!NJ5fh1q*w$?gP6}`)R6vpG$rusdU~>+;x24 z@@_Ss=x4i*vVGWeB~|nC6#p*Em*$s_``xhgI5+R&;kf;J?JuXU-u`uwblK(ZPhZhMqsd&lnHl?ji+x;mDa zOtfe8I2#qY?%INYC0VB@wI@bo`!O5x?rnK;#AT}W73Q6@%Nltdr!Wd%nLB0S}lI$;1Qgr4rE)Z~? z5t4M>wO_|cGw^io7*6jA&J$qT@tb6xvl)lUnT)%4BuN`rJ!(w8p zKSX_f_UqNl$*(`3xpU_G@59ySciiUd7k#tz+VOt<_e?i->)GmkH9!2}9(SiKnOX9QbDr(4jubOJy(Y5c;2$=3?wkm|QoBWZ2ffW|IoJ(K>|89= z*O|R9=AUVOW$~t`l`LB(TuoSNw54iY)YT0F#=&bC&S;C+i0elkjMx;iYMSWA=c*3w z3G>u8I$NIlJ}xth+&%>F?I9 zj=#PlUc8}K>&?M9=7|j*AM%*J8TM|y{>AKNxo;%5M;ez{YhA;QXcLEQ-4B<87ayLW zyZ^v*K9SjsZwiD3&(s{*%gDIgM0>?vSDm#tW*1M2tlrYFZJCE_s(srh*R8IO210ub zw=87Y?t1MEOVN?Tx^b(X{+G}FFSq#iwx_L$`wfrHDPH?^@2af*=O522zCXS0URxc* zUjtcZhS@WYxZUt`Qc8JaA)vg(qjND!&yyoX0c$iiDNp9Ot?2he!2Jfxk_d&4v&wk} zc?<5=?*9Gl?Qe_n`J3m5?q0R;c-0xp;}SoUYeTE&7wgNW9Q}ISF8=DusQ#VTW@Yz% ztUmcV!rI`6@Rf2kt)=fS>eq@s*~`Z6I5+z4X2Dy0`jd1emY0>-U%noq(PHW+sUpT_ zAycr@EasuZgxR-GMz4ulGTU&Et!Ao%k__t;K8+GfC5;Euv^ZWl=*cZRFx$+2$K2mM5?AwnKUad|9km4zl1&K zuTTC`Uhqt)QH#kfC+Wd65Ay^8>-^o~+BZvf?YO-s=tG2;iGq+sSm_GkgPj?(KHUit zJ>j{XQO2nK@63u@?%BbMS50sK>!#yYk$6>sZGytHCmT7}I?vW)G~o%(dVF=Ejr|q> z+!|rEx9^#PK8f%wofat}>}=swyd zc^It2CbP`x`udMy&8*h}?URMtFiPBd+kqp81N8KM#rQP5a%0Osy?67S zkDA<*4;>G`{^#6tzw-Tmm)rHm{+wL@cK!eE`oH;dc7KoB|GIYj$ISgro3{sO@iEKU zIc@$YzW>ww|GWG5eZOuSQTOcPC;opc-`<{`+{1F~<-ePEr&)I#6F42=mg_Qq?~C=n z`Pc4Z;@A?ge*1l``~U7d+N);wYA)-YR@P;Y50}rG$ztN4`NhrefuWbIdybY-0S(w zL#F(U>#oq$w%z^t3a^xMd!)A?GK;LX(y{BmtI8*zSkbnlMSU%g7>~aKcZ)B3Y2F^b z)bG8CPH!&#e=+~>-QRx~JoCMLMB=$n?YzM9-Ldz-Z=M_Nl_}Atwsg+suwKO}lUh|d zgkwUd$5vNWWi5-!iqV<9Z0D|BQR}a3E)6=Bw)y6o>(i%CZ$G>+b5^94o%8q4+W&w3 zs@fX0_WJ9{f_LwHFQ0kv(~$F7+seAdZ=dY^bI$Vo-MyAxwSAz)ZMnCXExY-??5_b! zTJiNc*Jt;xdKSgFW}3J0?4q#ix3!&A;+AcyX3{vswpFH(F)qPEVLwy5xd^GtFOSXN`TWKTPV1G^jI1^baQC~M4A{Jy^K2OJ=H)%hofbqEo2-1E6m^Ap z`OeBIv8hHO#jOV;r<9b*inPsieY(nQYsXfBMn|r>$_@TeO{<`}Wqd39mGf46)6`~M&9|38dp`gi1g{hPRd z1y#>apMMe>FB{u*W!2xjSy_d_9mhj0yk}=U<4800zrH(n(HTB&kRp=e$}Q)#SZDJD=k?BhVC1`X>(e`IC0{c& zgibz8F}av{)_Ys>0qL6yN)PT57tUOC*tv%xOLL0Tr7d~f9wD#yJV{+HsWh*sPI&*^ zWslySlUro2%jow(?rLa#lXEV*6gV6= z`;fD;E8$j0`iG!BLJ=t)cNHvBdA%01%;RS4l=Tgr+K?*V;kowMi=9Ce6l7c!dS*6M zatE8|US3iWz5RUM%=@nIy^fr@{noF4k-E30p4i+yk(zf{_RD_!5M&pYc<;)JcX6Tb z@7qR~+_hm83S9a1=;?0r)%RVm>rL;Ddj0m}jD6eKQl9O6WB-hI_jcdUX|i$laaZe~ z^Z&0CtD65{zVYQZ3GVyVUjH+Tez#lb-^{1Gm!7Q^KX$NO?(=cZZ|DAadjIKP$$VvN z+pCS?dckf!e;cd}Z9XcVet7kOWO%Lcn?38+_^s7=ZnH5)Uhl}U+MVmx*=}OsIM1zg zxOMFdL!ZLD<9lWbncwF>o;4qZX&E4|YuvYv8hCX-O86I_wj=dQ}*!g5$H@ zfmfetT&py^W5TH8la%+c=Y8BQ?L@)Vth48ZXQ}PspO*gid#|g<0A9qps;&3f^V<2CD^p%X~fE;f93n%*je-)7XUx3n^ZxX0o^x}` z%TxuP3FOH(aP!?#>1 z;GSLVo44ZqalwWe9V%@Ckq!5Yp77+oQdFC9=g-5BYx3J4B}5-MWz)={=qAB=p4Cvx zJ3XR!#?m}C=G{?q50*VGOlQ7(yOV?s^qh-7Ugn&UOaR1RVJ&) zRjKDWi}on3zsgwGm(HN%Z79sKS!KZtt!LH~WL9mM8!!3i+ZxWy4Tsx*d~^O}+7!sB zbKFshQDV)R=SrPMTO2(_(y|lUgqja*W}Pkj_k)K(NOuRzM2|*WxkG+C*A#9$JXJww z!;ME$OFk$+k!U(o%R5nELdy)9oS@V>V%N7jUMQ|IdFXMb#ITv|QRIv%?r&Sx%vi~p ztX6o?g6-tnfZI+A?ztOsF60(QmUt*uZQVCNOnb3(PWwz=7dMWp=lK{yv>s$+KTT}j zee%)Vg0&5ujw@FkzqzN*tk-n&$@JsjC7f4o-RgGoRhqX5L(vshKZcmYl7?QzH~bG- z*cq6xZ&6*~_1th}VeXq2@1}*B(o7MxhyP|6T#d5n;+$;Ys4{&~+UaeV!)9+%c(ju( z*V$?!&e@ZF_g$L9uX(g?&Lo%abJuEq>1@6g z5Pa@>n#Rq%*0SoDXJ=eg9!PS}I{Y_G;y;`Tsqe|8xGo^LEC6e}3Qp!`!~=&-%ZA&j0!S|K5E&*$>z2Pv^(htCf`d z^X;DV)aJp&uFg+=>naPwyEL}Qw`=du5e+Gm(D2_HrTDb)XVUz~ho8;ez3Khuga;oD zw(Qdgdazk6+^|}X%Rt&*^1&*#?acZrua3J#^Z$LsxBvI%_#NM0%2!x#v&atM+GYIV zll;${>;HS&_zDS%I31rg|JgIc^9Q$ZeYf=HUKg5enrgcC+>)y?Yd4AQkX$))*Y2cK z>wT{?9C;>JxaF2{Nl$q|`Hg=Zv=w4=-GGE$Df4d#BIq zwBo8wmVs+$fBluRS+Z^6l&b&t_7=*-@2lAvwKhvrTwMI~suH!U@5^>a=I*Z^c_-p= z*F3iF^_`sUw==I!_Wk|w8s}l7J&X4CZ#YoWTG8~)j_ly4%iatoLDtew2Vimc= zAu#ayuR}#{D=VL=b5983y=1W2P zn{tkB+$)tGmZ}rBQn_t)G1tO9vZFPbwqUWvRVIY?qoNv5)p^l&K|Nif18?_>n~zb&ba$l;mPZxL9!=zvx=*&%2GTd zH~-ifHecQZ*^_T#1ShrRm=+X#U&Q`wYNWur+ziI%&r`A%?MM{-wkj$+_1ew1y>mlF zlWzF<-AP({;bf@AO3f4Lu>!_3ySh$qnz8fTwGRt7O}Q5C&9sBz%_^bz>#_3T0lP14 zR8R>JS?A>-a?R-4g4)+7PNy;^@Cl`N74)5Pb6{E2^t#~11@Df^N~If{y9Cc0sx&ee z`*vPsGME{}VZLl0o6y7qicJ{{60~ojC+q_u$v`a6i zcvmjwzHFj-RKlr4@rZ;4%jTP@4T+XM*Ej8xPW<>`&$EA>d1pMmatumO2isL^bKSoz zURYzAI(N&JzkLhS1h=|wcF39@m~o5ux82c_d2fwgXHIh0ogQcwV%Mv5_pB-h4?V+oQtcZFKCj%kB}jYaTnk&PWtY=2lF~ zn=STWk@-IHcU!N$e*5oNm9^i@&Y-T>S3mvxyIb>^M8DK30gFx9YZop)svUDcM58TY zt*%4*Uzzh|&8yGvIMF-pgvSBHV`{1m8FP%y?j6Xt?^|)6fBq_IyMGTFjXzDD%Uswf zqARfHR*cyS;}G6HKJB&{Z)N0fA1(j=CTHVerenft?RS;_Z0{*CdbwfO%d*F6eJ9dI zn`?OH@Be#v=KPh-Tg4wAT9{>||1P|{ENIu9XIobF&;9yVj78_q{xKZR6y4yk+@I-}!STnU@Ex?$8mxyJnf$ z(R=H*@7R^@%Uw2wXMsF#iFm)!DL(NekqHd7lUEtY*R+c!TrV^H9Dg?Cq4pe|jW=(( z_Afqvc|p7R!ivU}Co5&%opKVmUUz67oAvowtR~w7Lu9okR90R4d&Ng})=sAd%Xox~ zIbTPWvk zxYH)i*mKi3o9{(x#mx0fV*6)jJNRz4N_oT5bK^7LQoRkCx1ZHinejPKOXTZa?|$!u z#fP$o-Y;hhUexlJ*tK3~G8o?WFqW_EQZ3uQNeqt=Mctc)OsmOtgw^GC`@9*Pj znRr+vfWaq5Y@1=l(S|DLm9u=6`Wt;_SZj*1XKA0Q-kX%xZTE4*p}Q_Q775e(EA;+H;iETN5KE*BW>TmVA?E$Bj zEavm%Nic0@d=!@$!+*M4CG*|9{1Vrt@b$8t=n-Xw{fz=LofcoAi2;rM{ch7T`}X) zYk3~a`SVPc+ka!Oc>VBu?T;7n|IU0(+xVlioqzhL#mm!|pLqGOfB)9MFSK{utNZeX zYjxnYZ3|MrX-4?S6a_k$e!usvEm&a%Pw8gcpa_lfh&u}>EP1oZ`OMsIvC!SMA?cx~ z1YH`>J$(A%_I#O%fhP`>p-2 z&HsPO|M@h%?sN0^|1KOIu})=gyh)rtEk4dOh*( zb7#`o{`nNoKYTmAxFu7`4pHDy)7*F{mQ{U2p3b8NX~82rKFZ@9&)8EXaHUunN<<|x=&G-sckRX!J%O{ z-C(MG^*^rV>NoQJ{R_%(o;$)WaY0g(@o9(o4C|atiKz>@ysTZ64zO-JpmyQHb*{wi zs{{&*cIbCq)eN+bKCo+16>mWEhK*gPI?sP-aFouu*0Ate^0pfv-4W!j@V~d%<<{j{0IA+*1vz%wLI;QEZ?{Me}~pTl>6^_d3WWey>CtbG)G*x zFCg(u=x^9IpOO&ybz%>@SN|vw+y5*=oCFe4W?wqIy%b1e$dS!vpC^R^V9dw^w zU@&pwh68;8KhOAy^s&F5Uo5}#+RJkmx@k%?8|>IV)Y;7AwV8h8-?L^mhlR4b_b2N! zDE{1bL+-)tyL&gJGrU|Y$eKMP_Hx|pEf0m~U)4(3ocx!^xCsO;)WA{S{ZVhUyAvDUCgVCX~VOsspn^Jn6vc@b6ce9L_?uP3R@!# zT=HW&3RiDZZEJSm>`dEpKCNmRlYsElQwnVT$JUjV1n%^_zcNaZRaDb!UR&(GJf~UZ zvJB3$3z7^2-PPJo9pAG5`uSpq9qR8>daU<|c&|Tx>XFUgywlH4e3&m?prm|k`oD*Z zzrCIOu0&$FcTA7boy+W}kA6D*kGXj6ZS4jVH`{gGuQcUMu0+gKjNP}9WwyNX+YNTE zySJZa2nuZFI(&we=R~SyadU_0QWkH;oYSS(kNH+}J-GQHWkX=o`ww1$?q4MTI!)iW zZj+DsI%D%M2JH@EMen8bpGLgh-Ncamb#2-2*r4Wx4h{l~H(g!L{_w8O<;gD%CuyGg z6K=gH=tkRnzonN9H@xKF(z?xg_wJ-=-@JT`VwBF>nmN}P&av!Bc&B$LdT&(O*~see zFF&N5U~bAUT&cmedWP7pjL%6=GV8*AYJ51__WpUn(i2=l2No_e%c|M4J6%C{a$#@A z`KL9Rv!5n?togH{#YjKeeXrSzsCTV*eBzim6Al@uh1vaX8!Yty}cu_OE4sgp75?$=fS-k;_&w;}2@f8D>kU;S%e@;`lN{QuJb&;9dj zKAiu*u>RTopZ@=U*?*sI_vi5bzsu$S9XQ$iee1vX|C2Zhw%erid+s&rn!4!Fys0h> zy*v`WTh+cPPCpT|SpL%szPU!+tQRJn?wNE}%iHGPE|WQKSzWU;rt5@>Z|wEj zSHbhEK0a!l85^_bl~q*fMo*QH_jA`N{5hL%{yOyghl6)7pZ#_2=F7d+CT^a8x!-ind1k)eE;^UCzWw?&)a$H{UH+`FmW~XYSY0t$hrB$6F4A=q+dDT@?!j!G6-SuX$2movD7+cNc!lBhsh)h(ji;lgoj&US_ptrW z*X`=zhu2rH=q}qHRTiS3i!aCFtP+g9NGdaj|ptF8}|TzVBcC&;K=3LhtY1 zyeGDb)5%Tj1e3I}?X0^F!t9P)lU3G+{0NHo!{M>+neP zpWm`-6v(9hxN{fufvm)Dy zUEc4xwnlZ|rh+ez{8%FT4|6ToE1Rj`#fwA6-nEz`)YQy+wH21?A>j zB1)$kltNO4I>c&Zm~4+-H{2;He14NunnC-4&>NXypPh=b)IPtsa-py??vC`P16NqO zZOj=%RC|1Lwx}F_bDppCnBc-Pmt{<1jqMlC*#46Yoj;eA=ZuSzqQ{)*>y5oPIgT7L zG*ddFG;#IMoN32TC3)XEIF56h^sCCg+k5xNl@(V^J5 z*Qb47*+btC*S`NgoL$d#C~NO7)72NEd_`ycJ;~X(F87{J^70t_Jf*K2y|&69vkW}+ z{EYjde?NHdtX*|%!kzRFXIr2A{eExn+?E+jC{qM@jq|IFsy1O@9uaPWbWm&@NbmXPbM&~DL210XNLjPxd{PAc**6ix?UDdf( zw$TsYh{W|TGGWSSTqelEI7`B7W%>I#Ph-xV*Y}ouci-CD+L}}79>X_Qj|E*bEXpo4 z-AIrKx;%eQ!d&t8efnGVqT5Px}<=sf_4Z8Hz?8%;- zrP>pXwx2ZPoltyJuB-GOYs~wXuevO5&hnmF!LdncngFv2k10<<|E#^w6sO;i^p4S= zCGyrlVX3P~v)?Oj%N{oA9G2-<^pY;~J+|$M*$pYoD?ytCG1zdrMd2)l!a#nv8L_qH{m zEcpKgUdE7ov&0L*JL|o^AGBES&#?dad%?>ohg)s;vYxytlk~|g{72UA^E1-+{#sv{ zv+8O1_IPWd`HJ@w&q_Q?f4RtRWxmZ9CZlR$=VNl~zs;C_>}#^~?_=yUQ)kLE&!4Dk zzcv3{u(N=nvD;iz2Xp3iOj^^s0Uedxi#! zGcs>k+<5x=@aC!dX>Z%ER=vpGS+~XZzgh>&qJZ|ig8fMi=H<7fUVhrNCiLeFQ;x|3 zM>K9Pj0kkSZg#=y>Ap!!8KqOV@>$Fe6mJs=aToLWpmyv_Qq8TZUWP^!fA&k;-__jz zRC~t@7l$ z4^LLJ`>Jo*FLT}L=?op`6|-d3jwGKn7m?6f(Rb+=+o@NVeR4FG9`n2R>yiJT+44FQ zKEB@f*ZaL)?Z2b!@AdDmKfio_=D&N}*Gg}wD05d!`24y4>+^k|4qpE3{BL{R!`b|| zp89RCe(!kequP@z*9}`a%G*~}x2!2FU;X^}ZE2?q3)k)2+r4U9lHnxpn6h0p%z@X< zugI_8zxQvv@i&Y5+7CZE|J~8AeOAo>k-hG`yk*Vjb47pk|6JgozwhhiA0;>J{!Wdr z+hl7MFTOTQY3DQtjYwNX(elQtQFEAN+M*sz7XNmvhiS!`x>J5v)2=dftk`pKT8f`y z-W~1CZCgHGj(0!ozD`-O!jMb$=+x`7$CtLHu8Ll{zGs@1P4W4HKOesDKQHvG^+DZ! z`L@KZQD=XBSoeC?GD}-qQNhsN_2r*0e*PT)>z4cMv#YXqC&fqRn%)2R!N%jp+vU5< z{q(m#zWjW;rkSCtQiq36?%J;#?la9;cISKh*=H^-zH6g(oO=G&#b(++VRmV8CT=m|{ zpVXaYnQ(w7>#~7?#kp@WVS%Sic$r^cpO^Fh*8S>_ zKhNu#zKOQny>5Es<{eE}4lb&BJZ-sJ!`_>J=Z5Q-cz2eD7$%lDFrMO_q${a);^0+5 zt*GgYq6(=;SpC8a*DMQFKKMYy;@YO*t76?;vR@4JJ69GhnIwsSw7 z-g!NC!Ad=UDUInlb7f>_A5^>&l&E)Xp&9d6CJCR=#q69$SHcf^J0~p_Q`n|{`Af~b zMP09?W_NKdbv+e2)8VuB?-Q3yb{9_h9Bo*(vUJ~OUNKfJ1+(opgZjAwbz4J@gtD|Q z%1UhT)Y-uNEH0vJ!pzKdlbK!EUY~i^lPa-$=DA5mvqeL$Rn9C{@4V4-w)a3!O1Crlu~<~Qeq{^2AzYaKZ0Rlo!Smee zNn)IeoL9O3zDSRk4D@*2p?O$enyd`hiZ9>XCq-(xC<9y@tVbRSH*@+beJi=zW>xG(5guMFN#}(AV=4NW% zmwK7aEvzJP(Sx6%H{bm>OS;>lCV1z0?DVWi=lGMKFSr?Z%n}dH*l*9jKx$?;*Mnuw zGcMXsTIo|8*tUgtW);W6D=}TEElG--3=KD1&B>XTCB+=*^3XMqw^7q|zurR|J?877 zk5V6HiK*ULD8Fo>MAXFEWjWW3)<@0PRQ@}oUFg7`3>g8_)u(*6`aI^Dp~GaVbaii! zSx@%?l{0x(p|&DgSrawSq#br)l$+6W(O>P*#Er9WIMhv$=verB%kDh?Z@&+V%3k`L z5OsXHzh~2yY<0i%R%hYlOB*z0PP}<~^s0%^9e(jCyPp-@44>z>*tc{W)7eyi?#?|C zTcUC$j^8|aciES{x$(~y z)`xwryFPv4{}+{k8?>5Qj%}OqD=2n(;+{G;h4YV1-pxKMV*bJA+3IxOPjlB<%fF~8 z$;>qURW^eyC)L<D94lR4>9u;!H04I8mSvMUbGK$i#mC0+I?vmo|8(Zb ze?~l8R~M;G?3%9Fo_eA-P=0A+;dRs4*}oFc>^!rjo*_K^_S<&}dH3u7q}0D)P3a_Vqu>%2Y;w6I#=x7cSNILL2})u*`W^w&USd7O>ZbamJomV z^qb>x3m%$s{B)aqy#Jmt56i>Fi@PKCc)l;$-0yh&c(3%?wlCidD{V@ysXHeqy~&v| zU1zgBTUy4f{zpHhRc%}P?ruHc=-Yp+bV`0=Wz(6gXSYRMjDuHm^~xl$@NM9lHv5Rj z4QZ!B^ZzZs=6;My)7Qyj2schJoA=zj<=t_GU?Vfrsk4PEKd-5D`)T*@u(^G@wt2k< z&#^bjSv&UbOF6}3t{Zb#=kOfI7kT{;&3?Rf3;Pmfd|1KpN%E1o911IDHqKsX7iZcq z<7+7s`e?Uw(0!_i=5} zH^2Tetrq#lx>#z{tg9O)UCxd<^VmRs7lT#1(xOcvA~RNUy#BK&^w62xs(nh2O&DdI zSOf(9KR6svK9;?QCE~)Ak2No^zxT{|-e$zi_$;n_P76EhWz%!43lA~h5b#i)WE}o% zXC;rV+kYpEhNZoR&xKlMES})-bp8*$Ktp*yoBkNF;V7QyCPe=WAY=-O*d?j^lv zyZ_|mE|1=?6?N?7<6m$0)#xfP6g=5_uXZ)(&Ew7MRHP>CHaoSVrg`nbseS3ISQ5^4 zm!9$av3)*c$aMybM?cG=mc0G-_WjP{9Zm}_PiDL8e8OG4{qW`V=?q1#o9E2WGFoZG zztlm5XZeHU%Z%o5HnJY_QJBQ#AHrz%=<%GiWnW7d>Ln`PS>?;4UAVdWtI$>^!LJON z3zZ94xj48hGK0?h8t7ZiecxX7{M@tS^>2Q61a|~w>n?pV=l8z!XJ^wMzk6)4Eq8Lz zXW{)n+y7s?|8Hmg@B4rG&$H(S&$+qKMa8jZ+O5kz_sW?B6WpT~&z>o-+}gsi`9^Qv z13M+NV5NEg|7@KuJ$d$QX=C9NcV_gxo6T2lK1=!a9nB;EK5l=1FZ*9_{p0@9*z&!1 z<-Pw#adIebiQRtxy}rJF`0A@U7V+LE6_+eyG}!#QQ^Q|)uj8ELrd9G~qGpM=4r?_v zGJoQZaDCxWeMGpyz|Jvd`HsHF+@+H*wC>O`SIk{zYB1wu>y(uHy`p~5NfPWe9m)4W8;yIE3S^|929*gbo` z$6Zxi6l1#DK`JZB_cb7g_|4|5BaQj-N3@dp_ITO;=&~_t9pIXmf(|Y&7RG9lbBjLTn@|Fy{>vv zDfc(6rE3;rqmxMFtIT6JPK$Zj#jang!pWyDXxh6qGsE&^**!Jq1yco>b{Z+QFP42@ z=vkrSbY7Axck;9FuXEPusWdKLoK?!8Y9=*1b>_6K=COW#oeqW@l{y-8H@!I4;ImS6 z(NmM9R%aIS%CLx#3?rovd{8j*Gg~>o)P(2ZqT|o{O1#q&HNP%?y+>DQN#yB}yX(Bm z|K{B0de3DWv#9gOi%q-dz1ecIdo$~zNYgAUpT8#0K5Kpzz3a0-x4o;Dwr51ZB`(1 z#N@FofcQJb`{@alj&>T#d(=-j{i3`|BRrX zp`7WfzXoDkBiIiJ?2h;*6}~m?-hLzP-6aef(;hb8{WjZwJMYXEhS?=EPGlc2cymtF zS99{s3pvg{O;s9yxewgfU@pURUgG{7Zb#0VhgY5$#ZCVk)_5Z%WB!SZ z<7y4^>2&2@9#Immx9;5Q(&%5BvA;j1zGgKOjfwB&{Bvh+OTzL^!G5cWa zr{cdJA7*{CdA}%PZ?}8zR<7Dq|F_ScyZ`vU)n?wd9d~X$?LG5b!`gn;v^9G&=b7p2 zzWW$m(flza@EPk)=k)(uqJtU^>3wt&cz@HH;p?{jhb|}FRX2InAp9uZ&rV;Ytn&Zc zyVskSn47+Gn8E*BZh!2}xGTrGCRsLKWpOxKaQt4}!zXv_UQK+SGW~rL7sJfZxZ^C_ z{kz`EvbC;Sd61#lTIm(zhnZKcskOjHuXBA}?Ag`X*2HOWVvqD4fhSWRq}e?; z-EsI(@44Mt-81B#{%UY~tMR8oc9E#Uf|=45<~lwn(~|c@zY|@ldBFJ6f>jL{4ZWY4 zHXpkCoVnq)%^k-RuL6H8ud<0c5olw-Iax-n&Ux9fk8!3F+IQn*cM9rGV*U`sWBgI& zmSASV6VGf}4xUyoyNm4JYtDbvIUV3z`~M>IV&z7$Q*(~3xhTdx$LGdG|IYRLs%j{%vwpE)aAO33b+0px=`HWtnX~kliJ2S1C{XYnP6!E#hn38c$z-$9! z@x@QdA{?_HE<=&2qi8;gfA@}y)&*zkl ze~14o|9@~Y|N3+HBCo!j)p3&NU7oS*m2^&BS?+rN$PnfVenrKa7DE{BTpCTacn@FU#*`_b?J*V_L)e(&d%YI)mQ$)+8VUYh>F z_P)87?=0(j^Zobb$-+DOFK>y^D?S(Xx@f1#_d*GOfB&%6eD0oqVm3;j(ryvU>@5(F zN>Elgn^H1C!DL1CkB-Ul@%;Ox_~n%(0u%D?C+tt3A--DV#u;gqrWDOrrkScuyH-vV z<34U9ej_(V-|pcv$0%(c!Q5ksd2cMzr^IaKobXD{OTb&0ZD(j^qs*$d1MX%ER)_6i z;teWlT)DE?R6w`J_|m*mjulgtqd6X&Jj-a->b7_0_Fi8B*6yx-(+ZpXXEIDz`4G_^ zrpY6>we0TQ``<3_|7rYw|L=#FpYQ+hR{SGNxr>Kr)c&upex1DfZu!3V*A*G;zdzq_ z|NkTX{jbi;+10)`KUVbf?dQ6UJljp88dXa8&cArhRC0>_YHLAw7n5UOkvcpxG?bUmm6}H z|1a$PsPK8MwvDms^Ov!ruM&1x-|af~(4cs~py{hK$?Xh}iZlAP51*aO8L;=JTHf}b zzy1}J3eVpDri?q_#zyaNvmVE;UNvhc}R5B3|FGd+qjL5nF0j`6@nM#+zAs|wxZa+ zZ$}ub@a(K_H)Cc*`W$%I+AqRW7P{KY?)3SDybE0CS2XnkY@Vj# zK}DyvCJNp>+sA!pk$XaK-z-Pt=E%j%@}>q{-LYxr!9BY~)6>Mi@6=!YZ0cSvwbdG{ z4o<3Exh!qw-I;+h4yq0d@^a=RT1}1FDyZfb7}_58cu`lBV0Wm#s(WXe)%w{&z8_Yp zrs$oy(lImB&>&#zwKy z#e6ZqZjVd?o8Ox1$!UA!t~8l6U(w9VewdmQ)i=5E%Eqjey;*E$+y6d%wZZu0K2uFe z9#+TRTk{+hJwoTUe%ajkF>SM-W?cSvRdFQ)w5e#h`YQF6#b&=5Zc{iz86T;YH71ssiq240d2wXc z>xkoZMjj2xGHsr^UXGWybQ<@_EOXoPF6p)j*SfgZ0er#NjAp)b;r{_wR#&l^2(qZyR3~%= zHC&dyv1t#Z;PnGLXEnX_o&NRlI{Sa8^Yb3^ZvL{kEWF$ClX}liiB~IRr!_7;_taMS z=GsSXSABN$svTEk>S*1++f6okE>pH^-CWf_vQnjLVgZl*c==4H-0lt8mLsgJwxv-u z`lny-F2|cmPC@gYWo$98Q(R(MdP6{viJc+EB>T#8W&OKTS8n86ajvys%E!wWPsHUt z{n+f^nRNQ>>;+XzChVQz|F)-4@P0$v_WBoWYi(|RoKzFOoACp$>O;4g^)(aI=I4q1 zF5RbhU`~3mjo&=o|IhsPO<)SuQs6rKVbbl$1;;+G4vd&_Rq238`ik2o(N8WcdGbsr zXhLjWh_sG-hz;9DiO3V20vzmon^$j*7WHv@;E}Rk*+n9&!9Y8!u+j9$LQ%F8X^gXV z(|1=ZiX~2DDc|?l--P@HLjS5}AZe5zi-fOqMAD%n;+y9U9|DNX8K8^lvUp_zO@|+q@v)Xo> z5Bq=Vn@{2sdG+qKj$Mk(zOP&B-<1Eovj6k)|5x9Z{kpZjZgun))#uAHy);9&=0=BZ zjdEQwWl|E$hN$(~YgM0LFUrj@Q_t;wYbw<|ZWh0@Z9~;(uFee%tUHc=Gwcws zmN?)Y(4@WXeoOcg>wuKV@+Ti==I@_%%sl1Qy1=HNd)lnmT{~*ILsGGc+u6le(Pr{0 z5d+nZ?n!54BNjNtm{{zV6Mw33CdfjKkB5Cq`f;&4t30PFx)^fwJ)6g1c(HfO-JHHp zYFtiS6NEYhnVxf5ta6y)@PWxIFhy-Y=f{hD$DKP~Z|R(nCdpu;xhv{$yKNQ8E`(o+_Ta{;**R{T5a5vae@;z_&s|Q=WZPof|gkcQIvB z+0^91$>J_}M=$+Iz$)%;gBde>nD!_LyzDjQ>`>G><71rf>cBSl`R1GV&#nmJxY;wO zJ+jHpoOcmR@ZmrOH9k=mrBof6zhP6_F!{uuv(a3_DeK-pFWb4ng~^zSA$6v8)w{T@_tu4%zYneKMIje4px?Yt@{1lYE(JIu`iF<3;fdIpGmsd#et~n6W zEgEuO)L^#L*{$EY0%JYIpB*}9Q||EmpTh3*_ZpUPaIFk;^qSHmX(HM8de2PH(*_d- zvIFN`{pulR)kCcany5Ba?=w$$>RZOC>^A zpWzg`dzFE?zgVBKF){GWnudukix-@E?NR)7<{vLELze@+b7vl$Tf{6av7&F$D~G2E zE_?#rh3q_R>)6&R2&pxv^s|12ZnKIvUgg&nBkp*vTv`^KJLwyX(C#f)%2s+Dw($GA zuV`~;c2;WfvC~)E&fjyI`E07q!JW2=iW677+ZrP9bk6tRb$0twudfa37Eu;#m1y$N z-6<9D?Zi!sjj8X%RxFL%Yr}Kc?r!p;6*o*i)m<;U>(r3IKKt9{DR(#V%@z>X2!5x3 zab970(ZG1C$=1o$S(n4$*1dA_)4x`0EVjP5rVytmrqz9Ftz_H6 z^_MLwA8d)yn|3zJ{94QA_C33fM`h2`F^N%UdANmX;*kS7tD;W#L{!`NJhnJ1c>VkP zW!vTN#+Sr)96bBv#nSZ4ArCmpKTh*#aPDwR*}7`R;`$h0dzZEO3>Wh*Yo5Ee$(lj$ zzukXbjPG4W>#>S>-+KhsCBdZ_6>h8zPdG#?@Z4}b6-ZSPqvi> z{ql87?%ztT?rzkcaKZM#VoL=>&og0erk2k_UbSyvV%@X#iz?rJ@5|aB zddU9t{fH%JHTRi3_uIMKAV2JN{=0qOl=lBvG>2nFZt#rg1)JS&TPXD0kj>Hd5qv1@ z{H1W?UJa1|(=r*u?azM&UNusF!ab*kYyR#7KU;Gdg+BD?b2^`qY-D`;@z$}HDUYve zFSW7l;GL$sJ-686%d&{|;o;Uo&q_Bb^0zD6&z+}tYukZigFe6Q8ATPStJA+dOT~)d!+q=gePmRy|wgO_Txzc$KokFuC6UHtemrKjjOusWh)uJy-}qF z{fX_zH(!)*P@0;0Bl==elaJ`TP1;MpA73rNpK2%?zLlx)g7zMUiU|k5$O+7vq3rpr zXj4?o`zHk}7GzrYWKL8$ae-fS*XQExi>p^M&0N0sgTZIjEh%$Xy*?K=uVt=7+nyB# zPJ0uBg3nL)U+&BO@>;4GU)}FHt$QQo+gA1z`pEpg8xX1dL}>c@4@?dYZ<6(6j%{2b zdDLvq^L?D*U7HVwXwPnlf3%ai)}rCbmV5mR{e4%&>1(g!-+5`F{go{~i=VyPwYB@_ zQHLTD|0eH$S^u%R?$^(Q;mg0bWXQBh+&uH|Q2eiJ`ZZgscWPIK z)&KEUt^M%E+&1(6thV(0+O)Zs_D=u%;ZcF*yY*#%|G!M<=RbGp{~!0;J9b^4{^#=k zho|>{HvfO)O8)$KySFNT^#7kZU-R|wX{jR4{^+Q8$JQN^4QJ z?@)lIoy`7^)vv$o{&$i6^3882_3eMx+wc2bZnxrp^ZV`Hg7*KU_x+i?|6%nCnSagy zpS=I~y8d0hZ=37w)>T_YI7C(*`1a`0mp5;?=l}n+Pd=IBa#?w8ZD6|2xifdRR+Y|w z_2py2y`zPp?{YX$y&l>dA1=&vVh zx67T~nZYk_cg8Y4K7Rh3Iai`WOLyPhSM~MPVRirim*Rh0T15)k-2MA?Q^oDq4( z`@diO?fs3u;eFW*hd<93Tp517vgY4K`JadXUy=X)xc^|oL~F(-R<-!w}67x zXAJMv@>?ZVx$^IOQThI|cFA&=M~_)mf^3>ax0G*)JN@uX?KESjl%LDy$E=*Gx~sBL zl8wE){mfL4g)s@uL0fbKPpxAx`n1NN--o4ihE_)No;)>VhDOq|$w=}DLntLr(dL~kYGqP5d5hKn>5UDT<4zlqP$ z;?32fbJbr$Pj^@Se0iCDe*O1unTuO}I|`T8Ub@4}uq>$fWWRk?+5dZU&sP1cu7BHGg(o&QsJ|HISse|?GG@xyzA_nxgRx7y?Pv}abm|FHSOA7;VkyLZFy zzB^tjEqb?O-wlJ^OVfKM1ljsebC+dVdFR02=zCc=m#ml=nD{rs>!Oq8;#G@|AMEGe zW0VyZ(b69`LA~|NyIR)D2ZyKlHwE*Eo@p0cIkV{OUO5K2J@Y@!>+2Qu36lv(VSOGY zGrjZpn~%$U+Acj@r6VM!so8PxhM&&LjH_O!I+w?6%08%Su>F@d}h_eGZ*F0WpNpEzEKFxpH!0jImI=!8vD!Cm{I0o(c+BKVIG4k)Q#qZvP+F1Es+B2gXUO^Br)nJR-ZE@I7KE%4 zQ|+9zx%UZ&8v|#xXeryKK+~2a&j7XJVp%P(wVI}{Y@TvmdzF{2P}2Jr8*G< za~zVE?%Q+2zV6Rk{KOQ!3ZqNJ}Ch>mX*Rnn3^2U|zl3N*iW(ZiDsBhrHw|NFteyEpZp@qT-6>Lt%6lQ!i>ET`eQb5G2rz6uxH zrcG^^4DUZ?_IhV&o8!t4E5Z&QD^7l6El`>q8*zM(q46%x{2BZEUNUWHdB#%C8Ms5H z_2YWYPMh?cZPyqzY*vYGIMK^soz6PpQu)oV>Mf7b68P4xJ($LpDy(9z?$%zq=t=SF zc@r;+E2qBAWnY#x*@5f%LB32o_iu^9DX^lykXD6 zz$fu44Lj(5>;qopzh7yKg72d#{tDaO6$p>*-xZ!V~IBYRj)M^RcC# zcp<>wWRk5g_vw*6N(OT*ADFf92IWd^2wvx|_xJ2Ou^pEe8dt5IkdoGH9x?Ak>@Lof zU5aWgV%d+4=6j?}S!3+1s8T6hwBPYc;)Rr4mpS^E->7e&W}F^+BY}NG{@YziLSn9R zYvvafZvOY;@mllK!TJ(Cf|m{2a_v`!mfX|0s?}F`>)YOz8IQjme!G3;v5L0k*AMTI zZ{Bj3Eo=d^(dv71{2X3w{JUaL#F|G%cikS^#@pSrJIZiGmKd(kTu;A&k zeU|Oxpmg)`h4=9sE*Bmh+ZOJ-TXdb+`6C5ivtn|ixC9bzWUaln@J&pL-ysOCYJzqBR9u4A4{}Si?`3Mx?*F@Ysuj2wSUU8JMWj-N=KLk{3zrM;oys*TuOj^@=$K$j!sp04J!y?w-*0}Nc z>F3qUFCXxolK+V#VgG)Q4&!BkjHbC(o~Ji*$p2cqnu(8Zsv&Pd_xW$~ZBx25ZzdgL zaoCU{pvEI!wfDQlQ;YsPZyKi--&!p4`fOjBE$^+bNiTCMZ}BePpv7s^Z8#w`xN_5P zgX?>u^s);r+*KNVR!n|tzh-OnoTVA!zo*A(7FAifJ#pS@Wq&DWk5~CDy_EZ*M;~so z=$P>B@4hDmad9!9>&(AA>fSu{|6}_PGykS9JHO}ahV3q#X15E@{8IX`_;>le{VSHA zelYL<<>P;&C)dpUe){R-@9+1@gxyWd-2Hc-Ok2F&`S zqxB!w|2)doDCk%_jnU$PjDN{4nfZT1e|$YXz3bnr`p@eBTK~U$|66_E$Fu2&4b0B2 zak*;vT*IjO`8q4#V2*{E(WcG2cTT=pv~$j}jY5JKzt^8nuY3Bs|NjB|@9S&6YTw`W z`K7UuH-BBzA)!*~;2bku@u!o^FW%Ys)_f1wfw*;dPsoY#N`O~&~fro4s zp1;{R53EYtl#oyq_PJq6%fzgWEw8+Ij6@G^WC?a?iDum7r)3=4#5!#uyR5WI=>Zo7 z$yEW4A?LHW`f4}c>=$Op%$g*k@cg5i9NW#3Ly87l4zM~#vwyD)diC6)Yu3-8RjXE< zKldmu&?#^Cw!6H|=X)m4$ym(Y_5MY(>u$zq4#Oiel5%EQOz$wiTadg=;o+;SXs_6k zyZ6h%A_v{S4c9a*r&$4*4tbf;r z5Lc&J3#Jx(IDVcGx_y?yiOE))j@RyPs+jp{MVrP9QQhZNf(mh^b&6q6oNav^w`^i| z5oB7iTD~>7J5O2g@{Q?UA`jY(9J%6`d!JVjJR1~x(5*v1<+=*v)9~NiJ3=1i95%`2 z6bvxu<#fKzU-kC#M1@=YOD{&Kzkcdk%^v^Y{pg+>-|i$6 zUmZ8gOt#2s&U?LCYHckq114>szx=!1MANfA+wEumx3)gHy88GflOn~C2WM0!oHTL! zHlfPcNi9iTXqxYXV?TCQ3n+13*|y}BqmqY8!$Uujq$ZKoVZ|IG%x}Gaes~%x9Np|2 zT)O$yt3ET6BQf4`MJu@`*7D?}8D~!GLhBN>XA557{)ENMeJEn>)H&>c`L3Q zIwNXe)M$ArQR`~JL03+dn>wFFB%|g!xXQjclBSo&qLsHXLgH%pMX3rEHKh#;vrK)& zBGp4rugaXpY;|+u?o-dQ7F<+a=&)JZjr;11BZ-egbkww-t#Wg@yfHW2aovMOTUveM zWY1>k`0Qmf?bn!;rd!Y&`pm%hR8DupqG{fV1*YuUSFZ5LRHa@{Q#6o}m}s#nL^p-c zynJq!Oy~?*yJ>Hx2Ha^9OVmgR^LiMiyl_TTS-QrY-XqW7M0`lG$duAJl8jIqk%DPq4bR{HacbMU2$%YJ#Tg|&szBG_v_8ptJ$V) zv|v_Za+dwwymF&vR@-kM)|85+4HCP|Obs_=cJ0Yfx^>O-z$rDM7ZxF=$$bUyr_Ff$ zD2ZD)rmcE*+WsE-n<1xelyq-@uk-Hcyjg6Q97G%i#UzZEF}HEb*uPuSEAZsr+{=Ie z71Vv~n)!atygR$gzn#>oows0_^NtP6H=mAlwL5&c#h5i=^E7|?hQ1eH?!;_cpfj`S zjQRFRhh5vI_gPwd=aoJjuy^7orS+UAHO)9{QFo!az8)koHe%-tB>j*30q;4{XDPezJlIG zzg?~zjcvPHJDWdQ2st*KUUkkfu=xG*himV(zkWF(qcd;g<=4G;XD_+3jXkAN*fC&= z>^9y@drQna3e%c5CN@r0cbHIl|EFe;%2c`Q+h5nWuG^rH?RRTi#K%{@6aGfD%)Ba) zQmQ{`h1lB6?bn^Ru(ux5I&3vB<&CyNNBWL%gExg4mtM78KiZ|@-BtDF@h6?br_40x zhVn4;ZQmVtA0`&xxH!P9v>wOAz2DNK%9s&gun{r2|S{(ohQ-WHTinznSxlZb8ex;c%{ zd$YTpymfos;-W*ETQV;% z2|qvo?4+}x6%}vayqmky=l+jtkH6c0{l?yJ|I7UUmH%tK7nwhq8TFO_|7H8QRdHX^ ze(s$5@$UIL$L@FAZ*v}=UjO3Y{|||O?*E&b|99U1H_kC%5)btMe{i?{>u2@-zuvt% z*!+9_w!d}%AANpbUse8M{qKjz_kUQKeqO)+VKe{vpqhQR!wwrfyxcGM@8A9J_kUX3 zfB$U%{dD}^io%)Q>!NLJqMH}*&b~U!JC#AgZe6rm*E_d3+qGrwt8T9keCesQrCs!c z=q&3CCr{n@5ay&aEiw7!a{ZczYpctz>BsG1*^{|D%1bZxT-s^N{Oz}o7VV76zgPeM zqJHhy=Azmc@AP+ADl0X`J#w7sw#jw}^Ty}l6TW78cdC`Das7*5=BD0$PXo-DUB zxtCXHt)3R-qO$DBs@nVWbIdkJ%#9T5KFVMB|M~yL`+u1KTlxQ={QPg1o6pakW&ZYX zy~nNU5PiX2;q5g${#C}=mE2!c<6xY)=iQz+nj!kkm)iEU%fE3*ym0x(xkRqx$36yj zXik#PmJY=Yk=LA5V%9jgOqpDg;L)XVb?>cddf_hdeUXWR5rav*Mi*rq- zj%e6Pp=lGRy>!_UVw#Yc{#|ZEOWwt^K|9vH_Vd5C`f1YEn3&l2pL%7ytKa z{@uOBQppxFzY1mE{yTiReBYPy_&NOgU+#AQ4!YH?DZVx6%k1lC_kZv1*83&?Ua#(r zcWhvCr~mxAi^OI3y;{)p0tr_`3Fv#j9pz$FAA6^8Bn%*Yw3csszux zW%tBP!Pmm%ifea+<)khRURg1f(j`3XflSn!nN+&a5>7HYo`j-anTRNL1Uwl)*mCb20A5PjUxaM~2!{m-%JUVCX6_tuP zSe**A!`?oyQq4_M94^(1hiWBuk2os_99p^QSV5C#fbn3CKaaD zqBD%D&sm;%r7_tu&aYKb?9QDvoo5aSFOizDH`hrxc&>Ab%(8@uM~y^v&34YzJ+!B0 z{pqruaUQ~JJXhwjVF4lY+qQkO^ zZPBE?rC%#oE>D{p^6==1tY-q<#vXH=OkI-r%%;wKz1u-gGn6&_^U9zl7dak3+2FLQ zjVY_jV{yg__Wo5GM|MuKoYio%E$ZxSBQfVNl@(IrlVi9i^S3f&?bsp8#@pId;B!@$ ztFig)u`I5%OF^y)4wHKq&0x;b2yO9Fn6Y!Wcx#k`4}+*_n$yW-$HHV03pd_CA%(3$ zX(G#ooi|1aHpWa@70r4*i}zH7$BDaY42w?|P5a@jWuq;T<#*Ialxf?|;@wa7K2EZ~ z^yAHgM}@LC;$rsuC?3`G7ZGERTq~CF@TlzRzag)$DoPt%t*YHxZOP|4E%@d8DaENb zGt+-h=eu@V&2+ckQHjH67+?G@tXreEe%rfyOH>Xmy|VI(q4=$M&wHP`Wt5L55vckNl`#V2LS@yt%tso%TUg_0l(Vh2L;`c{BIPdo^YM}$4j=AYi z=}9jw>%XhH-#t^Jx9Eq)2Qil>)|u)WW;a#{+pKG1Ht|l(zp`X=t!1BD*74dUxBJg( zoicnEy*25S-hrH5XKJDuABBBbvteiCfd`%KJvkeA&5U1dzUsZ$O8Y|kt)FLB>#y*R zn`pW@=IlL*DFD3JZi>zoJQ3VVO8b^a(QX&Q0P%f)$K*$y+kgO76ies$Cy zEpb-f$-3yP#e)@04!iv4eEnm2=G@s=ZD9tB%x_4kSMOf4;`3X>{=|p74k+F5?3#V0%>QG>p@(jF*YfV#{Httx zjzq=QyZ0<_=7gQS{CRbzmLiAo-Rg>{v){v~^Ir>i>Bi}4?lZ$F=hVxEJgkk9+SYSp zrUHaxcR?+YQ0p>?P*Kf9q&sdpS$|znaTG%@3zkTxoPF9HEvp=$}2;HI6d_g zCv4D_Nw}$`azKrTh3$}nL&9u(BSA5Nwg81gEk7F#S`<4Ll(V#~b@JV*5~{g+Q_$4V zn|r3O-+MQ+eDf^J=Mv5G#q&SEtDULZ^!xF#%I9l$hi|_=E4gQVx1q};#Y;_*A)9}? zB`?=z5`VH*v~59EZe-*mi$!mz<@&A4I(ss3^0O&jyQDY7m0X^4DP&D%@r&CpJ)F}M zB|m4)Uic>IBZoTY*}mMw*J7)06dc~dzh?=<*^{Tj@~dQ=126cpJ)FQY$-DpY;-i^b zmfKT5zSuIiSn65QGN0ZP2IoG`Q_18!vvT!GHdT{3(YaMj)m_o9$r+U!k6aXcqvTl4 za9Xb?wwOokL7LRI%FP#&+xR;;9ea$FnPmII{$8AsCh+yU|E<3}+3lKJOOoFd&J;YcA;F-~+_$P_@r#Hc!TW1fHZ48S5qoF*M&>@==gB^; z)wf*){+w_Slr#L|l&~fHt>3NVv3GooQw~3PYGnWFT7XgHro{PCh1nSl8LZaLIvca4 zPUhY|c4674ng|w&n->(^_&Xi+)Xy$czGrqv_wnq6gnRGG?{MlCT$0Z}{_)-_$*B>v?4tzWC|>iQoqoU{I1f1kg%RM9-n9ryU{|9!jo-2ZX8 z{l!J*S=%^zC8EC0{^98yQFNR2-^}?x9!*%8@$bXS$*b?y&079`-|vG4KH}5Ew;Q{L z+MPLRU$yDyMg9F3y{42-f3DW=99Fw)|IbsO&YrFN%=0byb$k8Gt>tx3pZsF`IX`0l zR(|<~`m>5uOh2za{d(W>hc>y#Of>E6?%RLE+WLvuA91EXIAQcMfbaSJ$xiv0k%}VYXq;1J=|@ zW`eHzCv%Uj$+zEieOu!0*=Gvt>hjVQTO|c}S7$x<|E8kyuXmT* ze_NdYxBCC@{lA{w&aeMG`+oG7*vm&MXRXrGHS;~&cGA)~kmKMT=N`@U^88t8ew{N_ zTONOXr5#`Sd;Q<9{}0!Hj{mnhLRY>n?`6%Via+j(wHnj2zMYgvtK^7zRlu3I&{#)y z+R@7Phn8ab2PKM~iuo_w`B!uAeZrgIGqEGv%!r{Z`(y`eiq|3|p`&d}RGqgpN3)9- z=esZ%etfW4xWh|&VzHrrqd?1oq&ZVOPqr2_GzzyfEL7-NIb{VOpN!;@7%ox4E+L6| zUW^yQwb^vlIL<6y+IA{vmYRpwK3BGq&Pye&2aY~VnYZ(;j`0109P5oSM#?^#8#Q*! zn$ofAp5+gN$p+k;XP#xh)cz%Q`-|PK_h;|4o;Fb|#;-aqb$>mNo=9j&rQeAj`Kafq z8~8pObkAz?3jU@ZbLEsznarw`H6N;E#rNF$7&^~X)G3_R`cUtXj8_*zW;IW~j}Gs^PVn9XG>JLJw1w)n}$*S{Xz zW#AW?e#d{}^h04KlP!{7&I>LLce3cxHb32cLoi1#`O-SxO1+4gN{dovMvq z6AtG-YF<0jpzP$g;ymXK3X22sudoD`p5N7RLrKV(sX5np>dexsk!qT3dPirttWC*g zS{>QJV6!yxM(4x2*Hbg{6c~31EkDq8z=!SpQR68G4HBOn@VI#<;_Qs0ce+-kS!jFQ zNaRz>tK!R3x)d_~t%9$xGlyZDg!ApFz|&dNPI`0(6sfh%>fV@UwZv@aDpvz%WrOqs zT}2#xY?eC@rkVx;R(j| zK^dybvQ8?VXGKM>DqYc$2tKK%*f1k@ZPfWE&F7af=lJ9uQR;D-@jU2)$+I=*uKwD; z<8$tX#Z70nhRjkIw)SE*z3Z65qkZshtB}r2PtLv97Bf#x@;TFT&V_jljjCmlBNxxJUKI&IUWRX3Tf7nn`2 z^s_C>%$?zVu~JUx{gkN9*&@Ah7GlepUKMq{V~8#^j`(=j?(Mh9yUp*hJu*3S-D~#S z30DOScrITyQh8H)l!N(p+~vI4=Cg!@6qB@PE<5R}lcjg)$j&Es>-OxsxnkDL*!cX8 zjGZsCjl3^TDh}Mbh-+`^8RLKh7oYWQ&pLOaPCI9J?vJ?4>%2=3e-d$eAZhJtxukFJ zm37zuEZ-e}X|1bg?Jm8yOdHI$9=Fh2b*5%ZoTSP$*-6U#T&l%6R1$X`6m4koJfL4+ zw4bNFM%B$w-Yl}=s8&(;?)&vpQU5M=3ViPQcuOzlLa_J+tCIBRoGTyxGnTe4YJMto zoGrCU-tO1s$yPaM?(er|R}fJ;8t8P5CDC%i8T;zQnU&k+Ic#oKsGg5LV$EIRc;ln1 zp{hssv(#;Wmdk(ow7mA$=7y~irYGDI4Zd;DHrg!rjIC(9p|jMN?-$&+i2upiP}{QS zNOHZUqBxJImECuBN9FfBW6g3epPSt*TV^`L^xX7sEqVSeyjNs%DwU7;Tr%*QuuS-O ziTKy6q6dz+O~0YSqrFD&%S>j*vv)3?oqFUk%&=g0c$Mwv;3dcvjOf5k2M zX4^#He53YaUBAPTs*7tMzx??x?c$fYGJ9|27r#s1`tXoxqw;(;?U`oZ!%nLn_rLaV zt;AQgv#sX8>lT~qxlTF0PQT^4alEcla=%g8nOJwL<@*-S@NQc^XJJHn%CpxO7_1(@ zYIs#(ZR&i!_psZ|(n~qb=Tqn8aJMY|ylE!Sonn_BTf;loR(=tb(?7c*AZONzuH>bs z^~|e;OCNABJ09Q0yhX>Ua7UHht0e8-sxax@0_i;2&v_rOxFh2gTyU`4Y?~~TSj5H+ zuanIWvpKBam3FghuU&cT<{O){`8FOrb&J(}z-(JY?`(Nkhx!7j8$nq_FZq;>f)c+)W_ObeO`Two| z|I|Oaxc7JX_3QWltj&LW+@$aE_Wi&AnVc_=ZQr1=;1I{Jd+p)&zn)(AzGafA8aU7P z=Z_-$+QN;$l77E9*>U>Yv-G-e$Ls!=$5%gZ&QG}c=gzC%wizL|C(NZpFf)GaNhTO)$ji|&j0yR{^uv_`d9sboj4xOzF+lSdjHqA@&Bsht*vc6 zUG4uoIUK|$X( zo9DW%EBM8Zu&z0vyu@{1n_otMqHRg{JCTj<`&vpmw3jzX$sTXq8EE@M)@NsdW&>;A ztG4lQKiJ7k6gB1VgqXS>i#>>75pe=ldKdK4_b$8k4M&&~ju7FKdT`&zU5#%A6oS zhBY${>fTwD`Bra<>->Gs>uw+H`Lr`h%Vp2Tfj`{(3k z%cq5No=sV|-u$wO`hzdG#oun&FXeXCY3G?2N!#;I>{?g)U1E{Pg~#risyo{v6=AU1!|sa11%_bqB>mslNhQ2ou)6SIniyUi9ZzM$wI zBVkt0TzdL;ALof)k*wMO%yPwUtjc_#;1FJ; z{5()4EpOKW-aA}Zk{b+IxmuSyB(kKPj*c-}Vw|1SDP(k<%|J_p<;Gf`)?~J6YP$~{ zG&=QFt8EkC`i(x;xBVv{Z2X<3r(@W1D!ZUOt;?`Q!6&9$ASsPQMwmnBT24~jm6lkO zz)dn?rmtKqm?kAoPV`*(kxAqEP1OxiW)+OA$z_`#o#W#bV({Y>y;-)ySgUDSln76j z@75Vtd*Z%pty#bA6`O*QYv<(&n|?K|e8wa>rE``*#Ac4`J;#EkbT)fj4*Ba^8dk29 zb}c9eCh&q`n(%=IW{xJStIZNUeCEl@?NlpM5^kB9 z$lE$iq~L4V)vFdW`B+&PCl+p*#N$>XF`2`egR!G$mdRW$g*TSc8dKYvv;$cp791;3 zO;h8}aXQq}r?Z$<$l>Thj>+cpCLg>rp=-vM#@rLTmx(5FNEa(CZ=0idCL>|uj83sf z#V1cLU2c%bdhVITCLQGMb}5hc^KFqp6)Su$0(4rr}(q#6^)BbXVcnQ&oKsVXP6PiCM`Mp=n1d+ zY#K8nPwg~YofbARC6rs}Rj=A2fhOYxS!teE_ps?SN`&3HbN0^ODUW6f@C80-XjU*Z zd2@;_X`YeV^}YiIr;HC4U(4LKGu6ap7RSOB{b?FkoLU~VNH}pYvY6ZdsA8OTR(JL6 zgJ(lFFJkQQy2eoM?B&{=smHV>cZO@s3cHM}U(C~eHK)hq6(6x#ddJN7vIX~5j#Ty| zIxc)SCpDi8Vv_iM?pc$RsB)H;gX+xtZDC=Vq1Y2368c zt=4SQqjqju&S$&n_g;C?^R?H@aJ_}Lj}t&MqXnZTz$`ABfXo+`})50-KI zg!~O)am6 z&R}ra64t6*uAZ@V)-<!0vf&rUq}aq^|A|F+CGL(LZ$2RVPdNOSKWt-&QzR?!R3pz?5$M{mIoIlh5kx z%3I|7gnx31QN!ZLhsE4yC8t#veUxnzXPU6(&dZI*-@dctPrGr}%jDdwO&2Q{r?qk3 zN!)Yue)ri-W&iEJJ#O&(GI<-ezc*bvg|Xx1@r%TgC%t(dPD)7k3C z>&<ztH&YO#Xg`#`SO8cJACL*|6b#^B=#-1`-WDAH6M)?UVM}H&gA_sa9=|D5d1} z$$QUzs0)tzxW)dTg}Jx$$JxT1j0<9K3BG+OZsw{JZXvf}<(lRrzeWAjW=loR`FwWb z#l|9!dx;BIDeLv=`yZbaG^6i_%LR!!jx%c#-WY2xKKyC%7Qw@}U(AnrroFpJ@3@69 z%hpX-XHQ+Tz2@qzRf(QEcKW4GE|!)OnWH50xij+RS4-~w+Gd$CpH5wHne4ly_ov4T z*~Rfp|1{j*T$=Xc)TOYWjP^SYsTMi5M1(Bq6F;B2c>@!>vP4be(pjtXB#aU_PrBS8 zc896x@GkSt2ij8WH~cp8`*LjFa_^~s1?QJq?Ar32Tj%?(O0NGm^W$3@%XO3a-}vt@Df)l^&z=5xL7&Z^cjr%C zDE{wq_woMZ>R;bxzYR=NJp1|Ivpu;R-SB7M{m)n5=Kb&A_jB$t z%M{UYmL-jUp1I2(i+}iak!_sp*V^*J!ml5He*SI$HTb{7zOS=%@6QsyZ8Q4@e`4*a z*MX;xK0UfG*Y3xQqu2G!?^hIkeEYq8|G%^GzgEuwTiYLB|62V2F2a%2&4>vrz zw6MOVq_c$8_k#9~mS=Vy3bqD@JR*rlO`D(mxcBkfvox8uuNMV7BhFba6>z$~Ep6J& z>9z48@-{pEKX_vo^7rzz+p$p@ZSNc#)TRjj%}!`m7to0nG!ebWTO9lFZZ?DTyMOz5 zzZ9FRKe{|w_4kv)mn;5#Kbpcj>E+G;$CvYeOO}{l_u>AJ_5XkF{}lc&I{f@0nQq&;S3< z{eS-d+5fLyJ>Qn=;GtjYx2pGFKP{1SN33$5Qo;isS%;>VmK}or0c-^gbCm8buw-QC znZH2RQ8(t|z2e9Tw#DU2id_f3tn|6NLv39r>%)v$!mB04R1`&)yIq{HDkyM=2HQ5a zO*~F-G|sLNab{RFOEjawA-O5^{iS)V2Qo4&8FDXq27Y?maO9%E#+B*~GdT*kh2&Z^ zq%pQDES?(}vGL2|nct$X3%?SueY*8mjDaO1Yya*OZM|&*vn7%*ANg7(np{!v|I1`` zeY?s}dk$@TY?68L{=aM0pXb#5dtTSoxjggMdw0{^+iK4z70-Nj<(PrN@@+paZU0_W zSNipf%n89+ednJXf0x}*nmf7Zy6K@!F?&39Zok!A{xjSEesaO}(n(eNn(Zsy)iWk5 zSluXC_j~KNi5oYsZdm3hQ+nTH`3a_*Niqz9GMze?yUb=jGLR^J!Rc|vZ$?PjjrE7_ zJlV2Id#D8BZ z%!=^tRW2!%I-#6VGA&*8Y!6HAGPB)6|e7twIrEoXP{>{^c%}*!mX>Sgb;`1)OZsaAGZazg%ygBrb zH~ZHSt+2W>Y+HW9FU; zjfkD^&(yGOWok}$b<=)QQ_(!_)rTj18;mFH)m3ZpFI+Qy%4W}EQ%o!TwK(wI44fHBqBc-=1lcBx{U564fQU0pVHE5pKsm1gX> zGlicgEv@KrX|b#}+tD3pprYU`HbF9=Y3IQicVef`X*!fA`}Xv~Kc^Ok%`HBgGShL9 z-qQFE;6C*q={ZWAEH3UDH;ne{{32 z>gKBLF)Yuf9Etah57u6*TfOC&-Ho%SkALgGS*rSH@=?>ip1Zthy&qT&IvO7wVo^3^ zykZf5+poYwWIf}j`J92xjuyvm-3XOm5^c7Bx3FQOi{@F&!*Nyz54T$^zb9tf$LPC9 zeD<$U=7bv}LRsSMC#yDP9^2RTikU;eT0kNG=avH=H~MEu>CRc;aX-6AtwgSTnSWEc zro`ot=_?8qj=Kj;FbGUpH*a6s-JM>?wrsm>o6it^>*BP0s}jNP{%76X?0f&HHI_#( zZQGchZXsPVJ4NKgEdLkNY7ZQ+SUh3o$A#SNwUhMxYwlKOsMeV9PR^OT)$x9^te{z0 z>IYN5?=^D`CY_w+Uv?vAS%`h>7SEM25mjsFbbY_M<>0c(pRPP?GwpM&Vt8`pO$3KN z&+4_DkJ`&h^`H4ITJ>e2r>pwqey(#LGQQkpoj2!^scG(obNl9Ruw2HzIahd(HUG>EQ0cWYz%PRSdmKXQBB)4O)|Q_neoJBL3>+KD|dUWqqv2o^X`LRg?;iDvj3Wt+ui-*PtS`96W1xO^=CZ1=0fVl zt2_C9>*I^7pGwdFe7*kl^$4?iUgN{rYTn_<6HA*Sn8eC+=OcA!==X-{W(q&6l6vI@RIdJ9huN z$I|cT-i@yL$el0q-QE0l=8R`CIXbZsTk7BW|COw$eB66|-><9s&o$@Ipa1`p{f}82 z>i(M7OFLZO*1hldgT?C4&+GO1^{-o(XIJ;B=Hl_`GPe^v>z~AZzxw#o)$4L)j%}4c zZeKpC|MzgdpYgvR^1mk@P5XcC-;UxBAB~yUXWqF|$e6TkZb7 z=I`~ox2N~~I=uh+^18IE#(#cxe}4Xa`h1(+J8${Q&$rhNyZ86Q-p4y4)?MqhmN{e4 zHhcC#{)hM0=+8VU$l38Q%XMkywSM^ol__$mEan`tg)M4ltwkR7?q;0l%RQsNPi)rd zs=aY~)0cH^d%J7hdn?^k3HP)zW3iXJ&-uP@>lghMHAhBt$Ac55KkY7lGQHaKvbCDI zVM(y=UnRR8+mkr2_J&5B>CJ9a2>g4Hu`GP^^Xtx@dNPdca~^e0O|1E~{lxnCIpS~h z|NV;p{r;c#|Ev3d+{edY6a|8A(>yiEO~8EXi0;M1p4eKY?Aw9K5yRqQNV zxOLVjNbLeKal1gLp*TYU6jxWBWOiMNWWWvjb?f&*)ah4!j~HGWIi zK6DFoxaM^w$T?`QSS{}o!9rdx{VCF0Tmp}@N{QWCt=7ZPBDPeaKxK;i@%8$@_jOL3 zS)`|bZhg?X*xNI*yng=Wx2ya0<`4h>h4bw!qr|Tk&e^{E@AG?A%1>)5OM}(Lci*j3 z`DJTvp8nW|$J{xsyux&Z`$nKEK?A~RM>Uumvw!P}r zIc_92>qsSwkm`b7r`QcK6?$hqlI8uqkG)V05LwE!m*L1ofz<7+yBH;BE!y55?J?;t zci@G{yWZQAUWC^1d3wt*+az9JIDdn9hkTZtPDI(V7P)N#ax=aj+j5cB@O)PW&y8y- zhSt_|Z|^&NXWFC)wd=2!sK0%+!_A&c*x0zWz&n3R^PY7g=a*UpgmC@~`18+(W3i(| zV~CG$^Um-k7cNe@*&?oUJgjfoTu@OnnET%!at_gy6&%L&htPRvwb(`Re5P+Z+T+u&@1uNqU6z{ad|LT9_A zECS5RCx~fiJ#TXU!1sCDHBVPYH^p#`2gfw>)_C$xjNE)R!S(thCm)Bj9g;_8IOH&~ zdX}}SR5|*+1={Nqku(N#|e~qnp*z{{PVJYc}M$Scv z?1$A=4az4mFTQHDsZwq6)W>cUqgZ5m6djlFct$Q%wYJ)@VW+b7WX2_t%@ag*r@ihz z)xA+lu`0ESL($_*aM=t;CFi@74kfdsZ4J1yO1yh!S~B;vEzf>S=}hlwiko=XXt!j` ziJ1>paB*>&K3y*Jt|(AH*JR>?Tb&t&O-Gi67OSy4t}b*wyTNQlR%-dol}l3soJ_bv z4fqe7^}Z-^@=WK7Lods~2NXm!D z;!j!N^(n^UxsImaIBzz|1hIw)Hr+X^W|tfi`Tt3b(*&*HaMR))Q7>{|GDynk+}L61 zAewsA+g@Iv)Y)m*wbb&;;hhvA;f|?Vp zQ^L*bI0Lue7rmCZbmjC%EBDS{xL)aMd&RaWgIP1>o-O{h%2bwTm4Wr2Fni(i-*&(K z<(HS>*0b?ox0RTmMHq`w{|&h>Z`~Ms9X|H|HnFTs1XS1H!8{aD3fA?-f(#_KcHS!A9-fwR{E^|XAaaQVP?eAx& zG4z=~J8bneg61QIV zc71W7`*9ar6N6GggU}`0IRXW(wcXd#Uo7yvaOBdJnXHYBTzzdCoFTFfQx3dT6BPcn zE&f$Y_TGT2th@M57w><#F8=35lVs-^6C2!2avt)3X0|X|n!7WSeb!a`i?4;ZFnWL7 z>*8ajI(6~8j5$ROJ~P<%^F8o(Dqz^N)b^(5@wZy`~c)LC={Z{U56w_Z@6i zcvrCR$i?U%pQJvsn(-VtF{zu0Z||XJF*p6 zlnWN`T2R*bSa;hx;Rl@NP5an%7?vi!Q|h0b{^t7)78j-B0tS||21l2^pT#P*#rwwcUc zzmV;@hwr!Pi#>d}>V169x_8wT!fz*qYqrfTxYN)7XjSX;)JQ#X&82gWpDoxrKkuFF zTwOU?ci-3UkU#dhd-ZjBdmh6%YtPyJ zdU2B7f8N8Bn;B$wm9VmGFZB4~9AES0{GWUFTmF1Bzi2dX&-OPTy!U_or~m(O{!x$1 z&+q+X|NQgs^6B!YCGwYjF8bIx^~=eR=6d>J+<$&p$5;Mt-#^Lr&kg?ihZS4@d|hAr z_u}Er5{G{ty?R;vdH?6h`XByy`_FUXRPR=1P*qnpGhUa#dSve}rz-SxY|E^LLdht{pzR{yB~&yoF~^#46N z?yvv6GH-TRlZo%;ZMgz3#k&QVI&R&tsVcEw`qTga&ic2_pZ_+CG89yY9b5kS^8S-+ z&fDMk#{JX&zx}`G`+unaf0F;_*mZBs@9*v!Z$6u5e17xM8z#LSZ@yQT$FILWZS%}d z4#Cj9zifPOpLL6Hlx$|G&O|e!Sg}N68Y~`|BKk9iOwF(?b5V+t%)r zlGD-)T<#qZUHE;?Il<4z_BL4S*u6h2+@mjW%`9)@52lhUXD7QEPn^-*`7vm5US3Yn z0>5{0bKPd{;E-|@DliKwJ>cLt;ayIvE5|cam6$2&OJ;pewovKdVz@T#(3-pe#})+- zpDF{ka*hbuMPUct^+gsN=3Ypd!W5b@K`(Qa=k~??6O-CG8s=`^wZr9zk7Qh+0=t6l zc3C?n`SIdcT5)Bi zu9Cs+XP-oVyyUm5EGYcyDa^gVvbpuP$Hnh8Up<=NJ-J(2ykw)P{Bg_an_|BB3csIw z@Af-0`F-Dgc9b|w`OO$}g+V5bOQ21-#xd5BEm?M#Wv+6gg2T)}r9LqM0gn>Lj}mh? zn5(c#WCW{UU#A<9=*FSY?!#>-Yry3gAd=cBdO-Ot?{8LFUBj~;3{DIpoIj<)T^Ivg zvo<8GSjez9+g|aUg!tK)Q%ab51p?z9>{!sVOTHnnJbTU;E|VKG?>$aEwd}-Xz9(DM z)mi7HN%pMCO4i9F#ezAcejFYnIWx$nxJ8%I>rUrw1-sr1cb&Yr6m zTv*Np&zo!|UOc-@GIvYGtCctNy^YP=F5Xqy@G~H2bt|ibhvxE0S2AV3y@=LkJ@;%z z`(K}f3c07B8AUJ5EU`W?RY@anw&Lz8&Ahk-t8>Xa7cE>d%QsT`V%Q8B6&;zQ1*<*s zvn=fx9h*9iC~STfE63EW$QL~GbI!W6DM|-7g_YMk3wp8oehm1#s3|kSc>(Ru38O~V-Tt+e3qtP`?pI8p?rI7|}| zUQ)Q=7UPU-bD|8VMe)s$)VkV!%u*nc^=-zCUd1?pwEQdGyHqznQYiX-ZNfo619zP$ z7UMvJqARNL9^A&3#*@B1n7I13G=q9En`g^ejbpineAAdezTtVUVJJUkr=Dr=(Je+3 z-EylY{8;jW$0j;u{_;gxZ|;dMVdLscYn(9S;G2YqtCKcgF`sU*^J;3yyJP3#&g6ex z(I9TRK5_-yYO${=VwUa8+*dM&y$dkCde~`AR{OCnT=UPI&Du6+_cW(Rml#;AKfJoS z;8|J15qUL*ybC$yxtlF_M9)$y7Jg=SO!2Uj0aw`F&ei{CG8UB1VBu#h&bqBJGxn;? zrL{d$GmP3#^@lYxwi^_KmHvttv~0VFx8^>Ld(;M zUI%P`ep6%2u$~yUV7}b!nptz(ZugjkU7hvsslM_n{kb6yo1dS($F^Y3C1uBwAKD$i z{;yB5*pL$Zc&^mR)gN=tT##BCxcT*pbJ_7unrG zv3}F5+Z8{A@9IqzvGuW=WgB9U6|oBwNc`;Sa<`=!lslHya5M)F>LHu z$2qI`tB6dYu)u0t&IX2*T-V(#vQB5^mu%HBt2)@z{`PO~y*!Stvx|P-2`ny>Trb7k zC>6la5Y&5Lc$w(Asx>lc0gQ$@cUxY(t&J{`?VsG4(5cqAZ3ox(FkvRITN9HO#qRH5 z+_WTBoU5hg0fY6xU!8NFqd($<}@yjI>uVk^jzH{>q7aRlt9J>mrfNmYyZ0P zx92$@9R$=T?ALcK!bV#U({&6Ywf^$W^Zd9;=n`8NnI zexMMZ=%Epy&eqJZsbNLCqgaH30vqFvWabGC>AvZO`zJm2Je&}H%uB%NRMExbGHkoM zIW>4@6(y+)7CrEam$|ri%XVJ9{zJ8&XZM@Uk2zcW`*yF(yW164-gjv_v2UiW|9|D^<@bNruCEP#5pDm^xc`MvkQ{{6_P z-Fx@_w_xFQw@+-$WQwq{tll>3cv9ZG1Y`S*y?ce!;=^h`=SD5fxcKnp?-}|Dl5-Cm zeKLK-sk88vC9|O5ni-NE)4Ev?x|B(7r$@3afyi7wJy1H zr^;5V=joZ}pS{jM^L+msj_3FH{+K9V_s9I-vHc$x$Nzn`{@<(X49U-C&z`+3bH>j1 zzcXjunLE=}r6nP4sYto_#9341l6f{puKlH7|FL)fzx@C6|2?gLTHhuix34&9%e(tO zSh=`2_U^R3`9@8qxw7h?-n99L_cTlED8?$5WW0O$Y4!t-z6Fy11a=7jX| z@uR}kaz6v41@@YpDtC^R>U226V=$$Q!6nFGwOFe727%Cd+KOy0M`GNsK46;=ot!H! zu(-U3!N{aa-6s5_K1UH?ylt<0zB^ss&hl-PP1T2=YE3V0UhOrow~4aPI^6#` zsjx-5=l|lrKfX7t_y5ebg7d9UO<|?s@-N5!UhjIhZMOJV&)g)H*OQ;^HjN40x+$^v z=8bIueJ{9{wLEdqf9*EACz_e7lTW-r+c!v%!=*XOGfUS=)xU|?Ewr7p?L@=MednYt zUzIxUhl#idHGg(sp~~%-F-*L!Qo6Ob51c7#%w*ma^KG;D^gABBn(1e6$`lLE zGZqv*9XzXicWz*`e*divEXq%dYAby&Y>PG6%JZhZa`%3Q3rAa9#7>;o+wQ3!nY2Gf zd)1e!WJ3VIfHZe)JCXJ$~GN#3mBnL+c;%+g$x9q?dP*M(Ky z?SZp0G-n<4E9%iXQ@la(qm#z1m;+(5AACZVC^zX~wMH7sX~+ZhHDcLfWiA;E<`$!HH8_Sp!;f*bjy>D+l})6IC|udNlE(vGhXW z+bQZw8%)duqz`&N5D1lEWlLpQl=Ps@S0Yy+@%*zhA}4#-rZZkty7kp0Ju*eZMtcb( z!`jT+r{X=IaF1**sb->HOS>@3K-D2mwbvGkZ zroCA5jN|A{k2eY?-=$mjdm>HVzN1<|`^Od)$F0xsX6J@!~FML;gwSL~$@9KxYzL-|@KW6?Z z?cI8u>D8}g^g?E8m3`3I$$E+V(nQ;c#H|~57suUmTHN+wSe!YM3{lf8PjrErf>lP z73R~1oh2RHnWVzMybxk|(eth8yUH((Gh7eZ<(Nv8Ev(8qRG4d>P zODI1(ZOf%*!O&3K*)ug|)&()2tvzy%^@2>n9go#f4J$Ti*}OY&VT1fJE3w=qnHSD! zF?2fcFX8HSn#h`_!1N+&J?oX8?*0#T&1q}rD+f3l9(b_$P1aVMA0?Mwb@iE_d$j57 z?~Z#dA-*lsyZ7;QvIIpK#69xz>c zmHj|9QN89!WLKX9+joXp0?RAc#OpK4ux&VWaLe&`yKL%{f9}k^DNwy_kIr9_ni=01 z?&myP=6EhmGS8qw`tjAW+g`z&?5un9pYPK+_{g*C`EMiN%Sw6=pQ*5A=xIb1)g7!n zx9jG!^y$~H?>^n_vi$Vdl#nl&uDo}(Yp2T>Mk<> zy7=SC*Qe5)ljVCv960tohqAk%`DkN;#vCj*`*nB_u|{l{-3@7 z@7VNnr=LacQ~$Plb&>1(`|)yXuN_xE9$)i$>*mReANSk+NSJlG{C@qWpHHmQy<_e6 z|GT%{?ZE%c`VZ$$_U|n(O#E?Vwc&;)4zBO7$`ZJiHy=NHQ1BW1t<8a|e-%BxK4IW~ zBI2UKm$l+$;5?assXoDUVnW9zX0906tlF^H=3~>)uh-I&O`hb5)-Ra02hUt)GmEXyM`qf_yS%$! zzx6#C=aVYs%#v`}uKzR3jOMHrj||Q|j=7%HzsO-`@vI=XV2hIu9T$>r^mJDD3H9h4 zyRbF)`qrWea!M?mj#34IzR3%Z-3ck4nZ}WMSz>b9)ugXaPjw|64Jm&mnYU2NR5T)R z?M?Fz1LJ+(kDSR1OW^&yqAO;9j-G>% zf zd9M48g+ZCcoB^j(xWw-6e)?u1pMLh_>2nvP%{;(m&U{!=!^Lv7;pVF|7U~nRTjm%ZVik}SIjDOFY{(H4-M6=dXaGT%pK+l!D&t{?8|Q2E@f(- zEqUke+IvaPC&NOtkHspd{YZGjA8J~Dr%lJGIP{JB^bkYt0}q2g-W1?JeRjFpL$)WC zEG`;yRY6~w_}kxw_`WS$^`_y_owHZ(`aEB<%I96$x~oqDw|;sP$I2O(*BBW7P#_`g zRh;57hPH@{QoE0`2scV|uSr~YM)cr8PrsSWYZ~5fTL1K_hgp^66qgRPc+|@ef22<;p_dk!F?Jscam>O)5xa(l|kC(@|c1P>n2+;d**Y?K&9W^%J6OAvgb-dfG{;Bq4 z!F;7xZKq~m5WKTFWlB=UtBM;6Q$6e2uYP_zT|{Qa;?5=pE}0unvKlgc6PHYEb71q= z6c$~UptvU_L6+frf>Gl4Dc{rdG7>Vvm9~F5GJVd!cV)WkYbHuQ|MunG!^I{V=)+C1=Z{=N=8gv{yY{9viOTs9zrm^4eP^A6( z_y<4LV(uKyW^j{yy&zy>C_{zgy$7vcb`O8c>Z)mIw+MK!B+hwuc&gY$<|(>GH%d#@ zpX#wCEn;Eg-xU5t*!FhL_UmPej&iT9XKmCx%qH$$wCmv2w@WQzayfW+NNzCQ7+fc@ zmNCJnH15%d+t%MS50!k#-T&9jgJniwx=m8k0X4~B?bmE;7YZnFs9Wql=DN4GYtvP| z0&&TLFiF;PF48+ij%n4%IXTo`y81AC&bmroCgJ#pl@q^I{G7Hc_xV|?HOmsUFKHTu zt6z=25U#Kk3CGi^yz@*s&Magq=gQq^^UOF+P5kqmj3hz*2Ofb- z8BR<5UnyT3-BSO5`n&%xr2C&GHCmp1Iom%@MgM-psr!GA?OuJu&ceFxQtUjRO?y3V zzbz}SG|k%L?DWnRnt(Me3c4doLfC5!?CV zcK*JP#}@zIen?Du`OYauXI~!fpXa05|F~xBO`qR?{yqIYyXNP!lef)2bIQd%{u}jI zZpP`LotvT-^!Hh%`JA0xYUenw{`2hj|JJ^@>*W1=Pv6cedTz|S?eAOfu9wpnJN$B?q^L*v%9$kF3_3A(Q+7BmpO{{;mRX$&T z*`GITzdIdW-mGI_ozQULi5ttV;P(4=FYOIl=g72dIljl)rb+M-7UbA2!Gxtqs$MB=Q@ewMq_gJw*4c2QQQT6W%~{pL^8zQ6f2|M&BMANK#c|64!q z-kbS<&Q+gw<-cLkXLw8^Su=J2{cwlmdCx27&0h9wUQuk{-Mz1Oy?#CWc)y?Huk-&} z>;K;WzyA;W@qD|yq=!$#XaBZ$wd&%VeKRg*UqkwYM74-xCz&M{t30tiWO%~BhTZUo z#AQ>b{-0a{N0J=^*{-bE^O9}aLP?o@`8`d{cX^y#7G(LhdMNP-DYYLy$XC(2KHoQK z>w#=tA;Jh8{}^0s zH@`jSQ<_XP=kI;V4}cfOu)`pUrJ>hsEZ>E}C-Te^Nr z=G*P*`uyhYjXrG)uO|66tjf~Nc*f{_Qs}SR$|*VP7~ZF=9-C7-kM~5}oyHi8bnXp1 zKHYr&H0$ewd(Ot|eLN?wnyc^o;!3gj?a=#DJcMvAoz7Y4WGL-0ua}1@$$5Q1le3S8#n(1A3C@BO z0gE=Q?lRigaVB~iOFoZ5ON!jd*PoY)xSaXq(xAAi@r1~cmZq|8l5-R%He@U_C=|7r z!7Y(?l;d53XWNTu2VXg^ObsME<`~=Wg{- ztTx=v^Yw7v7rg|ZmX;RD(mh*l^Rr0hDy=LqXG%#Aw~moCX?Qp3%>MT-bsx?%X)N=u zsbMH~)LvZ8VeJ0+K*J5jk4Dx!OYD3%Fq@UdmFpf@YRf#q`{mC4=kpmP{yt;1v;2JA zTSjtD{&MFdqA#xb=RbZpFO=_*sQUQ^9Pgf2lT#^Y<$?>c=BP$YW*pl z1+EQ@Ya`Z$#4}as-79%9Tf=2@_1_)Ee>IeB4oSy6O5It}sIIy;Il`08hC-Ht<%&F$Mmx_+&%X=V4lY_uwtM|H{afA^fims^BAv0^oda9p?TPC18r z>HdNZVb^<4?O^x0vvo>VW?{qXkauz7rYY-k6HjQ?Zu%d0{O;d(y?@JW4mIzM`~6mR zm#vei!Xv*Y%w;!!r-d)gi8A#lYhRIHaPCFb&iH_x(KBi!O12-$>a1?saIo?ai^Sd% zrBa8LTW^L3t}y26+MTZbHCyRGPht1#WUK63H{ zBXoaO+?~!e(Sk>Px{Z|8w9I)6>-I{AyBFF|pW8O)l*MwxrFYsFS!FaXdp+rF@8ud( zzuRw$`3jWzR)@NucAYuxsL$P*k;NNt-!ge_wA`fUvjxvI^^iyYb zf6EH_39h?pMOWPVT>EeT-5dTj&m|@HPkPio!EmPIw$I*jH&(Xci*SK{8hDCzHNPMY{P?f zq4NT?_Jk?C@%w6UCjaooxYpBrwcB@dXI?g5AGdr%$=pD(dFw0xHp~k4HDC1Vv*N_I zyqyJ?P4-n3*45NpDUH3otyFK_s?}FlRc*Md75jJUzsW0>if^-Vu8@AoRroNGH!ovX zk#_m*B~xbZd@ufa&aG{^v(%QF@Hj7BdH!?E)F-QEeNvp}#4>&Qymk8jwf{f0|Ed4) zX#KD9eV;#GJ?neF^!MM!`*Gb%qS-Gr&)lD{_Ehln%`4wicX#>C=QnGYNUV(9 zE$#U{wc+iLf6)tGRQ7NAxNPas=Ntdu_%i9*`qRFz7b{&z*PG6q7PfhRzF;YHdPMx+ zExb2-=UnxDA12%`-MqTu?kdG%%Yeju_4iByn~K-i*te^ltKvSCvGm!32@_^FS!M?6 zPyG}0vWRO(*2OJ#eyF;QZA_qxnSS+yz~XFjhCv1Mo2(l9I4*jT@$Ve?tpcTO_N4}#{l>@1oc?^-OO z^EEX7;QXqWWjEINE^1-ke`cz#UPfp@o?xkjfa1CWN5^~0uDN+LI|UMs2wW(S%eI}r zAzDEorSWTnZiCkr~{mk2xFGW^cO)J#QULmvR z;`=VXP*#)a64K^9S!v7GH0NF4QNBVU6=2xgu}NhehwhU+;Ud=AD7z zc31IT8V+-emghL`$bGbEp&3`xmr3z08`!;h9G6_`32Z#OsCyr`(mTc+&E0*GTSc@T zlTKQlw{+3FdB#~pZA#K1vt94*R282}3-ss>R`k%Fy_j2S%91nFE}EpBb7(U54(hwm zGs{5ym7_vtX5DAn z7915?l+CHM^?{}kbNjKfXPVMiR`526hwU&f;oIgZ#k=H`WZR@<;RA=5iUa1|b^Cgc z_vRi!=0`D2jjZfEAxktQPAOeXOq+IwS=@bfg6_0N4FlnD?JUO`6V7t-9gGWoc%_5W z=*)@qh`@B+j+m``*fyJpcLg@=RG47Mz_;!sV|qo#nFmn}dy{AH{qoH2OI2Rp1~oSJ zmur2`=sLWcpSWHmRB@We)>p1yyvodX+-F-V*|>ApMA;d|dHUr=O&dNKRB$DjF`s#! zGH+a@<;jNFp+P^m3uOpSdPTBZs^UR&AFSeaOn-^{0 z7E#>&aJlIGwYj-()~$=#)Aneeiu?2E)W*F_yyMGci+{X$_V*Tt$sHN9W3e~x-#sbk zrc?J}#dH75FXN}q?KYIi`nK}My}V|3`@3JY?eFJa+r*(EA*8uD#A}|x#76C~^9jux zjhZ3~^wOU4Ntio7S(dbor@`jsm8}nR3X8Vxo!Mn}h(WA-n}l-xsz7_55|+mSmr@w6 z9dFVoRkP2T#BlUt$)1RIE9sRY4Z?FwGgLpgKdlygD0ok*(qj8^#)XNJ1+}T?kIHVMXtRl7=Jvel1siU}JsX)L3y`6jypD-{qu`b_H-?^M^dh~>S z3wmyquaL;!`>MU+XLjYifX#=Lgwj{;wmQ=zlB%n|w#3Ipp*8OG=S8V^W%h3VvGDn0 zG-^B^X z-%K;lkGogma{0Epe%$GM_EoRl>)wgKTl?F8-ad&o!(#=tdwH>|JUG@BJbOQp2VNy+q(7oSM-Y|h#3>;KL( z`}alZ@x|Hpe|JUQ6!>i?U2|HsZ9=~bF+Kn z3V|Eh!bj(4UOAh{Qp9}w##}p*mk(cEZaGo%>+ltwy=SDV%-oX7*$oX3=gbQhR(de| zY}?G)%OsCqyqny!l~=CKG2%>|p}*~#!!ZOJkG9rju(bp)za)YG`gI~%lq)<=CwDz*#GeUf93rDJO2NgvekK8?{wuJ=vbDN%GwZxMI$ga?U1GbVN%={WK2 z%b!p8vIO79m##@){=Mw?-P)~zKV){SwTn|)diCEm9p7S2*Y0OEC$5Plnr_Xi64~Vw zdyUs2AxPW7Zq33oUkw#=XI;?N6z6ladn(2ywu@_Pv(+EBu4O8VU##WUU#xQ8H@5l7 zgBoSFss8u(NByiU@hCaUQvNtP%o^ zTBqkO*uryTON8@*?Xy3=z2NoQ$6a}vAB1f=6`wdG*ei0@HpAki z^bNCw!+-d_VwfVKq~pjkXQo;jTX%;`A*O|pM z`?i})5|0D-vASEV@nXg1*^5?9wM-2BwPvDc^ArcE6Frj!c#N*6&P&XY;S1&PKJhN? z(qW4utREQ?f?lyQq-QnvzPiV0sC;ZvQAoF99&hvF#GYq-K8+V9X$CE1(>tl)GqHnV z-ObBM2RPPDx!rKa#5Kv$%i{?50!6OhYmPdJzv^h=Y-O2cyjg+cr-Ie&IT{+1kDoZg zy|C77mPn(tqVe*SjiS<<(kAN(2#N?De!+3mXYJICMYpFoKXhFZsi7^Q@y2*!;D)w@ z8=B>f(#s51`?l`3n`C{4_1dJ=Gl%9XiY>Kj)i|bdWY%2=-p%iMwcS|^n*=k1PAN=U zAjXwbq`v5IhhYli9i@+Nb}7dhXg>HL!;sh&pemThJ85c?f^l%FxXRT7?7L_Fk_lP4 zOw2VQUo2pP81vMes4cVNG!MA=tYM!jbjQHg)8$po&RYu_PTct#rXG1WE^GO#Rlet3 zw%R|sz4XC@@U!|SukE;2a`)8aDydgTF3wziA^Pm3gOMS=TRA2Nyft@x;=|*nCwAh6 z!wQoo&y-Hyg?#F-PkS;roh*;C6iV8+`N7I5mTb+34{klhtJrp-ByH-I_d)S%^gBPl zzh_X&94b0-nKn zkFNS@%Qz$FMsl=nw3lT{z<$AfDgw)k8vPl*l$9(>mt<{t$8WBAFD>D~#KbJU`^y8j zPhawkN231O_wA8M0U2N0*IoG?Zt^Pa6bEDA+B>tB25w@>NS1ZTa(%69-Shc#hvas? zH*Dsgr<*>s575}>FC1R*fc@m-?36nX)w)f5@8p#CL|wg^?)rH9{0{=_Q#WhZe7fns zSzhOH`^rPz4@DmGE@nB;u_nCfbaTTsfp;G=gPFTta=x4U;9+a+p2N$o+1`v1kpDb; zh2bQDvdAAoZa1e)SnMYg<}#=F-1fe|>n}*k98zIS2y2wsvVwWz!z!60CDz@$mxjnM zd%5!YJYL~tNv#g0%#PdLjtd04FSYlMmDZZFa__g(J3d*Ry>wW^M>M?b;_FC}lTnLY z!rpD*5t^(v^-+}I*IzsqE}{81Pfqf^`$Oz=7;8t%UEWot%lOs9CPW;)%qp<3ci#3p zB^h&V%$g#ZGnUU@AOE-c`t*GL!kW$dD}C}ERl9fHvr@YfdtFI))`D}-Z;%*d+y>@>(*)Lq(uFe zxf;E`Vd|au7goyJ(ZY`{dRAs7&snH&A#R&>%w)NkBYf=gXXYHY+#Tt6^ysvzUB{wa z^V27kDWwM-IidNy>RQ9Pl%3aH+qff_2_H6j5uC_$M|a7jye~65R%EUH8FG~`Zco_F z{QgI?1EtdVlvG!Hn6N%jDfQ3*m>J&#RR9gm?rK`>PwzCNkZ;?R#m@%x&8pUOZ%-5!x-Xpw;@y3TH>jZu&;(gv$L`6vBk3~%P;U2?>d)iK0of)*ExFPi=+f1 z86JMS7by}oXYR+7uT0kXFU#DqZBp^ReK%KB%9WjvRy*|0mGS+0^Ao?H_Oxfj=lk^w zs>Z7a3w+DG{xtUTmaA0(d)DpJDseyQ)bNjQ)@6Zv^X5D5>zN(A=b~-h=b!%jEB{){ zuiy9Kr}_VX>;FCDul>o*@bBLGf6v_K*L-^O)AjNZDUW-lXN$J(s`>PmfB)av>Hn5) z-}mEae#__k-?z^G+y48x#{YAd&bLarw}h(jrM*&hI&pGM*RvCUug$#Xpzm3cRmJ5X zV=;YGZK3m1XZC|ap8gLnopxVmt-#T4y1AjlN!B61y!ycP*I&~%-^?-Nm$O+hp_C(0 z=J+is!_x*ls?T@-loAv^`S0iC9Jhr_Ef&8Cbn935KAD@!Vi}P#@A>od^IFqNi>jgo zFRoHae>TThaOo7SUw5|&p83r`fA7E6$@{;=|K7abw(igD{a^PAM7U&i2HgHMC8%ek zT*}(XT4^^-dRO^fzOv2NYv=n}`Zez^{(txXhy0)H|A*_QaG_AO6|4UgUOfg>L z$>h1dpM#UU;+)eSp19FB{P$K?;YHn|u382!2``ibvS(02ioA0PCb+}}!e*rwNf z*?wWoO54(U!A~;ITbNb;c(ueC^2$7LIrmJ#I61g@g|%a%Zt$fAE>lkm%{a-oOf^o@ zuOKPtbd!eC$>UMCeLfuUi`!Xe*)S>e?rtst%R9F7^8(r&j-1F^@zwFg8=eENTO4~; zoTK@>HaT#}v7g~J-TQq_vLn-q>>t|8S4;fNxVZ14>Hn3#)z9a*y-uvzC%^94$L!Vb zj-IRkyZ2?)?9jg@u8Yq95C8xA{-49~wU6ijG0y*aak|{!NA@4T?EQXk$F6@{XRqFG zdA9z~-hMH2^EVIFj#)13T-ZPD^uH?O{Icn1o;&QkvEuQ?=3t?ZU2_aXOC5@G&#moi zdacBH_7tzS0#otq%PA?nWvr@qh2}Gr^>DZg7|L!|T2QI_ki9`6+cS%MPFU_vjqer` z$99J`Ur$@8Ys}+)e8=V2tYHjuyxoL6T=&24nWoirZMvzHiLY_r{#Vxrsl-_eh>5XN-E1sr^c?)b3T!Y*=o0 zi5^W+P1h?>U^=*<^4@f#9o|||YkGVImMnac5@wsAzS%N)j+*+^<3Q}}fS)u(c4Ef;6< zy1!Bhdl2wF=}A+NPI@;Z!|G@~mw=CqTV`zD`F_?n7M;9KQ^zJo$-u)(4p(iNir3u| z3+vvHVpmwa#H&&3-zgq#Ki1c+Ti!j6v0-Dfk-pN^@}_l)INRqXmhUSHc^0iXv*Oi< z!oJ*(k-TdUB(qu1hRS6E%DSzRGnvn@PPwA7 zF#E-cngC-Nm*30gc{=7CJ^Di;<){(oYVpuS5oYV}v&_0?{`BZ&yqdBy^GaIl;k6lo z+~-7=t~!3UtCP|2N#?A^R+SABtwB>-9v+-ATKv1}H<@qz9% zjT0en7pR&pnY20W`@4X}20jnjSMh1GS87gsb79WzaE`s-Y^Dm;dpMhfPna}g_v)+q z-wsO38MvR*T6$&8npr~Yw{2d;ZZ~(wj=R4)CC_Z0HmNu}j_+lBs8Y7uiKr>wEJYh7 zc8a#`UUks-u9i~%rWS)!_ZGfe7P)n2YH@h8@v4WZA7&-y@om*qjo@h1QqomwZ}{Q= z|Hs`4hhAmnhFxKrEXJK+Ipe9cOo~t4iG7o-pA|k83BO)0ad+XtV@#Lli5sv*Z~DS@ z;Pm#j%C8TJ>6p51$l_ULb$6y}i-(_2yYPZH3_6}Q*ynOSM% zzj^l!!Hw5u755qbF;MF0tC#p!c8MDIow_wD}~aBNnw;-s&xzcdBPQ*Dng zHa+J(Gx(L%jX%M^Co=W_u$Azaxc^}DjbBeDzWcfEmNWxnpv2lef0tkRcVAxQ|H+5# zN*oe$a=W(p*|j+&p6$zFi|3tN&E32GU*^A;>3_dHc>84c1bd^!d2DVTFQ!cF;qFcn zyK>BL`ub9VJ-M~&3eh){U&j>cygEEdSm(jQ5XM^AVMlWLLtF7N%e zud&9~*|%ejRlay|R~9^z-xkRf_Pm*~dwu-vet`;u1Lj|^R)3UkJth9jGeL}-VajW! zm77%^+-ElmI80!7mHl3%{^Hz65p~X~{*{N#f9!Fu(vp|nGQF-bU!Xw$JA01eR7D}q zvK&tV#(>C2OwBUU>OPi?frjTlG>T1FoMe1)pGRrI0q5g0CuDR#s@NRJA=1%tfK6-T z<0(m+rdJ)jRsM$R-O=E!%OX%z0de52K4^6vKaQn=|FHv1#E(+Z;JeRU2IJY#ee%Zpz z(j*}u^1?65)4JmGvtZkGi#Ze$*-}j%EkZqSwAy4Yng4B#w!#fp!KdDKD`QlQZryvp zb9ZXBVC8pJq0X=~M+%mxJ0w4QV0rv-iNVJXzDL5}Sf%b8YJVwOUEy~mz~jli-`78L zB%HlawrTR?w#|#!KBg^JaN{Xiep#s4Nh(#pgY&N3?t9f7bLVwj`sgX2{Z{+a?@zs* z@5`S0sc0VC_KtS~BX@B4T9qS0@0VrIH8_%Zz>`_6L!r# z9iE?OR@s9;1tw$k9!?AD&G6zI)2pt>*8O;M=FD%qySe4p&C4HKJdX+LI+OE2!t!}h`F`bhyEdQwTUPb) zsIU3E`to^2mmVECz;tEb4KDE}$5w?u8?I*8u1i=DvAVH3eet`mJJfUTpZ|I8wCSbf zfcL3K-f?@}w^Gzr_hwToND$j`H0_WpOv=SPzg({iov9(uLxR%vYTs;|qnQ|@H3 zE&6;Z```~Lg%7J7C;Kax1zo-SZ^C=N;&ZRR?kc^q@|6Kk_tB(z&$s2v|Jq_Nv+vKz z@cKXE^Z$GQ|6H$k?BC_{aYr@Mu9-bt+q&=VndO;T(SO@rs+3P|ExlB>d;gvY+qx}( z!vFjKpI`s>|EKA@kH21?{r$_o4^PfFS-g7N^QFjV>AQuCuXDXq@Xl8|Im=PjV}Y>6 zK~4_4e8atr+zjW--IFJ8mXC9E|KeZxulq**tKZ=g_ma2t#TBi8_f&T4!z-H3?^1kt z(v&%Q7iP1lX7TU-MJM^y7P1?2CUJKj(MulWG?+xnJ?W zTI2Y`u)&Y#`|HZo24iIJ}&qJ`^MR){4&N%LF z`?f&YSGjwOz$6FGMbUfX5|-A;Of+^`c;?@Ph*qVgCw#W9Xwdc%SoLIEM#x3ux6LjG zgp0RnIc)P$>MJ@B7qVmNV-Y4*x09!0*Ge|skovT1ir31jQ&DEqD^?zrRkHDY{C2J8 zmQ9RDd6FAfoli@?>#Y1}QI>(V&*y7<+$6iAuS^pCo|hbGnx&O4t>N1)yC6a6VbHS} z9hY}jVYjbFHFq$st(<-P(wzRdKqm8p%eV6Gl)Ry6TJ%O$^wip02YH@sbKzSm;C-F- zu}D+TW1Hu9zIo(I2qv$XI<5Pt7O#2x?^QR}STR4V>?vL9l`J{4BS87&M}b2^IcZb< ze{p4s@$pvQR=;_o^tWdRW!v1Yzu>WMGYnv86ODDq_V6-cY+q|VoArE8B-7-b&RQCi z*o`V?^RXv~eMz&nwmQh0Aj8c#3(3+6gxRN9D_$F4FM1^$rX6_{$R`ivH{Ecwxkl|jDE8MX|m8W;% z9d}NatbJt$=DD+9oJcRrURSK>l&UEd-tGE%maOi(`1Q8>OOHu(baV!F%}GjIfBDRU zZAUW?{c7Hmw=Avhm8@xKs&im?^XKT>GQx*euKWGtW{7^&s;G$pXZ`bDoZ{_vRIi=! zkZW}m&!K>jjW_1am-bp&yz<$<36I6nE;7sslRMLUZdsMjv+cPpwQshR>yxEu6;k8tLC`F zgFj6zY08_&wG%n4_ST*C{_u5EiNuu!GL>Hr9~3!ofSdi>Hr6Ey+XbW;Tz$>fY+y7< z-lBHswmh$~eZM%LkD0AaiPL<6y8#AgZ*+TzCx@Qhxn*5=o|kps)D?X)I(wxzJb%1Y zgx}zseaqr?4GZ=k2sT%KrsKUW{K9JCM}O|}F@>ZwBq}<&-H|Vl_VJq=edC6*Ls^-+ zNcF4P^_w$%h3=kp2*|qo@r=(h{=>N&4H70Au9~`j`3u!Drz2%qd#aB=t?l{ff8+cN z#-QM@*CIUJ_2;x!-`RWcdH>hh+H2G$`{$o6-x8ClBc4<~Wliv>MPXMG_FdWBkzi4G zWcIc_hqBhDum32i&e0mvH8&;qfKVHErG9eOGoQ!YH!9YzFH=hYCcjepw zjBaISrB|;K!p-w$aJaUvd-Uw_Kj)4}14T2I;P)xATkXTr=82T%u2z`aC@FLItVp0# z@4@doiWOSd{bG}&ckCp})GZt?N(ex@z2Ja=|@IIXiuy02Jj^kyTsln~p2 z_ZwqB9=ckP)otITck`^GLu1(0@4J`lKi7BnS^SRGa8A(!E)6c0oxckHsNDGa&r~ty zLUPw0{q2Gi1(hyY%BA{T`_;(kaU%r|6u*I!v%k{Y_D@=vwV6pS)f>?A~^NlHg=Ooe_z_f?EC#% zKfZMO)8n^){JDJp&%cL9cl>;H(p~<`?fbvxzQ2Fx&f9P8;p^AW3cFK2xA?_|`~Hd5 z`_HS^|4!Gx_VnfbU()t9KYvQUx-28ZT-vcxY0C%8t%n|`Z76KGBAUS}m8Fs*+9&+t z)7nMv0v0_GiEuv?{z6ZY`I!C#(Kx~I7xR<%9eh}vEyvBJ!nk7hZrRooXWsAsUk6$N z$!sCR$8hf4IWNtnSLS^F`)=R0Ph}#tDq9SD0^F}upO9K1v7g1*DQAv2)9%}U|C(J_ z{+PNk!sJ!7*U7utYhQoeb&YHL;fVi_?7uGmcgg<0_SGMs#nHnVoza9Vc`@d_in!D}JZTa!|@ML@DxlQlNTjTQHGc@HL z;p<%P_C&@Kxw25>)#_pbSLXKeY@J$OBK0=cA=vTJx+9J~T1o7y>I}5_ z%#FK5&eXsBdDQ+-Z+h@Y`P$N2OPy=-Hj&S}1nSbZyI+m3sM~w~r~S{hyWhV(ZvT0C zecrdqyZe8H|C?$5b?@xk|6jPr|NFT7@%bbA^DOzff9r?e{~2R`{(trU8(;5!uY3D` z-_Cu1um69w{vX!|``x=zA8h8I|KD1EU;6w%DtG1TzD0j5I(YkKoP+)Uf5#eqetzF~ zDE|MxHP@>YuPNVCWC&fDy~fhb+RScAN5)aH8!0}5&fEpE;U%vUk~5g}ThgX7IyY~0 zcX4Ey;u!d6L24so&&D1`zN^>HbhT&LoB!)`N>y~e=Cv(9+@aaZWV+O?2aOLJylx_W0h|G_%r z*Fm$2^Wqjd-;^|16Sn&3FP~FdZVb0G@JonKdrIt=?Ro19l|J8jpDixTQ+yzHw57?i&SHlhv-FynX>4 zv!+FziL_*tF`RwY(5<+8p@^#PoaF5fWGAkec5-@5dK=fKUs)wOA_vy=e(k+zcxG$= z#{)JFAvxx+v+|SXCE8q{6)GFJaYbLkG=WXmIZHhQl|uLh8ZRlRa{5bMby}yfB-oJI zt@qu+74lhz;mnhdXiX2>7CYq?*A&H)hO?pPCmT-oKT&X_DRat%J>L#K>-pU5`q8;` z7Pm~OZP@#&brU?I(qfI9X7s+jBo?l!?5N#!ScpNUVV}5n%I{0uWyOJ;V>Yh4xAANY zPu_!6Gw-W+BP7CByNR95nN}FKQh;~C&4cY)9}2U&D+^Lq%82l6xOQvN!ZQp@+FX}Z zaV5@Kx9n<^s+)kG(+b{KZak@HKi@G9E-t;Px$q3t2b*d-8Qkxt5MQ* zy5gd`hKPbE`$NSHMfXk?{_4ALTHyb`T~(rKW*WlM2l7*Mlh+=9|A`3Mce#H_UANAir)!dA9~nTH`{ID~mW3jwQ+NUv~b~Mv>py znt291Ecb4mWi)U4QussC!R|2s?QLN?GEd8=xc|vXeV(vqgTGe#CXM<4x4+9RUim+* zZLr%azqLK=T=Q4)KO6_v#=hO?t665qy8Z3nb-a_K92t7JpJ&wepPu>t>Z!vA-{)N4 zb^ibA;w!T=*3am=t6$4^JYv1wmS4hCBG0{alieY;KkU@1#8n_7~DM2iMT{;BD{3%A~Mb?J-j4hyLcxw3VI z)6#Ay9>;*PDTcX{O|ti&H=nwaJX2ux77?krt0Y1dj4zzjTR6Kw?}&l7>`50%yTxK_ z!#}P1#S`dQbt57C#mdSC&S{e*Qm(z_t2idF=DGdbEWuYc0f!d7x7UuhJ5;^?W2M8c z1HtZ`5>bnTxPMDI&Ca;AvQ68wMn5O+{Nm}6sT(Kw9;>KgHA%dz^p&ag$Ug2H=`8tA z_p;3|II*HZsw+|SNrY*{LPn9sI}$TCFrL<#yeQ8_%PQgN2H9=PBPa1i=UXwKDn79+a7h>zSUl7boaGyo`9{) zMke{Y1NeI9trR z`R;sKpP&CYxnB3vzYF_U_8Tv+fAjZ;_w1loUp(W|xayzn|JEJrrBqW}{5`w=x%u_g zPrshMn;u^y>Ebk@yZf{K*NYqE-@i}$8*RTcY*YQG=leeD+x>Ij*R#IlvZ$KG1xU>i{k3OHO93&<`hm4*U#Jk^X=EGiT|hnzx7>j-{wEo{brM=&%Xcn z_x|6_^80u0(p|dm`7gSrPl-wSy~o*VUUhlA(m z8|+;qz~H{OHSGv%?4PFd+c!u)?S1X@I{(s-Nv74S-)+zOX6h>I<0a)^`{am=PC+rJ zkLwx7fTqhEPp&zs;}c?nxi##W!D9uv1Iul=mq_0AmCanJ(Nwi2E9-Ve6Gymy zibrOH*A|YDR&zHVZqA5U$9p2BN(P4BGd6`5Y8+H;<+yfJ?drQUz2J-2PTbw4mB_j^ z>D1CE=kwmL|Ni)L@z2Tkr55dtwETDD&VTp)-;*XDw!gN&yzQa$u0Mr!Ul)I@x_P(G zxM%*qJce>{N3q#g?f>)tKe>EW{JGDG+yB>;N8HWJ=f6C|cn$N9#h*jDKILp(bvCss z^4}Zz%ey(t&8xk5H>Q0mvne{V+%$jBLN*o0H8VuClqM^28Wt8>@zQ}(`RT*zF>21OM0Eop~Dy6 zet+yNV8~)QQS$AlM~`NoH|Ob!jc|+Po!2ID@cy0n|4-J*8HqXi{8$k8dCAfvBIbsA z;hK7EO3Q4f|BXFqY!aRIR4X>|AtcPj(o9ZmYqAF-(A?D(6u4jM}}#o zXU9d){%pNzA{K62OhmPHnk3J-?@lZUf7HywJx$NeCGfd`lZwvt?seX~?=ZrZjhrcE`e{f#&Y>h&goD8qsD$OE} zrB)q>jeNTzdb~nQ9eymBV<4{aV8S^MUGZmeGRw}$ZrsJtwZh|^M_kwPlbzX&y9GK+ zwAijH25_aWTcve&nL%{O6}=wo4=WPVzfKBWU8&$KknqrA#>QyBOLJYY3IDw;X#Sd6q5Ei`kT^o23&Nake9uq04RZNnfsA zD!;kTNZqjhZWzugExgkG>y>XOU(FCy{km8?U~hK*Ohqq5(Few3gneT+2%-2IJYKBFLr**&Usm3FXt|-oP7DNZO4_U zVm*V&3rsvgr?#ZfiSdd0@(nmK%5Ptl|9Is2Rk;!9Pu&t)tU5 zsY4+%kMZiQIY;>>>@MoRu-m_}nR~U?jShxqW^a_$ZfuNMA$iq7A^&&Mmg?(KGgo{J ziz;Y)xgyhO^WoXu21_4b+q(AbsTk)+?$rUiwKqzdZQZzR&W}a$&sXeUykh%3iwpO* zwLJLd_kZU~!%1Jcb!4tSJgNNR;L-O!cchj)*f+n-p=Y0W(c`WicND%F?A?3YOir!k zUS@53%#@c!cDbqt&!~uw=X^HcDr78@ws{jx4&KM{Ua9MzYJu`PG&R{U7EjteDOw6rtA^v^s5RiCo3p&7Ef$j>uWOvWebzEIj$%^ffOo zY)Gtp&(JEz@ahTIj;I$qUMrtTyk$}2|IuV&b>jZ*N3)q&ULJHeh`yPy_u$e+D$^P! zcpv2Kt;(uRHxJe2c|2MA_=h|V37wdfdykeK-{hX3#S^3sy_W8n6EmtLyzK9`|K@ywe8wz$=}8{X^9s%99t}SyG%lN%?(XihI~(3 zM#mZ7y;nYYv_-RG-_g^}dHyn`@sFGisI^8Y-Sl1Qa!hKHO6$6}Z`}6pIjrqi)z=@9 zv3mEnCgBb3+!J=N3f1zh=>e@Z1TNZ=SR}hgaPBoVDJ%fj25}Th(C>?#LP2 zs(6bQBSQof86^yI$$YAh_^55AFnpi`_?e&*{K*8|62q|RP-q2n*#Z8`5&z6jfI{o8+W z@fdk2imnPR-F3U}-Ro}iif84ivyyK7y|?=3`l&xE&x$Xdk{R-2ZEfnRou9&YFL_Wo zDZZ*N-*##K-u-(jew}Sz=3R2^)6>(}&$|D8eE;{Kv$J2DX}{U+((*LvY++&TkDM19 zUmSX6#rI&o_U!ZT;_}{q4_IxOb3f+gzW`MgcaM+P*6Sa7dbg&w<=56z99ffQGrbV{ z|M!07%LD%(vp#ak4}fLzZY{gW>=krpk4Bx$>!hRulzS%eD&dX-_k!` zRo|}sV(*39lfR3Z=kwYBx)*+WVYu?8V^hC~nb+PnT&4-=|-T{qT+5tY4>dg)@i!&sV<+ zX6#$ZGOWW{i-fjFR5qFaJWe>wE{mP$> z?{Dm_lC8aW=_&g`xyz^HOViZK*1x~S*eWdDy}|h4of$_CvWe+?x#T=aJND?PoW-kG zF=yv;8g#fkT)=WkO5o|kIoCeq@B}Qkc(P0Q)s(#_owztw%PU(8^mQf&DhpU}yvkX- zK3HkdqOgGAD~r3W9H#nP#JBcYd~sXhvC#U=ty@*znldcn*>@OLEq?iJ!~cJ)U(4^= zb$iW+UX{sp|5&>J@2dH-zUJ#q`@6Lt>}~$vjOBU$@#ptruj6aKUakMV_}yZ?f7$!@ z?%(`y|9}7gk%Bd^AM?L^_OpHW;q>@@zh>|Mm3{hfMe*+a`)_m3*R}fjZSvJs-Ra$Y z=T2)Q6Jr)#)dxUA+W zipsX>mF-Ni&b*%;qZO*b{)PQ+-Uk*h;X45_2L-~J&G+@O9kXnon|9UUMAenUhoux= zDk<(tW8u`j@}O*r&%Mr*GMPaQ0^(_HhfSAkk=gL-(9fp18f;dZ-=%T+l+FzJ?Yg|= z#zK~N(+>+=UVQcfZ(*5~KyuLe!m#USrA~idd1T-2{zI!nXMGZJE&sjq^`*=1&*NiX z=k4k~6YIkxEY>WnY!o_SmSoS&NEHE5hqIloJ{*k84kP$^swM540zo%Ltr+CQgowQXoqLd4DB$s)i-)YSt>MB43AFH z?#|46`X_Co+vf>|+jx|%wQR-qn~JcPb?sDG=(S|h_m(Z4+yQ!1XV2eIwX*T~q?3Cu z?R+F9!8orZh2w{ezzyDn#RnHGd~>j;<}&Bb(5m%YF5mq0@aw*~)Qxw)Oj;jw>*B&2d!nAb=8_aXU}(_%Z0D3pz0mBF zNAJEb|9$u0f>qngqtmzySvYIwFVhTP%MzA+&Fbn-{hdb^DO{PgbxPF3cgKp?7VBzG zxHhZ!>nu@`HD@J#uO6GzSdewV%I$QEg;(*;od%jS99-719y|K5Hi|XTtZ>EFX(b_> zJ(Sc>Ox?>Zz#F)td&aWGD!lB{SLZCBcy{J}$K&(Y9FNbh{l5O|z1a($UYY5efipT=`JS-dSs&*mUn5yMnleKFB-T$+ko+CJV{zCM zwNooYWy*^`UF&D%IMi~tNBZa&!+n>NLnpY))cwEv?@ImstlM#?tn<}54>|KpWXP)( ziT@wGFg-$lqh0RTHxDNq+O5izzkT+vbLnm$ORb!=;}Z)eov?^HzQ1nG5{>N@4=udD zcfHbB`{q~0J2w@NeszQ38KFz;)=cYu`*+=x!kre~-LCg?z30wey1`6C)#=!hJ+>)1 z`OCY#I}IgGo-fjR9T!qqAMP~CWJB1Sa#a?um5db`-P%nOI(vc{ZvW%+5Z=Zr%+oT# z@M59)*Kb!>YBYR|ZrbFonB=HyULh(NZXrC)=a*9b&xJJ`4&7>;{-dU~cgpAW$-kqx zLv3EgG&oo17?+T`1J$CJJMMC-b1zs|I4??M{uRI9=G0G^}A0?;>i0F_stB(oLTzU`!AmTW!G4?qczWe(Uz~f`aD=$ zSv@WMQ_F1sMSqy{oUdDVn@su?yX?q>(q4VNkBr))DaWIyDlt@lsp}Q>o-=B zcdi>>cq`U!{a56~!i{(C|7=+)#clQ6W~RE#mn_Aai)(j({=E6~m;C?W`y1!f|9)TpfA=m!KH(2fKK{JB@Lhe$ z+|z&F&j0yC|KBP7ef4tpWA5%f#VvO&zV6%05{sIDN41Mj?~IAP|E+dcQusXS-H* z%iQ+pi!KRg4ro+7`u^{^)9)Gm=RdxB`u>WoOGI|-W$iz(V1EqXZ29vaD}QG2*NFc2 zy*ssky8phfPmY$yXP57*l-jmFEWBK??C~sdfsE3C*JbKVcWhZImU`u^wOw^|a-&=6 zQ{`)|vv$6F&3bO>+0{GyzP<}x+WYpqiOu7}Z?EH|DGE)|Bg=YKDy}rF3aa{@5XXChVnn27qKk!*1K=BzcM*qIeUsvg)i!TebO10 zHqix*Pj5Ya&Zy$YCtz%RHR^a_&e08Tf6UgpmyzY0z^`aBH|F-FTYJAA-ugc@ySBh) zt8P`a$qFgs+TV|l|3A6D?)(2A^?&)hK2J_RSh=<0+jEO~H%nZPuzAJJH`L(ilQjGF zO7p0yO2dqj*DKvQJ}_?y_*`?gwRYCQ?cdA$8j9WAGj4PUlU&*R^W&cU6=MQRDm<_ z&sa$x#hRp{pRjn;q9(o7EC%1S4BQ|5xiK;#>^?osPcKiNXs&qf>uLY^ z|IYf$5^Fz8--NomwuQI+a*5nTLB1$!7VCqKI~=VRFihf6 znE(8DL#pkzP5U-$Fa~jj2wkbZIpdXW9*af7@&~FCOE~Y^e&ddFKX7nsRzl#lnD32M zt#S<|I!cO6TR+HiTK~!Ido|a!kNNwJb*Wd`m{^Wk=hrfnUEmYzuN7FA=2Ob>)**hk z8KX8&(*uiZH;se(&+cBLo_22Mp2aIYR)(GJ5O7t70_9z&Lwzt?%MJenAxa_oX} z_|ZhUt-EIWTXGuk{Ys)p zPxI$y=-yYAT#pxo+)M%&96S0qxr2-U3bCJD1j)> z1&urh1sfa;cWzQHlsNHjm7{bQqhHLC2N51RtIk|pu*Sq;*Mi%t3`(Y&$!<*%-_7pP zuq%l(MM&t%qPc|vSM*kCc}+bK#j00(DZ2HZT*)fyV~jhx?nb( zD!M2A_EY0-k2L@M^$Yj%7VDFmrlNQ>B0G2Tg35WzEw}sMweVfF?Y8W` zAFo7(OJk$fPb)s>_~gli_K15|u9t4eQQ3Vx>a@51_uq&0uKzqc<#f37fgK<6svdui zN#5@8&98Fbaf@AHt{YFV$(_!zJAB&rT>0W_sWD5A?3@yCAm~B$X~wNaUgzx2CPi)y z)2KLQWGFqoa(?ctY}33)rJ74G1)ek9u~Kr5!-OX)q6>n4+ZhI~yLva7XVu!z@78|Z z+k57WPTPeGlR|q1n+_@|bVT3pD=R;C(f7XVroF#67rxE;I76Sw;HJeVKOGl8-?Me? zeQGK1a=1cQpDD`T-o-hAH=d*K$FFS<6qLSeu_X!Ro?hFosdn(gy1jP-nfRr5CoD6c zp{CVa%VYg(g{#2h=Jgv+>MB^pUETj~=JgXsN%6VdZW|;lXJ|%e7fvqEtKsnu-)3BW z<@1K^cl+KauHF22`Jd;n=a~ z__V$F&Z^!A2YObWwN;<(&Z*+;-(I_S@445$E>>-PWjg{FJpOX6?|76ir;KGv&(&|d zVSha4oi&;@t=-5nG&aF9AvEFP4J-dfOO7L4%4W|O&l0+1{?WI2??LSYVMY6%i5hNf zW&d9pIw2q@O+`sfqNekxT(qm{i=_uHoUiawvc7XaS=%F5@$m`nNS7{!aBHSehSpVH zd^QXo*Z4Qpb^41}?VB=H$$#yxZ(sH_KZ?^@AS>7*d17Z+h3L)KsRzHA+9^-)R-Jme z#N+k=t)p%}xiSSq{a8dW(ny-@%JY%@HD&9udzq}}!LGbtqp0MCRUZvRi zNu0~nT!=htX_p*XGqL-Lt*MFWK07HKnX) z2Yc+SqQ2sb7w5{gF4zCC_-{?di^IZd8w@`?9r}~@&-PpX+~rTd8;gZqnkZcn)wzjT zM)TgUv)RA8&a~|?Dm@=@EL6Sb>yN#m`+o@6R*1d-@I&Z+#hyJ6`&0Sb_x|~F{k@nQ zGm~8X7vt`~|BkNyyL^6Z?d!kp_kW$UHxd8;aryqAC;RPo+Qk3A`v1%N{WX@nw_@DKCOZ4BP@&8`@)?XKQr+4)g&5)L; z_9ZWxTg4hS7#?I;uj}ickkx0WXtv_yb@NwI%O@{NNiDa&?R`eH-T#7pvrQB4jk^i| zg+FMmG3Q`lKd~r&N%;Kze;yrOz1;sCLzLe1-dW!+ynNMg&C2(4c>LaH1`InRRkzn>P}EPI@L`tCA* zU;F%7mEQB(1)|>kXPk3xx&6{>o4ZlDnj5NHql+V}ZXQ$E`Q+z<4^5leU09mlHdZ$T ze4imBvykhZp=`!)*2&SU8?StPD-y}XDY@D~VhKy4%xUMlEOQpRiNwmRt8scN(bDXo zJj1iQdhN2bb4M8hL|mRTIXn;*aB>jhSX`LpA8gaTTrMck-8FQ_My61K*3Gk|_AF== zeaiA~C7;n(t$C*M!E^4-NU(XeMfY>WCilv!ZM)=`@Be#P-v0I)Io`XUkN0o5X=88i zJjvbpR{!G<-`nq&{V9+C|KhLt?+Qsf>B^mvXLsLqm#_bGaB}`1@%Vi|wys`X|LwL+ z@}K(u-^4U)A0Phi^5jv?<_Mnmw)IxxI)}bm#jgLQF;Px`dVl!e0i zoo}w&y*Y;8JI61$d-1xGJ(moR9}_iJ;q;EXKh-)xWMbs^PFEAAhy>w)cZJ9IFe=F0 zS&-$*w`}Rrb8fBzeGDBsvz@rsi>}{ZaoJNuRP5+Vwq+YVs;9+Xb*fyX`&oHn+78>g z$1T#^dET*ZIH9@i3%k{$4XaE2_XMv0JSSA8<-$?LZ<{rWUxYlAu?YP%cT@0<1#(_< z(`3`9GM>;{vQ;TOf6bx;#~&QE5j~*lp&*u&C}#C-W|QhH?i)U5T{~PHR1S%pU|7aC zx9LX91yvg7_I#%Ed1seOSu-{=T`?#~DAVO&S>etVyWyq}uizCyg&pBS-wQ!PO^|*kXjYvGN5d z1;1J(%dzI;PXF+oH+Jyo+z~m^ z;QVDT6XP0P(Jt=G)?$7(=|^~&lgt=J7w$cLvv-bT^|2I#rj^I^&Rk^=VwgLP`D<$Y z<9iV+ly2CVUrWu_oz1z+C-bf=>uNE{X%6Av%?rYtt%_0)a5qV;TzZ$IjjNbJeCul2 zEzc77ai%gE9^EM!^vrTQhv(W`m-gxUC8h2Dd+=`dRe>e%cbP@L+r_d#jQ7dHI}307 zwtCN;p)mJZ(6Y?By=V3BU9j6!@TvE zPK#m?=~k@X_t~7~-pz`(Q`fS!58B+kd;jmvv&xT7U1N-ykob-L)1@<4K9-4jy0Pc* z?@&F$JKy-m$<4D~MKf>85Y&C8R*?Ad@m|vpw|7^Vu{@T2r*=o4FF92GN}lJc8GL-( z1uVa^OrK+V{?w5t1x6O%y??C~+m%$=`k9a0_)Xz*Gp5Au)!S8b-ygUqQkgoNSMSIp zG4q?>T5dR$)LYE+?GLSanNln6x^w%=lW&?Le2#X_JtcRNBSB*8y{}z9$HgZaS>^{kNV8x1tw>dR0YJ?AW zWIBl0WDEG&3cA0w5Pzt8&G^~Ge1?!4krMOD`uAVib2!eY`{Bz^nbTr3%d}Q+U+npy z`}CCxr?;Co9{k*}VRLr2gnh-PhfLziW!1YyRh;%tP5*an4Ovjn2!R zLg&4tJoZ=|Q0d)qjK5;rG}-UpP9~KnTx{*)D-GUJ^XRcuy8p|U+G~x!Jrr?}*x+Hv z^lF*0g5d8>Z%ZRfFCCq+uxKrAOB@V)S10p zv+c>M;GVl@`3JXnktvJcKKkM!leVHI zDpmJWPWbw``|B4>ow2=&xk6dtoP_!t@lDfCtZaEvDEImDXJf7llEQD=#lqJeE-AMY zJZC<``p=HTzhq8Jzu~X{b6EZSx;J5mH@m0LPBoV;Gi zmtI}`-Sg7_FG(@FC9^*z%SuV4PBl9=ZTY2rabHuIKV8maZ_?W)rpF+(!2G)T{U1O6 zW}Z8`)4fivwr1C!hdc3Q1D z(;{U)(Is}_JCy{3m1q6l)E$~tWf)#w%(dWK<)Pb*H!glGtS`ym*ivPD+wz(IgRTh* z&mZs|R@RxfuC^U zS9kj9YwFj9p5-38o%8nit%zsa^7Ea~v@`7tW4iyO#5nl#_jz%A&Ohw`%>O_4{}1~= z)vx>I`7dhP`uG3qnHs&CU8(ZKnwQoWZcY9w!5?~A{j{L%jU6+rRm(P+?Qe^b$-Gur ztMj{yQ{e@NbwW+!7QT=TeI);a9GDEsXI7pFR=?f%c-s;B(ZVV$|9kboUI!K`&9LL zTqaaKz2w4VDz3`0AxvuT>_C=n9J5<&g7ZSYNvLl8k~FL0BLChc5=VJ2be)_~XxjHX zHcn*TRu6G+{TGK9y>HtWzbAOV*#rqCn>8h@X_NNt{gds&a81bdFJFUM#mWkd^hD7=`P4hlS{sA*s6J8HRBmK z9X{c<4cxP>e5HP;s1}^FWJ*}&GxzL=lTuv=A4**{`>?Z{G3Dwk#xw;tf9ub0=ABs` zEIMn|?86I=1vPMVMnzPdV%oZ{LQTs+`U>b1(| zYNF`GCw{!G7nIq5Jo#XC{>7CP(Z>-g5k99w#ZM=`No3V1w&hnid!}$r59?XpV&~NJ zCG%yQpF7xD$D~hNQF&igN!E6a)04wG8n2h$*tenT#JmRP=7`m|45n^K=`ISNvMKq4 z(vzEkwd<`U0!m*zIzKRR$CR;A(U@;Xuy>7Rcghk!!lTX?G=gSx!i>cXRSiQpf&faxXPJNnG9=;&px9`eR25M&JYG$UJU%lIT z#Vj(dZutt?D=ah0<{B}{6e}L+QVb8^5O=&&JIzRJw&>DjFJ>7#Oj(ruRpPnT*(f#k zNgm=;-rVDl_7GXN^W4!d%N`j_{wb%>$vu5xhN-dHbBmKk5)xPE81_$I!t4;^9rY#n zThg|;4<=Wo=O!3`76~usJ`?l3_=d6ygW=K1%sjEz&wk!x-{YuqEWm5-GoAdK3^ z=lYku*uDAet|?WQZdhu3IhWV6xJfBV*!#a@mzCj6#+oC-4L|Q*+fwdztHm$%yMe{+ zV|wN_iEQO5`Yt8gB2Bd~{xe{ja5UAYf6>AapRX5gnMK>n+zY(Ie5rB!%`DfGS525( z^5!nkC@u?cxU9gC_03A_Wc2sk{XQ0~JNtjxI8P9}u_N-BVuF9kV=*T|k1i=4*RbkB%m&9W}0w%NHKR1}-~ z!*$ZQj%anLnSbA|u;S*0a>H$xO*Iejv|nA*-n=YicWY1C|3lAv?o3EMKkfb6l**&l z8lN&4Vy24E3FCdH!m}c?U2oy-&u23^f|O4utZu5&xo5P>V#X29Xwho%^UE$IuNGNx zR!_{(uXt`+_!aX{Mvi(rOi;cf+Jl;ozP2efQ5b z@o=qq;>aY~ad!OyV^M`=TE-#3~=wEIFWdbYZXGx457wt4;~6{IdP- zZ2oZh+R`6QrfIfQj&AUrt^aNQ_f0n+`MSJXS}%US?9rLZ;+uiiev@UZmcE=fAwYC1 zW4AnCgrU&-o02-JuUAy+2VcAE`aR;p&pq)8S3S2M{TlZ!IMycMY@bKGC8u7U2jd!} z^OAa=GA=qy0jqPkC(MmuZ8_L3k+f=MMxF8gs1qN~K0SZ;&Gy)clsntb*|W1(cK@)O z{+V(6g6`KZjS6EgmO6=WG?v$FKR&OIS@LGG_6xI!h6imi*$Z||{Jik}m6+nUOXqLM zza}A*&5_;yR7m_B*Q?HUr~FORuUSk=t!X!rD@^%Q#=!IRdls^6o;FDzHSjcthz=uyS{q5@C z@8vh2A+P_n{r9KO`wM%me;VD+-`{RN>wdJY)HyL9gX8;pCZBzD;E{Cr(^Lo6sx}9y z*OwMJh4@#yW_rAC5ID(@Xk@!c@Zv31r=F529gGg%w^tZ6Oyh(~H_AR$rTCTDmAp_59-{|L@d4 zKmY&t|99)}_9woyzwldr-`TQ@*Ufepus3>kFn_#wZC+58YJsnT{PBq$zxL%n{4C&f z|NGfrd1;?*?3X;aWuHG~2U}Yv)9zLO|D3O@DBu4-{$KvTwfBEj&$s(G^SM$^)`h!Y z9@!V1RygRd__X8RwIusLY!%hUN;`IkPT)WPpJ}Oag`43Kxwt^S2^~g`=`C`Jy;h6f zJnSlJHfk&4=elCU|6b#jkdk*oO8!44qeQhur#e)6&t3Cs?U>!VZfe+#PUZ{WG!z09 zPS(hzi+rDU{_Ft;7NOQ%;Z7YrOp}wJtYG0}aOY{&KEWv>$s!cA;gH|e)m=)uGjwOB z2o&;0azu%!Dt?veEt*}m>*n+PZJXQv|JJKXPOSg7H(7ZNi`%P<%&}ElcKJEvP1$$Y zvFXgxxc7Y5FaP=U%gyfZp5M3c|6Q@YzH#3FW6!@wZ+yMm*m=j!y}gD!wfqZS?>0PA z|LxP__#J;9tbTpF$o!l9B;yAO4^Myo9Q!f0^5Z|{fAW8Jey{tm>d(3E*?KQO&WWER zq&Y#Q%g=qw1;WOC${;991%bLxt;9?{pqJJs2b#I^mkSw3M~l{?4B zj6XuHW`AdR>DlQWW_uucYrn68!^Ekf3P*yLp0To)4{ESW0SjCk2mP>QX-!9s@ z*3(Jpx^G*0!r8r~r6wH=L9b~HRWHM-+ zsmPRAmYkX!UhT&E^W$&PjtC2Z@C?hV5giQoR=F*nqSvvZA=*)1dv&wa;S=4SI-xEz z7e=lAcjd$-zbz4lGY8;dISi9wL1B?3#0%c{`8J zNn$!};Jf*@&mB#R3bXu%MA5kpb0-V*7l1aAJ(?WWIiuCn_li-D8&}58 zC|N}<5g#G*r(V6gqqMtMss!y?^VF**OfqQ&>uMF{Gn}c4=3PujTzgFO7))iaW@_zu zm&S0@DM5YioTgt|ttUl#JYQ=t_Nh*o$+fVoBwO?Ry>6GOsk$67Sutl*x4-Ze{9f5% zs4AN%v!kFq?2W=MW?hxecoEZW#c^VtH>Sq^3aICOnmgRJe?eI3_*D>MJu+>P^r!VQ)!QIAZ7M5sN~6ltz< zb5ZnoUBFr(uyg0``Oojfe5}20Tf@6sk5z(ckwoU~3G!b*Z`XRg+y1b|;}6H)u|8Un z^o9M?r4Oa9cX>EvXZ={i7crS5s`u4PH76s7fKT_<%B}zVZ2A8CTCZ-pH3&H^;|ZGh z*m?2Q`v)Inscqyo43s{#rS-y=_0JNV_Ae7&9>BLldSm{?ncMhnI-mUFd$>k-!R2%7 zzVBdPWoNVdKUc-Jd2N$Vd|^q*P{WUMP z%e9vstXlEexZb;WQ~Mk7gs54zckbMlW-H6~xOyYe;*+-ni%{0T16EUY4sFfqGF08O zc!m6HzGpmQDjkbVj)hwH=#`~yyDv6D*X*oPId?UlnBr%k<^W?P;)dtYwZ%*9f7 z{KJ|8-4pIl$^I!d&sXiHQN_aN%RjQU3J6^-t^MT|&or^6A=%&BI$M)z#_tV291Cps zYJcFma`Zvo+JiT3=hz*7cWuHmX0dgA5=Z1dP2kpx6Gg1ukHV) z)tFv#^$Br#~+DfBU8Aqei=G zu98y1)&*j+f$v{^3;dpVZ4YI--u~6gIzK^oos8+idhzpZ$=`S1Y+fX{ zXqVinvj^F~iRl-I*ME=iwV2aAYxB02^fDzef9+d3za3YnpN%YbkuRKZWLp1x|Ms#7 z-Uw}JORiX{Q+)dGkMS-2c<)tM{Aug{(23gN4LoUi1vbmRGYLr?zR8<#ET`wx>nodxc{5dUfCzC?GOnZ^F<&416^ zt+cQA+}?lr=WFRX`X7v0=ilwE|1S3X?fP}|=hX-6Ir9AZaD4u>p!4Uh{lB^XpKsc! zrBi~uSDrrV)N()9gt4Hu`uorB^+7UXt6gipoo$V&|M&9U)!7U;mIvEDc$UA;Y|^p6 zpNt;=6MMWlC)>XIj{VA*tt%XNwyjb1c=Ouo$Xof}8|1r}HfWU0uw`L-lwVu4W3RrW zg#gQmzU$%n_x5x2>p%Xy`E0v<-Q&sc|GxNN{Xez*;(P0Ni>6f0d3H3ZBw(U~;+3LRz86of720UY zdY4z+f5+|(hr?&w`|znYs`g6JJ3Z~Q-@~e|&%8YAP}el``se2R{!gC2r^E4z#q*uH zDcLLEUMiMI4^8Eaulf0E_Wd6Z^#69(KfM3v`u|f8-^{tuU(9SCVmnKq#qrLpS*k93 zXTRIFc=os2_wUZ`ySMH2*{JyJwOiMPX{Y4{RT>}LzP0-HwC(b@^5u?{rPck(_z}JT z-{Jpn|G&FGYg=@TT~+Z`SMCqLeGeNp`&oZz|1;xvpws_b`7ahOkjr}AeRa{}XLn0O zIcxuCI6s}2()ocU&NjbMY;Iuqg-@3}9X`B_E3;N-+xT+L*IZVf)+f2T1#`GfJROcM z^6JSeP~yy!QrQ)0_vq%9uF|(oOPB-&!soAEsi0`MO7^;$fq{tHha9$L4FPMIA{8Ip zS;+3taZ$nP-PQ;>6|?;dcQI{T!Q~;Ls~S3Yp+U%Dfrl4ECzfy9pVDiW5^8X4=N-{s zU$39*U$gb5{7V@|4o<@|LlMF3#M@!QhZCaKOh zXCD>dd#rM(|M63uKaD*9PbB^LYTo{K-KNG9w<4IGr@MsYPJMCPf4csDzL&4KcxHH9RkgVDaC4v1#*9qqf~F}!)*4ECBAkpWuD4n~?`Y*< z$Xm9e<4X2+hLD^+_wtF-m($KPd(e6}pz zIQP^OHqKD@i178NKZT>CG_4WJupIMb#1Iih!Z?!EJlxg_? zH~e8>&3kEi8}X{c4j%*7_|K3@d%;&G?)=WhuIkgqR@M*mL>@{f#xaEH+Pr=)8sx@$ zGcj(ulG2T9jgIYxvp$9}%naXqw4x?}joHvZ)w4ik9m9>e0X!c=3RsgAI~#A6M|hst ztJ`oRV6u+)u#t^Wph8(H=h4-(TwYu}ZxC6~!J;Og zt(Y`J(0H@F(qakGf-862xUZTd$`vZwEf4rMGdA+ZnS?8xDnI-x-nATGb#sx2ctQAQ z7D*SC^x&XtJ3kyv+1R+(<$%E_=O>po<|MMpav$M$3YOoIGE-M2YQy^_J`eOx#P}X? zPHO#l_QlkcnMFy=S64=v`dvJ;;EFQWPsbM|YF5ZE+t2CBd_>!ap=}z| zld;Zp9PT>%G~wadLZPi*^8qrbx0YjbERzkzQp$KOrw4?T)( zYZ~LlLxI5K5$f}cS1w)jY)Sixb9dAuMV4kg zvlQK2Ycu;WlRwMjJP9%Fb;>FW&26>agpT?gJ~Ko7jhX*f?bq-4(<91bxjJR7vUDFX zygSyEwCc{$x~-eEUK|K`xz&F4gUoBQ_7-pa7jtk%Uir22myZaEiTlqu`Yn8$mejRt z>np7k@^cH2wB0o35}2u2 zVUf1uR!j1h=(_*HmEi{F>vZ4i{%?@}-&nCN{A+hvQuwiizn1p*ZFCe2?bdhh3s>DTr=3AY~iM^WCB=j9ZSeOnQ*nY#ECHd$B!Gs zZl0En+un7s{Xn#9sRXy+j=T8+I|__AI~pH%I4|7fIeY${`g+}dy9xPP75%HN_r~8^ z<#{6h`$;SIB&7`<_x9Y_FJN7-Gy81S=gDuq&0+*Xq*5l%UUT=`gg_xd)5U45Zj!5` z+V?&FQBh&zU#az(b>26lJ4y3Z&Bbr;uGr=#n8m@N%vLe;QB2;Zf@Qr9eWLuA4z-@W z;x77`d38_1IxfWtQa6{ypH)14rrql69Z#Xqqb*6M4IB$iHDrVLU*L*;5ZE5tFTJQF zKD?=VhH1Br;0&i@dHT0MTw7azpcBbLeoxo+CDj zu1o?FL8|*#>ig_@SlKP&IN`BdxXi^f2l>*}+z$MFWaDYFIfkpLbXUxmZo%(Xnd&be zw)*Y}V-ZmDZItlvT9$FPLz5_NS^~{pA(@)51E0%UCk+ zpFTEIIYfb5xVbYj^m>iq$G= zkv2m#?xJ?u?YkPiRd<&gl$`$65cJMrO4G2zOw#bVg0@%O3JQ$ zO-n6Y8GiiyekniH=cmQCHA{8E#Qo*$YxB=K{Q0x_yx`%d+wcFrw_N{y+;sbo7ngSC zz0cSCaq)Nj+xU6q_xI@Cuf69wJF$8}@WmC4_g2nO5#T&xsl3@?OJudhCKs(=7tXrz zvU_Tl@=KSlPF}lPz|8GX>iwfnJag&~eDaL2XV*S7$y4Wk(f)w=Uw(Q(E z|KIihznA~-LHNHv^MAg}|F6JO_`Pt}GfRHw_|=(ngT0o%GU=UP8~DHV|GW5zvb*p9 z{YdRN66S;^7S7y%A8L;Hu3efR9~bv!+y%H?eTt> zm#jJ?6aD7>7G3p4L0-EfquF=-F4`%v{H6VW>HU9~|NkPtyQU~D?Zf1w(?2-uY-pZw z>+ZHa42)@cX=m0hegDF%z@o9uuI_ue{qI*-?{4r|ZgoS;&YMf2>0ZRm)32P@FYeD@ zaXX-%Q%S#CIX%V9RB2hj>8raXze-dywKQ)LQc+h^N>j=#kW=Eg8kWtwaM~obBM;uV zOkfH%ITE$>fD%h0^ISi#wTF0SM7#Qaj^#S5$E2d7;kZEJpxY(q<^+yOT1C}5Y_1C% zCIrfT$Pwr`CU>%B!8*G~5z`MCw*CLJ{rUdy)#eH}Pd~hM_^WyQ!)4Va6*Ye^Ei|v-kEsKAZNd-(D*2>E`+CKgUJA-%m{7)zG zIp|(64P#17YE03U6b`a|Qgg6B>SO3{n+;kQoNL?eF)As(7V*DXI&raZH=|Hk?#>xn zO-l@wQs&KOkXztsByf;Pb!ts?p3tS}fH@snePX$4Moben@J#A?Gv#Tu_I~FL7dAU~ zEpBMpDOb44%{_+m!19Ct7^9m?4k^rCo%Y1lqyO>cw}171eaPA+#Kmy;SNQRHAq**5 znwK9LyfgnZOY-)n8K)OpR|#Is{qSP-ua}IIF^*{_(( zdG`9RPx61C*}s}}v{_-wtf{Y8Mc?T$PvA57ty2H`r+N5k_0LA)O|JSaxw|LnX}z)f zHbFyXhu0b&`6^CdM|EMdvx~0ZS=Zs%kgzehePwuN(>vH$sDxUCl+(gh9k9B{7l5ach_>`wXSF69*C%?lMXmDps9*Q1PZr!0v{YWROnd zffI%eqNfCAF<(6~>7byd=edODE{&em#{{-KpLu8Ef~^(|SGdyE8oMhVWTdT9&|Evq zMNsucLzcrwNwq^q4SJV$XI33yaepFYVQ9!6xc!=8oFh7vnnxe7 z_tz;by*SOZZ+eo8jKM*{6Dy)q+|Mc59DVR?$*DO>?y4Vo%9GwJubTZcXF^9()r>W( zb!IyTwj>yyJDB|KRKbnJb(hq=8d;YnKVH|q_}a#`Lb69SSi)YcNo$MC;40V}U-Sg@h0cAH1=NTDdPad5T5qepnrM>LH#I<2_&n|aLUT1hBE}@i9Gh}sAq-y!5 zlwMHZt{T1VXZZ1xHeY0-B0X_k+fsI z_jXm8Vq2YuP@7_;(&+~w6%(HuiEAHu9k6d>q@GjAG%m^Z;?B8OqpnU$2n(z@R1lpq zkI(eWo_`&=uiUz~em1zWLqT?ho>xTpoh|%{ZxjxlVZ8Lta?>0weZNn; zSIzx#qdGG_ps^R@C9Ni>cv!Un#|#_cl6STeZq-(bXcy-0O$fpKqHL z&Y>o|oae-uc*6_&o{GD3*%r877HCg>@k>Cd(?mhW-1ed2ywDfYdki+GWpZ3|6F4T| z*l|rq`rm>T%)1U`37g`uY}6D-#)gRe{E>V!v1p%k7PI8^V2@28xYB=JMEdxuXpR>0u>%8 zKI3gYzFkgq$E}8^Gvh^8_ukKFOA1v`Vp0xgS5b{DIXru9W1nPctIEy!FJl6^oD1$B zQ*vD%-NV!vH7#f>N6==5Ey*XMf?hLn8P^YwF!+tbW@Bn0kFz2E~ zMOVK;WL?a)Z8K~5FC=qY%W56s3wL*DyP zyML+Gj0Ew2pV-3>I=s`n^p)HDwV>+0jYdu9l^P8?-iXX)e#NYjAr;7Q^XJubVJCm* zq`noeo720&dG~CyxnatV4WhYDi+x@#?P2X?bY8dPz>k#3;)nXnSKII7uD^G#?(LcV zulX*&`FiowMg9B7`A;jw^4ZJe|MxQ2yRf(XwA0-jwsZEsok|7~9Z#cG>y5;#Yct*EwwY;v0p3 zh<%ky*wHhw%~ff~m-D;6RNu(||Ed1R_4=pszg3NOk3O?$7chT)6?Bff-m{pZdCn6K zC(e2HbN&zYx}VeEZ|6(mTY4te>;8`yf4|SO6-=qH4|FZwry*hP$ zROr@euL=?kw?}2^?F{qP%v}4-Z-GD)NBG*eQ{EStzu&#@%B4l0V(;5p#s4|||M&mv z{PiD>CjDHo*1qK0-?uD5FF0O0HvRUWkk6QKaq^p*8qV_(b-VZ9=eN`5cll&$Id`Ut z%^H~tPg3*5JyeZFn_QLDKQ-|z58iXf$T~pyOhcr5p%6Ep#%ig<{&|iJZ?+2FXuQd_ zB-SKUvDK7GXU)|+BBsk1?|(KUuzhiWne3K>buo^Q_`;dGUOL(`Nolsp=$`1AEBVgd z_KJ)XN4JgWop6VRM^A^m-M4S&?e|_c{ZqsCZv1)fl;TOTJ>~O)b%f6BU1HH%z+A&A zKj+TU6`TQAZs^S17_|Iq@wPe7gZlNKF`I`5`kqxS(%@*Hq&B4|*Fm2_d5K==`i7#_ zx3B3~Rv~)P%hp7Etk!^gDRS=pJy$t zGE8ZE%eUCcUF!by(=mC$J#7}U@5+zP{CjNq{F=J2S#>vG8#x#~^W>A>RWY$%$%koG z+y0Ly^X(_|SLsAp{8)WHH(yKs`~7#`GeauEuh|T~$*fsTI@Ns~jAn z@^Hh;+>4#1rz3KLngbNp%B^fZvsAcWNb$rnE=B#VFLDB%rUY$@R?TU0-kfoAo8DYi zwHG`Bt`UvVqE{B%Ufeos>n}GBt*DiyDn9bC&!k{rbjr+l|<;KrCou{u> zZ4^8#62r)su>C;5ix}=tHoFzsUpf^R&x%~TY`N2lRZE?GyEU_0cSUoBJXjiGVmxV4 z>8Ya@his=S32nMI$5?oJ)a}fbSEQUXoFDEi+x%-zslDiSttmT%19;?$md32|E#6r7 z$ntFD+H-nyUrIQw59LWNm3}UGAS75aur&V5g2Z`yuOwYKGHLCCa|xFMvgEuQH_G(H z<<;6o2&}X539P!TaOm!)!E?7wMZ|Re@hq#w z+XZGObG+D9p|L5rY31(gLC@zf9ubJ#G4++}N6}T&Y^G-!T#@P6nz}any_F6_{e(q* z<)6<;?P#3O67ZNF4kEpcDHSsKeqgpz2|ap$NHZMMM<8kox(m%m~Gqn zgeQ%uaCMgTt1bK&-?{ZF862Dy9`NRS>*4<=7VL0(y27!w=hGy6W9I`4=18(MuhwRM zcKpmUEPM4s1VC=+Hx{kR%K3kwjU3kaJ9af zU!c16<~xbEZ6X4)MVpQYXxJ>QRd{~U(q*0WFAX6@t-y$P&gYItc5PN*lsuOp$~xzv zlEw^%#3Tl}HPc?5e7G<&^fl+?dul2SpE6G!wzyWHQ2gSz;`E>i%}2Ox`aBdgGAD!> zzq-1dTeJP`X6YWa>(ZX-MW5KtzboK&o$w{BU0j2!aIR3&ydPg16pR+`KWDzY#_ZU< z9_ceO`;MDE@KL$ek3=$9m5YrHtEx`7&i5TJH?n&uq*sVoo{jn@ z$jZ{>?aHO)%<|^MR<8ox>&H(AY;QR1KjYOR8G)>#sv~JOw-{eVuwR{>!*Qnb*4{g2 zcU-#{**J9Va%tEoxBP3FdH#0sxnKTU?f(1UA6v}4{zK`$@Z)#o=RLa) zsD>ZrmDjlW{cU~EVP3`;x75BIeCRog+j9R^ZH7fg3<=#bqN~#UW|(fSlgVHH`eydq z&l4D%GcFo(C>k3qwRoT8)jsoY$d5lMrrmeD&wei3S><8;zvgpL`CF}d<-XE~``7EY zU-+~A$)7(j58u4|=-u(#`#Y6{!gFo-ZIXRnxo*7kZQhBbCru5X9^_RRJmbFV@A@tG z!K~thgf6NbTEXab&P24t=+>LU zowJW${`>#6{lByIf8+nA%g4>h{jSV@anWNF%ZyVy?|_G-{j3 zx8HY@3U6)8l}!~Y)O`Bt)vG5@QZ`0}ytQpH=zF}{yj=0f6rlr3lR~{OzAf8-`>j}a zYu`Gqi2}(n@4sC(G4*|&_x<56&(9WpXKZ+qci#Q}*TS#r!;U?&yS^(hx_Y#4I$FY> zInmGYO)3k|rH0@yOxs_r)xUi9cG?<&&YkO+rZTwuopA~gJi)-aYP$ZOKEBS?i`MxB zRIqVxR?rrmC=v52M9|^Ei}vJOjL9y3>=IHe-`513WGFY$pTp_m!X&m~Q=_1cs%MXa z4l7$%lY`2H0~~C#N|ZQ*Zyuk*y;7S^HmopiS?9x(kGku=J+J@s;kmeN{g=aEDnH-L zFW-MX_OHNgF`frs%g$KV@0p^0*!TCp$?xlbsJG;7omKz6Tl{){-nQWVGmN?&J{


e&c0k^FEI+Eru_X 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 M>#e6z5 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

g%??%&ml8k=^Z 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

eQ=G=Gfo-n$ew2sLdzeWjS9MxzJPzd1Jl)q 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

^RzYUNBB<=)%- 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 From 8182c0294710c9c21284d7cecd1f9e4450a5fdfd Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sun, 6 Jul 2025 20:46:21 -0400 Subject: [PATCH 256/384] Added ability to set visibility of menu options --- .../zscript/engine/ui/menu/menuitembase.zs | 5 +- .../zscript/engine/ui/menu/optionmenu.zs | 263 +++++++++++++----- 2 files changed, 194 insertions(+), 74 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/menuitembase.zs b/wadsrc/static/zscript/engine/ui/menu/menuitembase.zs index 2169e9fac..cc444ed10 100644 --- a/wadsrc/static/zscript/engine/ui/menu/menuitembase.zs +++ b/wadsrc/static/zscript/engine/ui/menu/menuitembase.zs @@ -21,7 +21,8 @@ class MenuItemBase : Object native ui version("2.4") virtual bool CheckCoordinate(int x, int y) { return false; } virtual void Ticker() {} virtual void Drawer(bool selected) {} - virtual bool Selectable() {return false; } + virtual bool Selectable() { return false; } + virtual bool Visible() { return true; } virtual bool Activate() { return false; } virtual Name, int GetAction() { return mAction, 0; } virtual bool SetString(int i, String s) { return false; } @@ -48,4 +49,4 @@ class MenuItemBase : Object native ui version("2.4") enum MenudefColorRange { NO_COLOR = -1 -} \ No newline at end of file +} diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index 7338885bf..17d52a7cb 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -82,6 +82,7 @@ class OptionMenuDescriptor : MenuDescriptor native for (int i = 0; i < mItems.Size(); i++) { + if (!mItems[i].Visible()) continue; thiswidth = mItems[i].GetIndent(); if (thiswidth > widest) widest = thiswidth; } @@ -171,7 +172,7 @@ class OptionMenu : Menu { i++; } - while (i < mDesc.mItems.Size() && !mDesc.mItems[i].Selectable()); + while (i < mDesc.mItems.Size() && !(mDesc.mItems[i].Selectable() && mDesc.mItems[i].Visible())); if (i>=0 && i < mDesc.mItems.Size()) return i; else return -1; } @@ -182,30 +183,51 @@ class OptionMenu : Menu // //============================================================================= + int LastVisibleItem() + { + int i = mDesc.mItems.Size(); + do + { + i--; + } + while (i >= 0 && !mDesc.mItems[i].Visible()); + return i; + } + + //============================================================================= + // + // + // + //============================================================================= + + int RemainingVisibleItems(int start) + { + int count = 0; + for (int i = start+1; i < mDesc.mItems.Size(); i++) + { + if (mDesc.mItems[i].Visible()) + { + count++; + } + } + return count; + } + + //============================================================================= + // + // + // + //============================================================================= + override bool OnUIEvent(UIEvent ev) { if (ev.type == UIEvent.Type_WheelUp) { - int scrollamt = MIN(2, mDesc.mScrollPos); - mDesc.mScrollPos -= scrollamt; - return true; + return MenuEvent(MKEY_Up, false); } else if (ev.type == UIEvent.Type_WheelDown) { - if (CanScrollDown) - { - if (VisBottom >= 0 && VisBottom < (mDesc.mItems.Size()-2)) - { - mDesc.mScrollPos += 2; - VisBottom += 2; - } - else if (VisBottom < mDesc.mItems.Size()-1) - { - mDesc.mScrollPos++; - VisBottom++; - } - } - return true; + return MenuEvent(MKEY_Down, false); } else if (ev.type == UIEvent.Type_Char) { @@ -215,7 +237,7 @@ class OptionMenu : Menu for (int i = 0; i < itemsNumber; ++i) { int index = (mDesc.mSelectedItem + direction * (i + 1) + itemsNumber) % itemsNumber; - if (!mDesc.mItems[index].Selectable()) continue; + if (!(mDesc.mItems[index].Selectable() && mDesc.mItems[index].Visible())) continue; String label = StringTable.Localize(mDesc.mItems[index].mLabel); int firstLabelCharacter = String.CharLower(label.GetNextCodePoint(0)); if (firstLabelCharacter == key) @@ -228,7 +250,7 @@ class OptionMenu : Menu || mDesc.mSelectedItem > VisBottom) { int pagesize = VisBottom - mDesc.mScrollPos - mDesc.mScrollTop; - mDesc.mScrollPos = clamp(mDesc.mSelectedItem - mDesc.mScrollTop - 1, 0, mDesc.mItems.size() - pagesize - 1); + mDesc.mScrollPos = clamp(mDesc.mSelectedItem - mDesc.mScrollTop - 1, 0, RemainingVisibleItems(mDesc.mSelectedItem) - pagesize - 1); } } return Super.OnUIEvent(ev); @@ -247,72 +269,136 @@ class OptionMenu : Menu switch (mkey) { case MKEY_Up: + { if (mDesc.mSelectedItem == -1) { - mDesc.mSelectedItem = FirstSelectable(); + mDesc.mSelectedItem = LastVisibleItem(); break; } + + int previousSelection = mDesc.mSelectedItem; do { - --mDesc.mSelectedItem; - - if (mDesc.mScrollPos > 0 && - mDesc.mSelectedItem <= mDesc.mScrollTop + mDesc.mScrollPos) + mDesc.mSelectedItem--; + if (mDesc.mSelectedItem < 0) { - mDesc.mScrollPos = MAX(mDesc.mSelectedItem - mDesc.mScrollTop - 1, 0); + mDesc.mSelectedItem = mDesc.mItems.Size() - 1; } + } + while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != previousSelection); - if (mDesc.mSelectedItem < 0) + if (mDesc.mSelectedItem != previousSelection) + { + int viewTop = mDesc.mScrollTop + mDesc.mScrollPos; + + if (previousSelection == FirstSelectable() && mDesc.mSelectedItem == LastVisibleItem()) { - // Figure out how many lines of text fit on the menu int y = mDesc.mPosition; + if (y <= 0) y = DrawCaption(mDesc.mTitle, -y, false); + int lastrow = screen.GetHeight() - OptionHeight() * CleanYfac_1; + int rowheight = OptionMenuSettings.mLinespacing * CleanYfac_1 + 1; + + int maxitems = (lastrow - y) / rowheight + 1; + Console.printf("%d %d", maxitems, RemainingVisibleItems(0)); + if (maxitems < RemainingVisibleItems(0)) + { + maxItems -= 2; + } + if (maxitems <= 0) maxitems = 1; - if (y <= 0) + int lastItem = LastVisibleItem(); + int newTopIndex = 0; + int visibleItemsOnPage = 0; + for (int i = lastItem; i >= 0; i--) { - y = DrawCaption(mDesc.mTitle, -y, false); + if (mDesc.mItems[i].Visible()) + { + visibleItemsOnPage++; + if (visibleItemsOnPage >= maxitems) + { + newTopIndex = i; + break; + } + } } - int rowheight = OptionMenuSettings.mLinespacing * CleanYfac_1; - int maxitems = (screen.GetHeight() - rowheight - y) / rowheight + 1; - mDesc.mScrollPos = MAX (0, mDesc.mItems.Size() - maxitems + mDesc.mScrollTop); - mDesc.mSelectedItem = mDesc.mItems.Size()-1; + mDesc.mScrollPos = newTopIndex - mDesc.mScrollTop; + } + else if (mDesc.mSelectedItem < viewTop) + { + int visibleLinesJumped = 0; + for (int i = previousSelection - 1; i >= mDesc.mSelectedItem; i--) + { + if (mDesc.mItems[i].Visible()) + { + visibleLinesJumped++; + } + } + + int visibleLinesToScroll = 0; + int newScrollPos = mDesc.mScrollPos; + while (visibleLinesToScroll < visibleLinesJumped && newScrollPos > 0) + { + newScrollPos--; + if ((newScrollPos + mDesc.mScrollTop) >= 0 && mDesc.mItems[newScrollPos + mDesc.mScrollTop].Visible()) + { + visibleLinesToScroll++; + } + } + mDesc.mScrollPos = newScrollPos; } } - while (!mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mSelectedItem != startedAt); break; + } case MKEY_Down: + { if (mDesc.mSelectedItem == -1) { mDesc.mSelectedItem = FirstSelectable(); break; } + + int previousSelection = mDesc.mSelectedItem; do { - ++mDesc.mSelectedItem; + mDesc.mSelectedItem++; + if (mDesc.mSelectedItem >= mDesc.mItems.Size()) + mDesc.mSelectedItem = 0; + } + while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != previousSelection); - if (CanScrollDown && mDesc.mSelectedItem == VisBottom) + if (mDesc.mSelectedItem != previousSelection) + { + if (previousSelection == LastVisibleItem()) { - mDesc.mScrollPos++; - VisBottom++; + mDesc.mScrollPos = 0; } - if (mDesc.mSelectedItem >= mDesc.mItems.Size()) + else if (mDesc.mSelectedItem > VisBottom && VisBottom != -1) { - if (startedAt == -1) + int visibleLinesJumped = 0; + for (int i = previousSelection + 1; i <= mDesc.mSelectedItem; i++) { - mDesc.mSelectedItem = -1; - mDesc.mScrollPos = -1; - break; + if (mDesc.mItems[i].Visible()) + { + visibleLinesJumped++; + } } - else + int visibleLinesToScroll = 0; + int newScrollPos = mDesc.mScrollPos; + while (visibleLinesToScroll < visibleLinesJumped && newScrollPos < mDesc.mItems.Size() - 1) { - mDesc.mSelectedItem = 0; - mDesc.mScrollPos = 0; + newScrollPos++; + if ((newScrollPos + mDesc.mScrollTop) < mDesc.mItems.Size() && mDesc.mItems[newScrollPos + mDesc.mScrollTop].Visible()) + { + visibleLinesToScroll++; + } } + mDesc.mScrollPos = newScrollPos; } } - while (!mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mSelectedItem != startedAt); break; + } case MKEY_PageUp: if (mDesc.mScrollPos > 0) @@ -325,9 +411,9 @@ class OptionMenu : Menu if (mDesc.mSelectedItem != -1) { mDesc.mSelectedItem = mDesc.mScrollTop + mDesc.mScrollPos + 1; - while (!mDesc.mItems[mDesc.mSelectedItem].Selectable()) + while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible())) { - if (++mDesc.mSelectedItem >= mDesc.mItems.Size()) + if (++mDesc.mSelectedItem >= RemainingVisibleItems(0)) { mDesc.mSelectedItem = 0; } @@ -344,7 +430,9 @@ class OptionMenu : Menu if (CanScrollDown) { int pagesize = VisBottom - mDesc.mScrollPos - mDesc.mScrollTop; - mDesc.mScrollPos += pagesize; + if (pagesize > 0) mDesc.mScrollPos += pagesize; + else mDesc.mScrollPos++; + if (mDesc.mScrollPos + mDesc.mScrollTop + pagesize > mDesc.mItems.Size()) { mDesc.mScrollPos = mDesc.mItems.Size() - mDesc.mScrollTop - pagesize; @@ -352,7 +440,7 @@ class OptionMenu : Menu if (mDesc.mSelectedItem != -1) { mDesc.mSelectedItem = mDesc.mScrollTop + mDesc.mScrollPos; - while (!mDesc.mItems[mDesc.mSelectedItem].Selectable()) + while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible())) { if (++mDesc.mSelectedItem >= mDesc.mItems.Size()) { @@ -368,13 +456,13 @@ class OptionMenu : Menu break; case MKEY_Enter: - if (mDesc.mSelectedItem >= 0 && mDesc.mItems[mDesc.mSelectedItem].Activate()) + if (mDesc.mSelectedItem >= 0 && mDesc.mItems[mDesc.mSelectedItem].Activate()) { return true; } // fall through to default default: - if (mDesc.mSelectedItem >= 0 && + if (mDesc.mSelectedItem >= 0 && mDesc.mItems[mDesc.mSelectedItem].MenuEvent(mkey, fromcontroller)) return true; return Super.MenuEvent(mkey, fromcontroller); } @@ -386,7 +474,6 @@ class OptionMenu : Menu return true; } - //============================================================================= // // @@ -404,26 +491,50 @@ class OptionMenu : Menu } else { - int yline = (y / OptionMenuSettings.mLinespacing); - if (yline >= mDesc.mScrollTop) + int visual_line_clicked = y / OptionMenuSettings.mLinespacing; + int current_visual_line = 0; + + for (int i = 0; i < mDesc.mItems.Size(); i++) { - yline += mDesc.mScrollPos; - } - if (yline >= 0 && yline < mDesc.mItems.Size() && mDesc.mItems[yline].Selectable()) - { - if (yline != mDesc.mSelectedItem) + if (i == mDesc.mScrollTop) { - mDesc.mSelectedItem = yline; + i += mDesc.mScrollPos; + + if (i >= mDesc.mItems.Size()) + { + break; + } } - mDesc.mItems[yline].MouseEvent(type, x, y); - return true; + + if (!mDesc.mItems[i].Visible()) + { + continue; + } + + if (current_visual_line == visual_line_clicked) + { + if (mDesc.mItems[i].Selectable()) + { + if (i != mDesc.mSelectedItem) + { + MenuSound ("menu/cursor"); + mDesc.mSelectedItem = i; + } + mDesc.mItems[i].MouseEvent(type, x, y); + return true; + } + + break; + } + + current_visual_line++; } } + mDesc.mSelectedItem = -1; return Super.MouseEvent(type, x, y); } - //============================================================================= // // @@ -453,7 +564,7 @@ class OptionMenu : Menu //============================================================================= // - // draws and/or measures the caption. + // draws and/or measures the caption. // //============================================================================= @@ -476,7 +587,6 @@ class OptionMenu : Menu // // //============================================================================= - override void Drawer () { int y = mDesc.mPosition; @@ -494,17 +604,26 @@ class OptionMenu : Menu int lastrow = screen.GetHeight() - OptionHeight() * CleanYfac_1; int i; + int lastDrawnItemIndex = -1; for (i = 0; i < mDesc.mItems.Size() && y <= lastrow; i++) { // Don't scroll the uppermost items if (i == mDesc.mScrollTop) { i += mDesc.mScrollPos; - if (i >= mDesc.mItems.Size()) break; // skipped beyond end of menu + if (i >= mDesc.mItems.Size()) break; // skipped beyond end of menu } + + if (!mDesc.mItems[i].Visible()) + { + continue; + } + + lastDrawnItemIndex = i; + bool isSelected = mDesc.mSelectedItem == i; int cur_indent = mDesc.mItems[i].Draw(mDesc, y, indent, isSelected); - if (cur_indent >= 0 && isSelected && mDesc.mItems[i].Selectable()) + if (cur_indent >= 0 && isSelected && mDesc.mItems[i].Selectable() && mDesc.mItems[i].Visible()) { if (((MenuTime() % 8) < 6) || GetCurrentMenu() != self) { @@ -515,8 +634,8 @@ class OptionMenu : Menu } CanScrollUp = (mDesc.mScrollPos > 0); - CanScrollDown = (i < mDesc.mItems.Size()); - VisBottom = i - 1; + CanScrollDown = LastVisibleItem() > i; + VisBottom = lastDrawnItemIndex; if (CanScrollUp) { From 1bbd6c136f3119bbc1288c81b0ef2bc157d335e6 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sat, 5 Jul 2025 13:19:58 -0400 Subject: [PATCH 257/384] Only block keydown --- src/common/menu/menu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/menu/menu.cpp b/src/common/menu/menu.cpp index a07dfd36f..6fbe1dd1b 100644 --- a/src/common/menu/menu.cpp +++ b/src/common/menu/menu.cpp @@ -680,7 +680,7 @@ bool M_Responder (event_t *ev) else if (menuactive != MENU_WaitKey && (ev->type == EV_KeyDown || ev->type == EV_KeyUp)) { // eat blocked controller events without dispatching them. - if (ev->data1 >= KEY_FIRSTJOYBUTTON && m_blockcontrollers) return true; + if (ev->data1 >= KEY_FIRSTJOYBUTTON && m_blockcontrollers && ev->type == EV_KeyDown) return true; keyup = ev->type == EV_KeyUp; From 6a126e616d5aa3a89c78bf71781aa1c65288fdee Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Fri, 4 Jul 2025 21:42:56 -0400 Subject: [PATCH 258/384] Enable gamepad by default --- src/common/engine/m_joy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp index 1e4b2f3e0..fe9e9e589 100644 --- a/src/common/engine/m_joy.cpp +++ b/src/common/engine/m_joy.cpp @@ -59,7 +59,7 @@ EXTERN_CVAR(Bool, joy_xinput) // PUBLIC DATA DEFINITIONS ------------------------------------------------- -CUSTOM_CVARD(Bool, use_joystick, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present") +CUSTOM_CVARD(Bool, use_joystick, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present") { #ifdef _WIN32 joy_ps2raw->Callback(); From 6ec17f28eb0e5b99a6ddba461f1bbd3ecffc2163 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sun, 29 Jun 2025 17:19:33 -0400 Subject: [PATCH 259/384] Cleanup: Alignment, long lines, Replace 0 with SDLK_UNKNOWN --- src/common/platform/posix/sdl/i_input.cpp | 192 ++++++++++++++-------- 1 file changed, 128 insertions(+), 64 deletions(-) diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp index 0148eaad2..e48160c79 100644 --- a/src/common/platform/posix/sdl/i_input.cpp +++ b/src/common/platform/posix/sdl/i_input.cpp @@ -61,74 +61,138 @@ extern int WaitingForKey; static const SDL_Keycode DIKToKeySym[256] = { - 0, SDLK_ESCAPE, SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6, - SDLK_7, SDLK_8, SDLK_9, SDLK_0,SDLK_MINUS, SDLK_EQUALS, SDLK_BACKSPACE, SDLK_TAB, - SDLK_q, SDLK_w, SDLK_e, SDLK_r, SDLK_t, SDLK_y, SDLK_u, SDLK_i, - SDLK_o, SDLK_p, SDLK_LEFTBRACKET, SDLK_RIGHTBRACKET, SDLK_RETURN, SDLK_LCTRL, SDLK_a, SDLK_s, - SDLK_d, SDLK_f, SDLK_g, SDLK_h, SDLK_j, SDLK_k, SDLK_l, SDLK_SEMICOLON, - SDLK_QUOTE, SDLK_BACKQUOTE, SDLK_LSHIFT, SDLK_BACKSLASH, SDLK_z, SDLK_x, SDLK_c, SDLK_v, - SDLK_b, SDLK_n, SDLK_m, SDLK_COMMA, SDLK_PERIOD, SDLK_SLASH, SDLK_RSHIFT, SDLK_KP_MULTIPLY, - SDLK_LALT, SDLK_SPACE, SDLK_CAPSLOCK, SDLK_F1, SDLK_F2, SDLK_F3, SDLK_F4, SDLK_F5, - SDLK_F6, SDLK_F7, SDLK_F8, SDLK_F9, SDLK_F10, SDLK_NUMLOCKCLEAR, SDLK_SCROLLLOCK, SDLK_KP_7, - SDLK_KP_8, SDLK_KP_9, SDLK_KP_MINUS, SDLK_KP_4, SDLK_KP_5, SDLK_KP_6, SDLK_KP_PLUS, SDLK_KP_1, - SDLK_KP_2, SDLK_KP_3, SDLK_KP_0, SDLK_KP_PERIOD, 0, 0, 0, SDLK_F11, - SDLK_F12, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, SDLK_F13, SDLK_F14, SDLK_F15, SDLK_F16, - SDLK_F17, SDLK_F18, SDLK_F19, SDLK_F20, SDLK_F21, SDLK_F22, SDLK_F23, SDLK_F24, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, SDLK_KP_EQUALS, 0, 0, - 0, SDLK_AT, SDLK_COLON, 0, 0, 0, 0, 0, - 0, 0, 0, 0, SDLK_KP_ENTER, SDLK_RCTRL, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, SDLK_KP_COMMA, 0, SDLK_KP_DIVIDE, 0, SDLK_SYSREQ, - SDLK_RALT, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, SDLK_PAUSE, 0, SDLK_HOME, - SDLK_UP, SDLK_PAGEUP, 0, SDLK_LEFT, 0, SDLK_RIGHT, 0, SDLK_END, - SDLK_DOWN, SDLK_PAGEDOWN, SDLK_INSERT, SDLK_DELETE, 0, 0, 0, 0, - 0, 0, 0, SDLK_LGUI, SDLK_RGUI, SDLK_MENU, SDLK_POWER, SDLK_SLEEP, - 0, 0, 0, 0, 0, SDLK_AC_SEARCH, SDLK_AC_BOOKMARKS, SDLK_AC_REFRESH, - SDLK_AC_STOP, SDLK_AC_FORWARD, SDLK_AC_BACK, SDLK_COMPUTER, SDLK_MAIL, SDLK_MEDIASELECT, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 + SDLK_UNKNOWN, SDLK_ESCAPE, SDLK_1, SDLK_2, + SDLK_3, SDLK_4, SDLK_5, SDLK_6, + SDLK_7, SDLK_8, SDLK_9, SDLK_0, + SDLK_MINUS, SDLK_EQUALS, SDLK_BACKSPACE, SDLK_TAB, + SDLK_q, SDLK_w, SDLK_e, SDLK_r, + SDLK_t, SDLK_y, SDLK_u, SDLK_i, + SDLK_o, SDLK_p, SDLK_LEFTBRACKET, SDLK_RIGHTBRACKET, + SDLK_RETURN, SDLK_LCTRL, SDLK_a, SDLK_s, + SDLK_d, SDLK_f, SDLK_g, SDLK_h, + SDLK_j, SDLK_k, SDLK_l, SDLK_SEMICOLON, + SDLK_QUOTE, SDLK_BACKQUOTE, SDLK_LSHIFT, SDLK_BACKSLASH, + SDLK_z, SDLK_x, SDLK_c, SDLK_v, + SDLK_b, SDLK_n, SDLK_m, SDLK_COMMA, + SDLK_PERIOD, SDLK_SLASH, SDLK_RSHIFT, SDLK_KP_MULTIPLY, + SDLK_LALT, SDLK_SPACE, SDLK_CAPSLOCK, SDLK_F1, + SDLK_F2, SDLK_F3, SDLK_F4, SDLK_F5, + SDLK_F6, SDLK_F7, SDLK_F8, SDLK_F9, + SDLK_F10, SDLK_NUMLOCKCLEAR, SDLK_SCROLLLOCK, SDLK_KP_7, + SDLK_KP_8, SDLK_KP_9, SDLK_KP_MINUS, SDLK_KP_4, + SDLK_KP_5, SDLK_KP_6, SDLK_KP_PLUS, SDLK_KP_1, + SDLK_KP_2, SDLK_KP_3, SDLK_KP_0, SDLK_KP_PERIOD, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_F11, + SDLK_F12, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_F13, SDLK_F14, SDLK_F15, SDLK_F16, + SDLK_F17, SDLK_F18, SDLK_F19, SDLK_F20, + SDLK_F21, SDLK_F22, SDLK_F23, SDLK_F24, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_KP_EQUALS, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_AT, SDLK_COLON, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_KP_ENTER, SDLK_RCTRL, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_KP_COMMA, + SDLK_UNKNOWN, SDLK_KP_DIVIDE, SDLK_UNKNOWN, SDLK_SYSREQ, + SDLK_RALT, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_PAUSE, SDLK_UNKNOWN, SDLK_HOME, + SDLK_UP, SDLK_PAGEUP, SDLK_UNKNOWN, SDLK_LEFT, + SDLK_UNKNOWN, SDLK_RIGHT, SDLK_UNKNOWN, SDLK_END, + SDLK_DOWN, SDLK_PAGEDOWN, SDLK_INSERT, SDLK_DELETE, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_LGUI, + SDLK_RGUI, SDLK_MENU, SDLK_POWER, SDLK_SLEEP, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_AC_SEARCH, SDLK_AC_BOOKMARKS, SDLK_AC_REFRESH, + SDLK_AC_STOP, SDLK_AC_FORWARD, SDLK_AC_BACK, SDLK_COMPUTER, + SDLK_MAIL, SDLK_MEDIASELECT, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, + SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN, SDLK_UNKNOWN }; static const SDL_Scancode DIKToKeyScan[256] = { - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_ESCAPE, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4, SDL_SCANCODE_5, SDL_SCANCODE_6, - SDL_SCANCODE_7, SDL_SCANCODE_8, SDL_SCANCODE_9, SDL_SCANCODE_0 ,SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS, SDL_SCANCODE_BACKSPACE, SDL_SCANCODE_TAB, - SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_E, SDL_SCANCODE_R, SDL_SCANCODE_T, SDL_SCANCODE_Y, SDL_SCANCODE_U, SDL_SCANCODE_I, - SDL_SCANCODE_O, SDL_SCANCODE_P, SDL_SCANCODE_LEFTBRACKET, SDL_SCANCODE_RIGHTBRACKET, SDL_SCANCODE_RETURN, SDL_SCANCODE_LCTRL, SDL_SCANCODE_A, SDL_SCANCODE_S, - SDL_SCANCODE_D, SDL_SCANCODE_F, SDL_SCANCODE_G, SDL_SCANCODE_H, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L, SDL_SCANCODE_SEMICOLON, - SDL_SCANCODE_APOSTROPHE, SDL_SCANCODE_GRAVE, SDL_SCANCODE_LSHIFT, SDL_SCANCODE_BACKSLASH, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_C, SDL_SCANCODE_V, - SDL_SCANCODE_B, SDL_SCANCODE_N, SDL_SCANCODE_M, SDL_SCANCODE_COMMA, SDL_SCANCODE_PERIOD, SDL_SCANCODE_SLASH, SDL_SCANCODE_RSHIFT, SDL_SCANCODE_KP_MULTIPLY, - SDL_SCANCODE_LALT, SDL_SCANCODE_SPACE, SDL_SCANCODE_CAPSLOCK, SDL_SCANCODE_F1, SDL_SCANCODE_F2, SDL_SCANCODE_F3, SDL_SCANCODE_F4, SDL_SCANCODE_F5, - SDL_SCANCODE_F6, SDL_SCANCODE_F7, SDL_SCANCODE_F8, SDL_SCANCODE_F9, SDL_SCANCODE_F10, SDL_SCANCODE_NUMLOCKCLEAR, SDL_SCANCODE_SCROLLLOCK, SDL_SCANCODE_KP_7, - SDL_SCANCODE_KP_8, SDL_SCANCODE_KP_9, SDL_SCANCODE_KP_MINUS, SDL_SCANCODE_KP_4, SDL_SCANCODE_KP_5, SDL_SCANCODE_KP_6, SDL_SCANCODE_KP_PLUS, SDL_SCANCODE_KP_1, - SDL_SCANCODE_KP_2, SDL_SCANCODE_KP_3, SDL_SCANCODE_KP_0, SDL_SCANCODE_KP_PERIOD, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F11, - SDL_SCANCODE_F12, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F13, SDL_SCANCODE_F14, SDL_SCANCODE_F15, SDL_SCANCODE_F16, - SDL_SCANCODE_F17, SDL_SCANCODE_F18, SDL_SCANCODE_F19, SDL_SCANCODE_F20, SDL_SCANCODE_F21, SDL_SCANCODE_F22, SDL_SCANCODE_F23, SDL_SCANCODE_F24, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_EQUALS, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_ENTER, SDL_SCANCODE_RCTRL, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_COMMA, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_DIVIDE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_SYSREQ, - SDL_SCANCODE_RALT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_PAUSE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_HOME, - SDL_SCANCODE_UP, SDL_SCANCODE_PAGEUP, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_END, - SDL_SCANCODE_DOWN, SDL_SCANCODE_PAGEDOWN, SDL_SCANCODE_INSERT, SDL_SCANCODE_DELETE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LGUI, SDL_SCANCODE_RGUI, SDL_SCANCODE_MENU, SDL_SCANCODE_POWER, SDL_SCANCODE_SLEEP, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_AC_SEARCH, SDL_SCANCODE_AC_BOOKMARKS, SDL_SCANCODE_AC_REFRESH, - SDL_SCANCODE_AC_STOP, SDL_SCANCODE_AC_FORWARD, SDL_SCANCODE_AC_BACK, SDL_SCANCODE_COMPUTER, SDL_SCANCODE_MAIL, SDL_SCANCODE_MEDIASELECT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, - SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_ESCAPE, SDL_SCANCODE_1, SDL_SCANCODE_2, + SDL_SCANCODE_3, SDL_SCANCODE_4, SDL_SCANCODE_5, SDL_SCANCODE_6, + SDL_SCANCODE_7, SDL_SCANCODE_8, SDL_SCANCODE_9, SDL_SCANCODE_0, + SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS, SDL_SCANCODE_BACKSPACE, SDL_SCANCODE_TAB, + SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_E, SDL_SCANCODE_R, + SDL_SCANCODE_T, SDL_SCANCODE_Y, SDL_SCANCODE_U, SDL_SCANCODE_I, + SDL_SCANCODE_O, SDL_SCANCODE_P, SDL_SCANCODE_LEFTBRACKET, SDL_SCANCODE_RIGHTBRACKET, + SDL_SCANCODE_RETURN, SDL_SCANCODE_LCTRL, SDL_SCANCODE_A, SDL_SCANCODE_S, + SDL_SCANCODE_D, SDL_SCANCODE_F, SDL_SCANCODE_G, SDL_SCANCODE_H, + SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L, SDL_SCANCODE_SEMICOLON, + SDL_SCANCODE_APOSTROPHE, SDL_SCANCODE_GRAVE, SDL_SCANCODE_LSHIFT, SDL_SCANCODE_BACKSLASH, + SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_C, SDL_SCANCODE_V, + SDL_SCANCODE_B, SDL_SCANCODE_N, SDL_SCANCODE_M, SDL_SCANCODE_COMMA, + SDL_SCANCODE_PERIOD, SDL_SCANCODE_SLASH, SDL_SCANCODE_RSHIFT, SDL_SCANCODE_KP_MULTIPLY, + SDL_SCANCODE_LALT, SDL_SCANCODE_SPACE, SDL_SCANCODE_CAPSLOCK, SDL_SCANCODE_F1, + SDL_SCANCODE_F2, SDL_SCANCODE_F3, SDL_SCANCODE_F4, SDL_SCANCODE_F5, + SDL_SCANCODE_F6, SDL_SCANCODE_F7, SDL_SCANCODE_F8, SDL_SCANCODE_F9, + SDL_SCANCODE_F10, SDL_SCANCODE_NUMLOCKCLEAR, SDL_SCANCODE_SCROLLLOCK, SDL_SCANCODE_KP_7, + SDL_SCANCODE_KP_8, SDL_SCANCODE_KP_9, SDL_SCANCODE_KP_MINUS, SDL_SCANCODE_KP_4, + SDL_SCANCODE_KP_5, SDL_SCANCODE_KP_6, SDL_SCANCODE_KP_PLUS, SDL_SCANCODE_KP_1, + SDL_SCANCODE_KP_2, SDL_SCANCODE_KP_3, SDL_SCANCODE_KP_0, SDL_SCANCODE_KP_PERIOD, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_F11, + SDL_SCANCODE_F12, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_F13, SDL_SCANCODE_F14, SDL_SCANCODE_F15, SDL_SCANCODE_F16, + SDL_SCANCODE_F17, SDL_SCANCODE_F18, SDL_SCANCODE_F19, SDL_SCANCODE_F20, + SDL_SCANCODE_F21, SDL_SCANCODE_F22, SDL_SCANCODE_F23, SDL_SCANCODE_F24, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_EQUALS, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_KP_ENTER, SDL_SCANCODE_RCTRL, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_COMMA, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_DIVIDE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_SYSREQ, + SDL_SCANCODE_RALT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_PAUSE, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_HOME, + SDL_SCANCODE_UP, SDL_SCANCODE_PAGEUP, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LEFT, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_END, + SDL_SCANCODE_DOWN, SDL_SCANCODE_PAGEDOWN, SDL_SCANCODE_INSERT, SDL_SCANCODE_DELETE, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_LGUI, + SDL_SCANCODE_RGUI, SDL_SCANCODE_MENU, SDL_SCANCODE_POWER, SDL_SCANCODE_SLEEP, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_AC_SEARCH, SDL_SCANCODE_AC_BOOKMARKS, SDL_SCANCODE_AC_REFRESH, + SDL_SCANCODE_AC_STOP, SDL_SCANCODE_AC_FORWARD, SDL_SCANCODE_AC_BACK, SDL_SCANCODE_COMPUTER, + SDL_SCANCODE_MAIL, SDL_SCANCODE_MEDIASELECT, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, + SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_UNKNOWN }; static TMap InitKeySymMap () From d75d3aacf937753e9afb4c5b5356324747d519cc Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sun, 29 Jun 2025 18:32:21 -0400 Subject: [PATCH 260/384] SDL2 GameController API utilized --- src/common/console/c_bind.cpp | 4 +- src/common/console/keydef.h | 9 +- src/common/platform/posix/sdl/i_input.cpp | 42 ++- src/common/platform/posix/sdl/i_joystick.cpp | 358 ++++++++++++------- 4 files changed, 275 insertions(+), 138 deletions(-) diff --git a/src/common/console/c_bind.cpp b/src/common/console/c_bind.cpp index fe794c736..078a9a2b1 100644 --- a/src/common/console/c_bind.cpp +++ b/src/common/console/c_bind.cpp @@ -147,7 +147,9 @@ const char *KeyNames[NUM_KEYS] = "DPadUp","DPadDown","DPadLeft","DPadRight", // Gamepad buttons "Pad_Start","Pad_Back","LThumb","RThumb", "LShoulder","RShoulder","LTrigger","RTrigger", - "Pad_A", "Pad_B", "Pad_X", "Pad_Y" + "Pad_A", "Pad_B", "Pad_X", "Pad_Y", + "Paddle_1", "Paddle_2", "Paddle_3", "Paddle_4", + "Guide", "Pad_Misc", "Pad_Touchpad" }; FKeyBindings Bindings; diff --git a/src/common/console/keydef.h b/src/common/console/keydef.h index 108806774..620b5f5e7 100644 --- a/src/common/console/keydef.h +++ b/src/common/console/keydef.h @@ -134,8 +134,15 @@ enum EKeyCodes KEY_PAD_B = 0x1C1, KEY_PAD_X = 0x1C2, KEY_PAD_Y = 0x1C3, + KEY_PAD_PADDLE1 = 0x1C4, + KEY_PAD_PADDLE2 = 0x1C5, + KEY_PAD_PADDLE3 = 0x1C6, + KEY_PAD_PADDLE4 = 0x1C7, + KEY_PAD_GUIDE = 0x1C8, + KEY_PAD_MISC1 = 0x1C9, + KEY_PAD_TOUCHPAD = 0x1CA, - NUM_KEYS = 0x1C4, + NUM_KEYS = 0x1CB, NUM_JOYAXISBUTTONS = 8, }; diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp index e48160c79..d9c05a666 100644 --- a/src/common/platform/posix/sdl/i_input.cpp +++ b/src/common/platform/posix/sdl/i_input.cpp @@ -31,6 +31,9 @@ ** */ #include +#include +#include "c_cvars.h" +#include "dobject.h" #include "m_argv.h" #include "m_joy.h" #include "v_video.h" @@ -56,7 +59,6 @@ static bool NativeMouse = true; CVAR (Bool, use_mouse, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) - extern int WaitingForKey; static const SDL_Keycode DIKToKeySym[256] = @@ -527,14 +529,52 @@ void MessagePump (const SDL_Event &sev) case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: + if (SDL_IsGameController(sev.jdevice.which)) + break; // let SDL_CONTROLLERBUTTON* handle this event.type = sev.type == SDL_JOYBUTTONDOWN ? EV_KeyDown : EV_KeyUp; event.data1 = KEY_FIRSTJOYBUTTON + sev.jbutton.button; if(event.data1 != 0) D_PostEvent(&event); break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + event.type = sev.type == SDL_CONTROLLERBUTTONDOWN ? EV_KeyDown : EV_KeyUp; + switch (sev.cbutton.button) + { + case SDL_CONTROLLER_BUTTON_A: event.data1 = KEY_PAD_A; break; + case SDL_CONTROLLER_BUTTON_B: event.data1 = KEY_PAD_B; break; + case SDL_CONTROLLER_BUTTON_X: event.data1 = KEY_PAD_X; break; + case SDL_CONTROLLER_BUTTON_Y: event.data1 = KEY_PAD_Y; break; + case SDL_CONTROLLER_BUTTON_BACK: event.data1 = KEY_PAD_BACK; break; + case SDL_CONTROLLER_BUTTON_GUIDE: event.data1 = KEY_PAD_GUIDE; break; + case SDL_CONTROLLER_BUTTON_START: event.data1 = KEY_PAD_START; break; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: event.data1 = KEY_PAD_LTHUMB; break; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: event.data1 = KEY_PAD_RTHUMB; break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: event.data1 = KEY_PAD_LSHOULDER; break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: event.data1 = KEY_PAD_RSHOULDER; break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: event.data1 = KEY_PAD_DPAD_UP; break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: event.data1 = KEY_PAD_DPAD_DOWN; break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: event.data1 = KEY_PAD_DPAD_LEFT; break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: event.data1 = KEY_PAD_DPAD_RIGHT; break; + case SDL_CONTROLLER_BUTTON_MISC1: event.data1 = KEY_PAD_MISC1; break; + case SDL_CONTROLLER_BUTTON_PADDLE1: event.data1 = KEY_PAD_PADDLE1; break; + case SDL_CONTROLLER_BUTTON_PADDLE2: event.data1 = KEY_PAD_PADDLE2; break; + case SDL_CONTROLLER_BUTTON_PADDLE3: event.data1 = KEY_PAD_PADDLE3; break; + case SDL_CONTROLLER_BUTTON_PADDLE4: event.data1 = KEY_PAD_PADDLE4; break; + case SDL_CONTROLLER_BUTTON_TOUCHPAD: event.data1 = KEY_PAD_TOUCHPAD; break; + default: event.data1 = 0; + } + if(event.data1 != 0) + D_PostEvent(&event); + break; + case SDL_JOYDEVICEADDED: case SDL_JOYDEVICEREMOVED: + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + if ((sev.type == SDL_JOYDEVICEADDED || sev.type == SDL_JOYDEVICEREMOVED) && SDL_IsGameController(sev.jdevice.which)) + break; // skip double event I_UpdateDeviceList(); event.type = EV_DeviceChange; D_PostEvent (&event); diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index 8dc9fd8ac..b276f0fb4 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -31,10 +31,12 @@ ** */ #include +#include #include "basics.h" #include "cmdlib.h" +#include "d_eventbase.h" #include "m_joy.h" #include "keydef.h" @@ -48,30 +50,54 @@ class SDLInputJoystick: public IJoystickConfig public: SDLInputJoystick(int DeviceIndex) : DeviceIndex(DeviceIndex), Multiplier(1.0f) , Enabled(true) { - Device = SDL_JoystickOpen(DeviceIndex); - if(Device != NULL) + if (SDL_IsGameController(DeviceIndex)) { - NumAxes = SDL_JoystickNumAxes(Device); - NumHats = SDL_JoystickNumHats(Device); + Mapping = SDL_GameControllerOpen(DeviceIndex); - SetDefaultConfig(); + DefaultAxes = DefaultControllerAxes; + DefaultAxesCount = sizeof(DefaultControllerAxes) / sizeof(EJoyAxis); + + if(Mapping != NULL) + { + NumAxes = SDL_CONTROLLER_AXIS_MAX; + NumHats = 0; + + SetDefaultConfig(); + } + } + else + { + Device = SDL_JoystickOpen(DeviceIndex); + DefaultAxes = DefaultJoystickAxes; + DefaultAxesCount = sizeof(DefaultJoystickAxes) / sizeof(EJoyAxis); + + if(Device != NULL) + { + NumAxes = SDL_JoystickNumAxes(Device); + NumHats = SDL_JoystickNumHats(Device); + + SetDefaultConfig(); + } } } ~SDLInputJoystick() { - if(Device != NULL) + if(Device != NULL || Mapping != NULL) M_SaveJoystickConfig(this); + SDL_GameControllerClose(Mapping); SDL_JoystickClose(Device); } bool IsValid() const { - return Device != NULL; + return Device != NULL || Mapping != NULL; } FString GetName() { - return SDL_JoystickName(Device); + return (Mapping) + ? SDL_GameControllerName(Mapping) + : SDL_JoystickName(Device); } float GetSensitivity() { @@ -127,7 +153,7 @@ public: } bool IsAxisMapDefault(int axis) { - if(axis >= 5) + if(axis >= DefaultAxesCount) return Axes[axis].GameAxis == JOYAXIS_None; return Axes[axis].GameAxis == DefaultAxes[axis]; } @@ -141,18 +167,31 @@ public: for(int i = 0;i < GetNumAxes();i++) { AxisInfo info; - if(i < NumAxes) - info.Name.Format("Axis %d", i+1); - else - info.Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y'); + + if (Mapping) { + switch(i) { + case SDL_CONTROLLER_AXIS_LEFTX: info.Name = "Left Stick X"; break; + case SDL_CONTROLLER_AXIS_LEFTY: info.Name = "Left Stick Y"; break; + case SDL_CONTROLLER_AXIS_RIGHTX: info.Name = "Right Stick X"; break; + case SDL_CONTROLLER_AXIS_RIGHTY: info.Name = "Right Stick Y"; break; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: info.Name = "Left Trigger"; break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: info.Name = "Right Trigger"; break; + default: info.Name.Format("Axis %d", i+1); break; + } + } else { + if(i < NumAxes) + info.Name.Format("Axis %d", i+1); + else + info.Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y'); + } + info.DeadZone = DEFAULT_DEADZONE; info.Multiplier = 1.0f; info.Value = 0.0; info.ButtonValue = 0; - if(i >= 5) - info.GameAxis = JOYAXIS_None; - else - info.GameAxis = DefaultAxes[i]; + + info.GameAxis = (i < DefaultAxesCount)? DefaultAxes[i]: JOYAXIS_None; + Axes.Push(info); } } @@ -161,7 +200,7 @@ public: { return Enabled; } - + void SetEnabled(bool enabled) { Enabled = enabled; @@ -188,9 +227,173 @@ public: } } - void ProcessInput() + void ProcessInput(); + +protected: + struct AxisInfo { - uint8_t buttonstate; + FString Name; + float DeadZone; + float Multiplier; + EJoyAxis GameAxis; + double Value; + uint8_t ButtonValue; + }; + static const EJoyAxis DefaultJoystickAxes[5]; + static const EJoyAxis DefaultControllerAxes[6]; + const EJoyAxis * DefaultAxes; + int DefaultAxesCount; + + int DeviceIndex; + SDL_Joystick *Device; + SDL_GameController *Mapping; + + float Multiplier; + bool Enabled; + TArray Axes; + int NumAxes; + int NumHats; + + friend class SDLInputJoystickManager; +}; + +// [Nash 4 Feb 2024] seems like on Linux, the third axis is actually the Left Trigger, resulting in the player uncontrollably looking upwards. +const EJoyAxis SDLInputJoystick::DefaultJoystickAxes[5] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_None, JOYAXIS_Yaw, JOYAXIS_Pitch}; + +// Defaults if we have access to the Gamepad API for this device +const EJoyAxis SDLInputJoystick::DefaultControllerAxes[6] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_Yaw, JOYAXIS_Pitch, JOYAXIS_None, JOYAXIS_None}; + + + +class SDLInputJoystickManager +{ +public: + SDLInputJoystickManager() + { + this->UpdateDeviceList(); + } + + void UpdateDeviceList() + { + Joysticks.DeleteAndClear(); + for(int i = 0; i < SDL_NumJoysticks(); i++) + { + SDLInputJoystick *device = new SDLInputJoystick(i); + if(device->IsValid()) + Joysticks.Push(device); + else + delete device; + } + } + + void AddAxes(float axes[NUM_JOYAXIS]) + { + for(unsigned int i = 0;i < Joysticks.Size();i++) + Joysticks[i]->AddAxes(axes); + } + + void GetDevices(TArray &sticks) + { + for(unsigned int i = 0;i < Joysticks.Size();i++) + { + M_LoadJoystickConfig(Joysticks[i]); + sticks.Push(Joysticks[i]); + } + } + + void ProcessInput() const + { + for(unsigned int i = 0;i < Joysticks.Size();++i) + if(Joysticks[i]->Enabled) Joysticks[i]->ProcessInput(); + } + +protected: + TDeletingArray Joysticks; +}; +static SDLInputJoystickManager *JoystickManager; + +void I_StartupJoysticks() +{ +#ifndef NO_SDL_JOYSTICK + if(SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) >= 0) + JoystickManager = new SDLInputJoystickManager(); +#endif +} +void I_ShutdownInput() +{ + if(JoystickManager) + { + delete JoystickManager; + SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER); + } +} + +void I_GetJoysticks(TArray &sticks) +{ + sticks.Clear(); + + if (JoystickManager) + JoystickManager->GetDevices(sticks); +} + +void I_GetAxes(float axes[NUM_JOYAXIS]) +{ + for (int i = 0; i < NUM_JOYAXIS; ++i) + { + axes[i] = 0; + } + if (use_joystick && JoystickManager) + { + JoystickManager->AddAxes(axes); + } +} + +void I_ProcessJoysticks() +{ + if (use_joystick && JoystickManager) + JoystickManager->ProcessInput(); +} + +void PostKeyEvent(bool down, EKeyCodes which) +{ + event_t event = { 0,0,0,0,0,0,0 }; + event.type = down ? EV_KeyDown : EV_KeyUp; + event.data1 = which; + D_PostEvent(&event); +} + +void SDLInputJoystick::ProcessInput() { + uint8_t buttonstate; + + if (Mapping) + { + // GameController API available + + auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > MIN_DEADZONE; + auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > MIN_DEADZONE; + + for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i) + { + buttonstate = 0; + + Axes[i].Value = SDL_GameControllerGetAxis(Mapping, static_cast(i))/32767.0; + Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate); + } + + auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > MIN_DEADZONE; + auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > MIN_DEADZONE; + + if (lastTriggerL != currTriggerL) PostKeyEvent(currTriggerL, KEY_PAD_LTRIGGER); + if (lastTriggerR != currTriggerR) PostKeyEvent(currTriggerR, KEY_PAD_RTRIGGER); + + buttonstate = Joy_XYAxesToButtons(Axes[0].Value, Axes[1].Value); + Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); + Axes[0].ButtonValue = buttonstate; + + } + else + { + // Joystick API fallback for (int i = 0; i < NumAxes; ++i) { @@ -248,121 +451,6 @@ public: } } } - -protected: - struct AxisInfo - { - FString Name; - float DeadZone; - float Multiplier; - EJoyAxis GameAxis; - double Value; - uint8_t ButtonValue; - }; - static const EJoyAxis DefaultAxes[5]; - - int DeviceIndex; - SDL_Joystick *Device; - - float Multiplier; - bool Enabled; - TArray Axes; - int NumAxes; - int NumHats; - - friend class SDLInputJoystickManager; -}; - -// [Nash 4 Feb 2024] seems like on Linux, the third axis is actually the Left Trigger, resulting in the player uncontrollably looking upwards. -const EJoyAxis SDLInputJoystick::DefaultAxes[5] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_None, JOYAXIS_Yaw, JOYAXIS_Pitch}; - -class SDLInputJoystickManager -{ -public: - SDLInputJoystickManager() - { - this->UpdateDeviceList(); - } - - void UpdateDeviceList() - { - Joysticks.DeleteAndClear(); - for(int i = 0; i < SDL_NumJoysticks(); i++) - { - SDLInputJoystick *device = new SDLInputJoystick(i); - if(device->IsValid()) - Joysticks.Push(device); - else - delete device; - } - } - - void AddAxes(float axes[NUM_JOYAXIS]) - { - for(unsigned int i = 0;i < Joysticks.Size();i++) - Joysticks[i]->AddAxes(axes); - } - - void GetDevices(TArray &sticks) - { - for(unsigned int i = 0;i < Joysticks.Size();i++) - { - M_LoadJoystickConfig(Joysticks[i]); - sticks.Push(Joysticks[i]); - } - } - - void ProcessInput() const - { - for(unsigned int i = 0;i < Joysticks.Size();++i) - if(Joysticks[i]->Enabled) Joysticks[i]->ProcessInput(); - } - -protected: - TDeletingArray Joysticks; -}; -static SDLInputJoystickManager *JoystickManager; - -void I_StartupJoysticks() -{ -#ifndef NO_SDL_JOYSTICK - if(SDL_InitSubSystem(SDL_INIT_JOYSTICK) >= 0) - JoystickManager = new SDLInputJoystickManager(); -#endif -} -void I_ShutdownInput() -{ - if(JoystickManager) - { - delete JoystickManager; - SDL_QuitSubSystem(SDL_INIT_JOYSTICK); - } -} - -void I_GetJoysticks(TArray &sticks) -{ - sticks.Clear(); - - if (JoystickManager) - JoystickManager->GetDevices(sticks); -} - -void I_GetAxes(float axes[NUM_JOYAXIS]) -{ - for (int i = 0; i < NUM_JOYAXIS; ++i) - { - axes[i] = 0; - } - if (use_joystick && JoystickManager) - { - JoystickManager->AddAxes(axes); - } -} - -void I_ProcessJoysticks() -{ - if (use_joystick && JoystickManager) - JoystickManager->ProcessInput(); } IJoystickConfig *I_UpdateDeviceList() From 92e66479fcb0849e6459311eda509341ee2d3d53 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 17:46:19 -0400 Subject: [PATCH 261/384] Added stubs and helper functions --- src/common/engine/m_joy.cpp | 68 ++++++++++-- src/common/engine/m_joy.h | 31 ++++++ .../platform/posix/cocoa/i_joystick.cpp | 44 ++++++++ src/common/platform/posix/sdl/i_joystick.cpp | 29 +++++ src/common/platform/win32/i_dijoy.cpp | 97 ++++++++++++++++- src/common/platform/win32/i_rawps2.cpp | 101 ++++++++++++++++-- src/common/platform/win32/i_xinput.cpp | 93 ++++++++++++++++ 7 files changed, 443 insertions(+), 20 deletions(-) diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp index fe9e9e589..0fce7d994 100644 --- a/src/common/engine/m_joy.cpp +++ b/src/common/engine/m_joy.cpp @@ -278,6 +278,48 @@ double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons) return axisval; } +//=========================================================================== +// +// Joy_ApplyResponseCurveBezier +// +// Applies cubic bezier easing function +// Curve is defined by control points [(0,0) (x1,y1) (x2,y2) (1,1)] +// https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/cubic-bezier +// +//=========================================================================== + +double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input) +{ + // clamp + trivial cases + if (input == 0) return 0; + double sign = (input >= 0)? 1.0: -1.0; + input = abs(input); + input = (input > 1.0)? 1.0: input; + if (input == 1.0) return sign*input; + + double t = input, T; + float x1 = curve.x1, y1 = curve.y1, x2 = curve.x2, y2 = curve.y2; + + const int max_iter = 4; + for (auto i = 0; i < max_iter; i++) + { + T = 1-t; + + double x = 3*T*T*t*x1 + 3*T*t*t*x2 + t*t*t; + double dx = 3*T*T*x1 + 6*T*t*(x2-x1) + 3*t*t*(1-x2); + + // no div by 0 + if (abs(dx) < 0.00001) break; + + t = clamp(t - (x-input)/dx, 0.0, 1.0); + } + + T = 1-t; + t = 3*T*T*t*y1 + 3*T*t*t*y2 + t*t*t; + + return sign*t; +} + //=========================================================================== // // Joy_XYAxesToButtons @@ -315,6 +357,22 @@ int Joy_XYAxesToButtons(double x, double y) return JoyAngleButtons[int(rad) & 7]; } +//=========================================================================== +// +// Joy_GenerateButtonEvent +// +// Send either a button up or button down event for supplied key code +// +//=========================================================================== + +void Joy_GenerateButtonEvent(bool down, EKeyCodes which) +{ + event_t event = { 0,0,0,0,0,0,0 }; + event.type = down ? EV_KeyDown : EV_KeyUp; + event.data1 = which; + D_PostEvent(&event); +} + //=========================================================================== // // Joy_GenerateButtonEvents @@ -330,15 +388,12 @@ void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, in int changed = oldbuttons ^ newbuttons; if (changed != 0) { - event_t ev = { 0, 0, 0, 0, 0, 0, 0 }; int mask = 1; for (int j = 0; j < numbuttons; mask <<= 1, ++j) { if (changed & mask) { - ev.data1 = base + j; - ev.type = (newbuttons & mask) ? EV_KeyDown : EV_KeyUp; - D_PostEvent(&ev); + Joy_GenerateButtonEvent(newbuttons & mask, static_cast(base + j)); } } } @@ -349,15 +404,12 @@ void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, co int changed = oldbuttons ^ newbuttons; if (changed != 0) { - event_t ev = { 0, 0, 0, 0, 0, 0, 0 }; int mask = 1; for (int j = 0; j < numbuttons; mask <<= 1, ++j) { if (changed & mask) { - ev.data1 = keys[j]; - ev.type = (newbuttons & mask) ? EV_KeyDown : EV_KeyUp; - D_PostEvent(&ev); + Joy_GenerateButtonEvent(newbuttons & mask, static_cast(keys[j])); } } } diff --git a/src/common/engine/m_joy.h b/src/common/engine/m_joy.h index e8d9d3b13..60321785a 100644 --- a/src/common/engine/m_joy.h +++ b/src/common/engine/m_joy.h @@ -2,9 +2,30 @@ #define M_JOY_H #include "basics.h" +#include "keydef.h" #include "tarray.h" #include "c_cvars.h" +union CubicBezier { + struct { + float x1; + float y1; + float x2; + float y2; + }; + float pts[4]; +}; + +enum EJoyCurve { + JOYCURVE_CUSTOM = -1, + JOYCURVE_DEFAULT, + JOYCURVE_LINEAR, + JOYCURVE_QUADRATIC, + JOYCURVE_CUBIC, + + NUM_JOYCURVE +}; + enum EJoyAxis { JOYAXIS_None = -1, @@ -31,10 +52,16 @@ struct IJoystickConfig virtual EJoyAxis GetAxisMap(int axis) = 0; virtual const char *GetAxisName(int axis) = 0; virtual float GetAxisScale(int axis) = 0; + virtual float GetAxisDigitalThreshold(int axis) = 0; + virtual EJoyCurve GetAxisResponseCurve(int axis) = 0; + virtual float GetAxisResponseCurvePoint(int axis, int point) = 0; virtual void SetAxisDeadZone(int axis, float zone) = 0; virtual void SetAxisMap(int axis, EJoyAxis gameaxis) = 0; virtual void SetAxisScale(int axis, float scale) = 0; + virtual void SetAxisDigitalThreshold(int axis, float threshold) = 0; + virtual void SetAxisResponseCurve(int axis, EJoyCurve preset) = 0; + virtual void SetAxisResponseCurvePoint(int axis, int point, float value) = 0; virtual bool GetEnabled() = 0; virtual void SetEnabled(bool enabled) = 0; @@ -48,6 +75,8 @@ struct IJoystickConfig virtual bool IsAxisDeadZoneDefault(int axis) = 0; virtual bool IsAxisMapDefault(int axis) = 0; virtual bool IsAxisScaleDefault(int axis) = 0; + virtual bool IsAxisDigitalThresholdDefault(int axis) = 0; + virtual bool IsAxisResponseCurveDefault(int axis) = 0; virtual void SetDefaultConfig() = 0; virtual FString GetIdentifier() = 0; @@ -58,10 +87,12 @@ EXTERN_CVAR(Bool, use_joystick); bool M_LoadJoystickConfig(IJoystickConfig *joy); void M_SaveJoystickConfig(IJoystickConfig *joy); +void Joy_GenerateButtonEvent(bool down, EKeyCodes which); void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, int base); void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, const int *keys); int Joy_XYAxesToButtons(double x, double y); double Joy_RemoveDeadZone(double axisval, double deadzone, uint8_t *buttons); +double Joy_ApplyResponseCurveBezier(const CubicBezier &curve, double input); // These ought to be provided by a system-specific i_input.cpp. void I_GetAxes(float axes[NUM_JOYAXIS]); diff --git a/src/common/platform/posix/cocoa/i_joystick.cpp b/src/common/platform/posix/cocoa/i_joystick.cpp index 44d1fb961..e338ad8dd 100644 --- a/src/common/platform/posix/cocoa/i_joystick.cpp +++ b/src/common/platform/posix/cocoa/i_joystick.cpp @@ -94,15 +94,23 @@ public: virtual EJoyAxis GetAxisMap(int axis); virtual const char* GetAxisName(int axis); virtual float GetAxisScale(int axis); + float GetAxisDigitalThreshold(int axis); + EJoyCurve GetAxisResponseCurve(int axis); + float GetAxisResponseCurvePoint(int axis, int point); virtual void SetAxisDeadZone(int axis, float deadZone); virtual void SetAxisMap(int axis, EJoyAxis gameAxis); virtual void SetAxisScale(int axis, float scale); + void SetAxisDigitalThreshold(int axis, float threshold); + void SetAxisResponseCurve(int axis, EJoyCurve preset); + void SetAxisResponseCurvePoint(int axis, int point, float value); virtual bool IsSensitivityDefault(); virtual bool IsAxisDeadZoneDefault(int axis); virtual bool IsAxisMapDefault(int axis); virtual bool IsAxisScaleDefault(int axis); + bool IsAxisDigitalThresholdDefault(int axis); + bool IsAxisResponseCurveDefault(int axis); virtual bool GetEnabled(); virtual void SetEnabled(bool enabled); @@ -386,6 +394,21 @@ float IOKitJoystick::GetAxisScale(int axis) return IS_AXIS_VALID ? m_axes[axis].sensitivity : 0.0f; } +float IOKitJoystick::GetAxisDigitalThreshold(int axis) +{ + return 0; +} + +EJoyCurve IOKitJoystick::GetAxisResponseCurve(int axis) +{ + return JOYCURVE_DEFAULT; +} + +float IOKitJoystick::GetAxisResponseCurvePoint(int axis, int point) +{ + return 0; +} + void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone) { if (IS_AXIS_VALID) @@ -412,6 +435,17 @@ void IOKitJoystick::SetAxisScale(int axis, float scale) } } +void IOKitJoystick::SetAxisDigitalThreshold(int axis, float threshold) +{ +} + +void IOKitJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset) +{ +} + +void IOKitJoystick::SetAxisResponseCurvePoint(int axis, int point, float value) +{ +} bool IOKitJoystick::IsSensitivityDefault() { @@ -425,6 +459,16 @@ bool IOKitJoystick::IsAxisDeadZoneDefault(int axis) : true; } +bool IOKitJoystick::IsAxisDigitalThresholdDefault(int axis) +{ + return true; +} + +bool IOKitJoystick::IsAxisResponseCurveDefault(int axis) +{ + return true; +} + bool IOKitJoystick::IsAxisMapDefault(int axis) { return IS_AXIS_VALID diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index b276f0fb4..6654bcae3 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -128,6 +128,18 @@ public: { return Axes[axis].Multiplier; } + float GetAxisDigitalThreshold(int axis) + { + return 0; + } + EJoyCurve GetAxisResponseCurve(int axis) + { + return JOYCURVE_DEFAULT; + } + float GetAxisResponseCurvePoint(int axis, int point) + { + return 0; + }; void SetAxisDeadZone(int axis, float zone) { @@ -141,6 +153,15 @@ public: { Axes[axis].Multiplier = scale; } + void SetAxisDigitalThreshold(int axis, float threshold) + { + } + void SetAxisResponseCurve(int axis, EJoyCurve preset) + { + } + void SetAxisResponseCurvePoint(int axis, int point, float value) + { + } // Used by the saver to not save properties that are at their defaults. bool IsSensitivityDefault() @@ -161,6 +182,14 @@ public: { return Axes[axis].Multiplier == 1.0f; } + bool IsAxisDigitalThresholdDefault(int axis) + { + return true; + } + bool IsAxisResponseCurveDefault(int axis) + { + return true; + } void SetDefaultConfig() { diff --git a/src/common/platform/win32/i_dijoy.cpp b/src/common/platform/win32/i_dijoy.cpp index 33c4574c9..d3ada1e22 100644 --- a/src/common/platform/win32/i_dijoy.cpp +++ b/src/common/platform/win32/i_dijoy.cpp @@ -170,15 +170,23 @@ public: EJoyAxis GetAxisMap(int axis); const char *GetAxisName(int axis); float GetAxisScale(int axis); + float GetAxisDigitalThreshold(int axis); + EJoyCurve GetAxisResponseCurve(int axis); + float GetAxisResponseCurvePoint(int axis, int point); void SetAxisDeadZone(int axis, float deadzone); void SetAxisMap(int axis, EJoyAxis gameaxis); void SetAxisScale(int axis, float scale); + void SetAxisDigitalThreshold(int axis, float threshold); + void SetAxisResponseCurve(int axis, EJoyCurve preset); + void SetAxisResponseCurvePoint(int axis, int point, float value); bool IsSensitivityDefault(); bool IsAxisDeadZoneDefault(int axis); bool IsAxisMapDefault(int axis); bool IsAxisScaleDefault(int axis); + bool IsAxisDigitalThresholdDefault(int axis); + bool IsAxisResponseCurveDefault(int axis); bool GetEnabled(); void SetEnabled(bool enabled); @@ -923,6 +931,39 @@ float FDInputJoystick::GetAxisScale(int axis) return Axes[axis].Multiplier; } +//=========================================================================== +// +// FDInputJoystick :: GetAxisDigitalThreshold +// +//=========================================================================== + +float FDInputJoystick::GetAxisDigitalThreshold(int axis) +{ + return 0; +} + +//=========================================================================== +// +// FDInputJoystick :: GetAxisResponseCurve +// +//=========================================================================== + +EJoyCurve FDInputJoystick::GetAxisResponseCurve(int axis) +{ + return JOYCURVE_DEFAULT; +} + +//=========================================================================== +// +// FDInputJoystick :: GetAxisResponseCurvePoint +// +//=========================================================================== + +float FDInputJoystick::GetAxisResponseCurvePoint(int axis, int point) +{ + return 0; +} + //=========================================================================== // // FDInputJoystick :: SetAxisDeadZone @@ -965,6 +1006,36 @@ void FDInputJoystick::SetAxisScale(int axis, float scale) } } +//=========================================================================== +// +// FDInputJoystick :: SetAxisDigitalThreshold +// +//=========================================================================== + +void FDInputJoystick::SetAxisDigitalThreshold(int axis, float threshold) +{ +} + +//=========================================================================== +// +// FDInputJoystick :: SetAxisResponseCurve +// +//=========================================================================== + +void FDInputJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset) +{ +} + +//=========================================================================== +// +// FDInputJoystick :: SetAxisResponseCurvePoint +// +//=========================================================================== + +void FDInputJoystick::SetAxisResponseCurvePoint(int axis, int point, float value) +{ +} + //=========================================================================== // // FDInputJoystick :: IsAxisDeadZoneDefault @@ -988,10 +1059,28 @@ bool FDInputJoystick::IsAxisDeadZoneDefault(int axis) bool FDInputJoystick::IsAxisScaleDefault(int axis) { - if (unsigned(axis) < Axes.Size()) - { - return Axes[axis].Multiplier == Axes[axis].DefaultMultiplier; - } + return true; +} + +//=========================================================================== +// +// FDInputJoystick :: IsAxisDigitalThresholdDefault +// +//=========================================================================== + +bool FDInputJoystick::IsAxisDigitalThresholdDefault(int axis) +{ + return true; +} + +//=========================================================================== +// +// FDInputJoystick :: IsAxisResponseCurveDefault +// +//=========================================================================== + +bool FDInputJoystick::IsAxisResponseCurveDefault(int axis) +{ return true; } diff --git a/src/common/platform/win32/i_rawps2.cpp b/src/common/platform/win32/i_rawps2.cpp index 2f89739bd..69eea017a 100644 --- a/src/common/platform/win32/i_rawps2.cpp +++ b/src/common/platform/win32/i_rawps2.cpp @@ -104,10 +104,18 @@ public: EJoyAxis GetAxisMap(int axis); const char *GetAxisName(int axis); float GetAxisScale(int axis); + float GetAxisDigitalThreshold(int axis); + EJoyCurve GetAxisResponseCurve(int axis); + float GetAxisResponseCurvePoint(int axis, int point); void SetAxisDeadZone(int axis, float deadzone); void SetAxisMap(int axis, EJoyAxis gameaxis); void SetAxisScale(int axis, float scale); + void SetAxisDigitalThreshold(int axis, float threshold); + void SetAxisResponseCurve(int axis, EJoyCurve preset); + void SetAxisResponseCurvePoint(int axis, int point, float value); + bool IsAxisDigitalThresholdDefault(int axis); + bool IsAxisResponseCurveDefault(int axis); bool IsSensitivityDefault(); bool IsAxisDeadZoneDefault(int axis); @@ -786,6 +794,39 @@ float FRawPS2Controller::GetAxisScale(int axis) return 0; } +//========================================================================== +// +// FRawPS2Controller :: GetAxisDigitalThreshold +// +//========================================================================== + +float FRawPS2Controller::GetAxisDigitalThreshold(int axis) +{ + return 0; +} + +//========================================================================== +// +// FRawPS2Controller :: GetAxisResponseCurve +// +//========================================================================== + +EJoyCurve FRawPS2Controller::GetAxisResponseCurve(int axis) +{ + return JOYCURVE_DEFAULT; +} + +//========================================================================== +// +// FRawPS2Controller :: GetAxisResponseCurvePoint +// +//========================================================================== + +float FRawPS2Controller::GetAxisResponseCurvePoint(int axis, int point) +{ + return 0; +} + //========================================================================== // // FRawPS2Controller :: SetAxisDeadZone @@ -828,6 +869,36 @@ void FRawPS2Controller::SetAxisScale(int axis, float scale) } } +//========================================================================== +// +// FRawPS2Controller :: SetAxisDigitalThreshold +// +//========================================================================== + +void FRawPS2Controller::SetAxisDigitalThreshold(int axis, float threshold) +{ +} + +//========================================================================== +// +// FRawPS2Controller :: SetAxisResponseCurve +// +//========================================================================== + +void FRawPS2Controller::SetAxisResponseCurve(int axis, EJoyCurve preset) +{ +} + +//========================================================================== +// +// FRawPS2Controller :: SetAxisResponseCurvePoint +// +//========================================================================== + +void FRawPS2Controller::SetAxisResponseCurvePoint(int axis, int point, float value) +{ +} + //=========================================================================== // // FRawPS2Controller :: IsAxisDeadZoneDefault @@ -836,10 +907,6 @@ void FRawPS2Controller::SetAxisScale(int axis, float scale) bool FRawPS2Controller::IsAxisDeadZoneDefault(int axis) { - if (unsigned(axis) < NUM_AXES) - { - return Axes[axis].DeadZone == DEFAULT_DEADZONE; - } return true; } @@ -851,10 +918,28 @@ bool FRawPS2Controller::IsAxisDeadZoneDefault(int axis) bool FRawPS2Controller::IsAxisScaleDefault(int axis) { - if (unsigned(axis) < NUM_AXES) - { - return Axes[axis].Multiplier == DefaultAxes[axis].Multiplier; - } + return true; +} + +//=========================================================================== +// +// FRawPS2Controller :: IsAxisDigitalThresholdDefault +// +//=========================================================================== + +bool FRawPS2Controller::IsAxisDigitalThresholdDefault(int axis) +{ + return true; +} + +//=========================================================================== +// +// FRawPS2Controller :: IsAxisResponseCurveDefault +// +//=========================================================================== + +bool FRawPS2Controller::IsAxisResponseCurveDefault(int axis) +{ return true; } diff --git a/src/common/platform/win32/i_xinput.cpp b/src/common/platform/win32/i_xinput.cpp index 0f3c19237..f9cdbe2e5 100644 --- a/src/common/platform/win32/i_xinput.cpp +++ b/src/common/platform/win32/i_xinput.cpp @@ -94,15 +94,23 @@ public: EJoyAxis GetAxisMap(int axis); const char *GetAxisName(int axis); float GetAxisScale(int axis); + float GetAxisDigitalThreshold(int axis); + EJoyCurve GetAxisResponseCurve(int axis); + float GetAxisResponseCurvePoint(int axis, int point); void SetAxisDeadZone(int axis, float deadzone); void SetAxisMap(int axis, EJoyAxis gameaxis); void SetAxisScale(int axis, float scale); + void SetAxisDigitalThreshold(int axis, float threshold); + void SetAxisResponseCurve(int axis, EJoyCurve preset); + void SetAxisResponseCurvePoint(int axis, int point, float value); bool IsSensitivityDefault(); bool IsAxisDeadZoneDefault(int axis); bool IsAxisMapDefault(int axis); bool IsAxisScaleDefault(int axis); + bool IsAxisDigitalThresholdDefault(int axis); + bool IsAxisResponseCurveDefault(int axis); bool GetEnabled(); void SetEnabled(bool enabled); @@ -568,6 +576,39 @@ float FXInputController::GetAxisScale(int axis) return 0; } +//========================================================================== +// +// FXInputController :: GetAxisDigitalThreshold +// +//========================================================================== + +float FXInputController::GetAxisDigitalThreshold(int axis) +{ + return 0; +} + +//========================================================================== +// +// FXInputController :: GetAxisResponseCurve +// +//========================================================================== + +EJoyCurve FXInputController::GetAxisResponseCurve(int axis) +{ + return JOYCURVE_DEFAULT; +} + +//========================================================================== +// +// FXInputController :: GetAxisResponseCurvePoint +// +//========================================================================== + +float FXInputController::GetAxisResponseCurvePoint(int axis, int point) +{ + return 0; +} + //========================================================================== // // FXInputController :: SetAxisDeadZone @@ -610,6 +651,36 @@ void FXInputController::SetAxisScale(int axis, float scale) } } +//========================================================================== +// +// FXInputController :: SetAxisDigitalThreshold +// +//========================================================================== + +void FXInputController::SetAxisDigitalThreshold(int axis, float threshold) +{ +} + +//========================================================================== +// +// FXInputController :: SetAxisResponseCurve +// +//========================================================================== + +void FXInputController::SetAxisResponseCurve(int axis, EJoyCurve preset) +{ +} + +//========================================================================== +// +// FXInputController :: SetAxisResponseCurvePoint +// +//========================================================================== + +void FXInputController::SetAxisResponseCurvePoint(int axis, int point, float value) +{ +} + //=========================================================================== // // FXInputController :: IsAxisDeadZoneDefault @@ -640,6 +711,28 @@ bool FXInputController::IsAxisScaleDefault(int axis) return true; } +//=========================================================================== +// +// FXInputController :: IsAxisDigitalThresholdDefault +// +//=========================================================================== + +bool FXInputController::IsAxisDigitalThresholdDefault(int axis) +{ + return true; +} + +//=========================================================================== +// +// FXInputController :: IsAxisResponseCurveDefault +// +//=========================================================================== + +bool FXInputController::IsAxisResponseCurveDefault(int axis) +{ + return true; +} + //=========================================================================== // // FXInputController :: GetEnabled From 33b22d99dad7878a61a655fade54d6d000767b54 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 21:13:40 -0400 Subject: [PATCH 262/384] Implemented saving, ccmd, and options menu --- src/common/engine/m_joy.cpp | 238 ++++++++++++++++++ src/common/menu/joystickmenu.cpp | 50 ++++ wadsrc/static/menudef.txt | 9 + .../zscript/engine/ui/menu/joystickmenu.zs | 145 ++++++++++- wadsrc/static/zscript/engine/ui/menu/menu.zs | 19 ++ 5 files changed, 460 insertions(+), 1 deletion(-) diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp index 0fce7d994..9f1c3cae5 100644 --- a/src/common/engine/m_joy.cpp +++ b/src/common/engine/m_joy.cpp @@ -33,6 +33,7 @@ // HEADER FILES ------------------------------------------------------------ #include +#include "c_dispatch.h" #include "vectors.h" #include "m_joy.h" #include "configfile.h" @@ -163,6 +164,48 @@ bool M_LoadJoystickConfig(IJoystickConfig *joy) joy->SetAxisScale(i, (float)atof(value)); } + mysnprintf(key + axislen, countof(key) - axislen, "threshold"); + value = GameConfig->GetValueForKey(key); + if (value) + { + joy->SetAxisDigitalThreshold(i, (float)atof(value)); + } + + mysnprintf(key + axislen, countof(key) - axislen, "curve"); + value = GameConfig->GetValueForKey(key); + if (value) + { + joy->SetAxisResponseCurve(i, (EJoyCurve)clamp(atoi(value), (int)JOYCURVE_CUSTOM, (int)NUM_JOYCURVE-1)); + } + + mysnprintf(key + axislen, countof(key) - axislen, "curve-x1"); + value = GameConfig->GetValueForKey(key); + if (value) + { + joy->SetAxisResponseCurvePoint(i, 0, (float)atof(value)); + } + + mysnprintf(key + axislen, countof(key) - axislen, "curve-y1"); + value = GameConfig->GetValueForKey(key); + if (value) + { + joy->SetAxisResponseCurvePoint(i, 1, (float)atof(value)); + } + + mysnprintf(key + axislen, countof(key) - axislen, "curve-x2"); + value = GameConfig->GetValueForKey(key); + if (value) + { + joy->SetAxisResponseCurvePoint(i, 2, (float)atof(value)); + } + + mysnprintf(key + axislen, countof(key) - axislen, "curve-y2"); + value = GameConfig->GetValueForKey(key); + if (value) + { + joy->SetAxisResponseCurvePoint(i, 3, (float)atof(value)); + } + mysnprintf(key + axislen, countof(key) - axislen, "map"); value = GameConfig->GetValueForKey(key); if (value) @@ -227,6 +270,33 @@ void M_SaveJoystickConfig(IJoystickConfig *joy) mysnprintf(value, countof(value), "%g", joy->GetAxisScale(i)); GameConfig->SetValueForKey(key, value); } + if (!joy->IsAxisDigitalThresholdDefault(i)) + { + mysnprintf(key + axislen, countof(key) - axislen, "threshold"); + mysnprintf(value, countof(value), "%g", joy->GetAxisDigitalThreshold(i)); + GameConfig->SetValueForKey(key, value); + } + if (!joy->IsAxisResponseCurveDefault(i)) + { + mysnprintf(key + axislen, countof(key) - axislen, "curve"); + mysnprintf(value, countof(value), "%d", joy->GetAxisResponseCurve(i)); + GameConfig->SetValueForKey(key, value); + } + if (joy->GetAxisResponseCurve(i) == JOYCURVE_CUSTOM) + { + mysnprintf(key + axislen, countof(key) - axislen, "curve-x1"); + mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 0)); + GameConfig->SetValueForKey(key, value); + mysnprintf(key + axislen, countof(key) - axislen, "curve-y1"); + mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 1)); + GameConfig->SetValueForKey(key, value); + mysnprintf(key + axislen, countof(key) - axislen, "curve-x2"); + mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 2)); + GameConfig->SetValueForKey(key, value); + mysnprintf(key + axislen, countof(key) - axislen, "curve-y2"); + mysnprintf(value, countof(value), "%g", joy->GetAxisResponseCurvePoint(i, 3)); + GameConfig->SetValueForKey(key, value); + } if (!joy->IsAxisMapDefault(i)) { mysnprintf(key + axislen, countof(key) - axislen, "map"); @@ -243,6 +313,174 @@ void M_SaveJoystickConfig(IJoystickConfig *joy) } } +CCMD (gamepad) +{ + int COMMAND = 1, IDENTIFIER = 2, VALUE = 3; + int argc = argv.argc()-1; + + TArray sticks; + I_GetJoysticks(sticks); + + auto usage = []() + { + Printf( + "usage:" + "\n\tgamepad list" + "\n\tgamepad reset pad" + "\n\tgamepad enabled pad [0|1]" + "\n\tgamepad background pad [0|1]" + "\n\tgamepad sensitivity pad [float]" + "\n\tgamepad deadzone pad.axis [float]" + "\n\tgamepad scale pad.axis [float]" + "\n\tgamepad threshold pad.axis [float]" + "\n\tgamepad curve pad.axis [-1|0|1|2|3]" + "\n\tgamepad curve-x1 pad.axis [float]" + "\n\tgamepad curve-y1 pad.axis [float]" + "\n\tgamepad curve-x2 pad.axis [float]" + "\n\tgamepad curve-y2 pad.axis [float]" + "\n\tgamepad map pad.axis [-1|0|1|2|3|4]" + "\n" + ); + }; + + if (argc < COMMAND) + { + return usage(); + }; + + FName command = argv[COMMAND]; + + if (argc < IDENTIFIER) + { + if (command == "list") + { + for (int i = 0; i < sticks.SSize(); i++) + { + Printf("%d: '%s'\n", i, sticks[i]->GetName().GetChars()); + for (int j = 0; j < sticks[i]->GetNumAxes(); j++) + { + Printf(" %d.%d: '%s'\n", i, j, sticks[i]->GetAxisName(j)); + } + } + return; + } + return usage(); + } + + const char * id = argv[IDENTIFIER]; + const char * hasAxis = strchr(id, '.'); + + int pad, axis; + + try { + pad = (int)std::stod(id); + + if (pad < 0 || pad >= sticks.SSize()) + { + return (void) Printf("Pad # out of range\n"); + } + } catch (...) { + return (void) Printf("Failed to parse pad #\n"); + } + + if (hasAxis) + { + try { + axis = (int)std::stod(hasAxis+1); + + if (axis < 0 || axis >= sticks[pad]->GetNumAxes()) + { + return (void) Printf("Axis # out of range\n"); + } + } catch (...) { + return (void) Printf("Failed to parse axis #\n"); + } + } + + float value = 0; + bool set = argc >= VALUE; + + if (set) + { + try { + value = std::stod(argv[VALUE]); + } catch (...) { + return (void) Printf("Failed to parse args\n"); + } + } + + if (command == "reset") + { + if (set) return usage(); + sticks[pad]->SetDefaultConfig(); + sticks[pad]->SetEnabled(true); + sticks[pad]->SetEnabledInBackground(sticks[pad]->AllowsEnabledInBackground()); + sticks[pad]->SetSensitivity(1); + return; + } + if (command == "enabled") + { + if (set) sticks[pad]->SetEnabled((int)value); + return (void) Printf("%d\n", sticks[pad]->GetEnabled()); + } + if (command == "background") + { + if (set) sticks[pad]->SetEnabledInBackground((int)value); + return (void) Printf("%d\n", sticks[pad]->GetEnabledInBackground()); + } + if (command == "sensitivity") + { + if (set) sticks[pad]->SetSensitivity(value); + return (void) Printf("%g\n", sticks[pad]->GetSensitivity()); + } + if (command == "deadzone") + { + if (set) sticks[pad]->SetAxisDeadZone(axis, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisDeadZone(axis)); + } + if (command == "scale") + { + if (set) sticks[pad]->SetAxisScale(axis, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisScale(axis)); + } + if (command == "threshold") + { + if (set) sticks[pad]->SetAxisDigitalThreshold(axis, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisDigitalThreshold(axis)); + } + if (command == "curve") + { + if (set) sticks[pad]->SetAxisResponseCurve(axis, (EJoyCurve)value); + return (void) Printf("%d\n", sticks[pad]->GetAxisResponseCurve(axis)); + } + if (command == "curve-x1") + { + if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 0, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 0)); + } + if (command == "curve-y1") + { + if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 1, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 1)); + } + if (command == "curve-x2") + { + if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 2, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 2)); + } + if (command == "curve-y2") + { + if (set) sticks[pad]->SetAxisResponseCurvePoint(axis, 3, value); + return (void) Printf("%g\n", sticks[pad]->GetAxisResponseCurvePoint(axis, 3)); + } + if (command == "map") + { + if (set) sticks[pad]->SetAxisMap(axis, (EJoyAxis)value); + return (void) Printf("%d\n", sticks[pad]->GetAxisMap(axis)); + } + + return usage(); +} //=========================================================================== // diff --git a/src/common/menu/joystickmenu.cpp b/src/common/menu/joystickmenu.cpp index 3d3e881b1..e3bb8544b 100644 --- a/src/common/menu/joystickmenu.cpp +++ b/src/common/menu/joystickmenu.cpp @@ -84,6 +84,56 @@ DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisDeadZone) return 0; } +DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisDigitalThreshold) +{ + PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); + PARAM_INT(axis); + ACTION_RETURN_FLOAT(self->GetAxisDigitalThreshold(axis)); +} + +DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisDigitalThreshold) +{ + PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); + PARAM_INT(axis); + PARAM_FLOAT(dt); + self->SetAxisDigitalThreshold(axis, (float)dt); + return 0; +} + +DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisResponseCurve) +{ + PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); + PARAM_INT(axis); + ACTION_RETURN_INT(self->GetAxisResponseCurve(axis)); +} + +DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisResponseCurve) +{ + PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); + PARAM_INT(axis); + PARAM_INT(curve); + self->SetAxisResponseCurve(axis, (EJoyCurve)curve); + return 0; +} + +DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisResponseCurvePoint) +{ + PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); + PARAM_INT(axis); + PARAM_INT(point); + ACTION_RETURN_FLOAT(self->GetAxisResponseCurvePoint(axis, point)); +} + +DEFINE_ACTION_FUNCTION(IJoystickConfig, SetAxisResponseCurvePoint) +{ + PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); + PARAM_INT(axis); + PARAM_INT(point); + PARAM_FLOAT(value); + self->SetAxisResponseCurvePoint(axis, point, value); + return 0; +} + DEFINE_ACTION_FUNCTION(IJoystickConfig, GetAxisMap) { PARAM_SELF_STRUCT_PROLOGUE(IJoystickConfig); diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 046a7f1f6..2577b9aa7 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -837,6 +837,15 @@ OptionMenu "JoystickOptions" protected Title "$JOYMNU_OPTIONS" } +OptionValue "JoyAxisCurveNames" +{ + -1, "$OPTVAL_CUSTOM" + 0, "$OPTVAL_DEFAULT" + 1, "$OPTVAL_LINEAR" + 2, "$OPTVAL_QUADRATIC" + 3, "$OPTVAL_CUBIC" +} + OptionValue "JoyAxisMapNames" { -1, "$OPTVAL_NONE" diff --git a/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs b/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs index 02669c1a3..425b7dd67 100644 --- a/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/joystickmenu.zs @@ -100,6 +100,50 @@ class OptionMenuSliderJoyScale : OptionMenuSliderBase // //============================================================================= +class OptionMenuSliderJoyCurve : OptionMenuSliderBase +{ + int mAxis; + int mNeg; + int mPoint; + bool mShown; + JoystickConfig mJoy; + + OptionMenuSliderJoyCurve Init(String label, int axis, double min, double max, double step, int showval, JoystickConfig joy, int point) + { + Super.Init(label, min, max, step, showval); + mAxis = axis; + mNeg = 1; + mJoy = joy; + mPoint = point; + mShown = false; + return self; + } + + override bool Visible() + { + mShown |= mJoy.GetAxisResponseCurve(mAxis) == JoystickConfig.JOYCURVE_CUSTOM; + return mShown; + } + + override double GetSliderValue() + { + double d = mJoy.GetAxisResponseCurvePoint(mAxis, mPoint); + mNeg = d < 0? -1:1; + return d; + } + + override void SetSliderValue(double val) + { + mJoy.SetAxisResponseCurvePoint(mAxis, mPoint, val * mNeg); + } +} + +//============================================================================= +// +// +// +//============================================================================= + class OptionMenuSliderJoyDeadZone : OptionMenuSliderBase { int mAxis; @@ -130,7 +174,94 @@ class OptionMenuSliderJoyDeadZone : OptionMenuSliderBase //============================================================================= // -// +// +// +//============================================================================= + +class OptionMenuSliderJoyDigitalThreshold : OptionMenuSliderBase +{ + int mAxis; + int mNeg; + JoystickConfig mJoy; + + OptionMenuSliderJoyDigitalThreshold Init(String label, int axis, double min, double max, double step, int showval, JoystickConfig joy) + { + Super.Init(label, min, max, step, showval); + mAxis = axis; + mNeg = 1; + mJoy = joy; + return self; + } + + override double GetSliderValue() + { + double d = mJoy.GetAxisDigitalThreshold(mAxis); + mNeg = d < 0? -1:1; + return d; + } + + override void SetSliderValue(double val) + { + mJoy.SetAxisDigitalThreshold(mAxis, val * mNeg); + } +} + +//============================================================================= +// +// +// +//============================================================================= + +class OptionMenuItemJoyCurve : OptionMenuItemOptionBase +{ + int mAxis; + JoystickConfig mJoy; + + OptionMenuItemJoyCurve Init(String label, int axis, Name values, int center, JoystickConfig joy) + { + Super.Init(label, 'none', values, null, center); + mAxis = axis; + mJoy = joy; + return self; + } + + override int GetSelection() + { + double f = mJoy.GetAxisResponseCurve(mAxis); + let opt = OptionValues.GetCount(mValues); + if (opt > 0) + { + // Map from joystick curve to menu selection. + for(int i = 0; i < opt; i++) + { + if (f ~== OptionValues.GetValue(mValues, i)) + { + return i; + } + } + } + return JoystickConfig.JOYCURVE_CUSTOM; + } + + override void SetSelection(int selection) + { + let opt = OptionValues.GetCount(mValues); + // Map from menu selection to joystick curve. + if (opt == 0 || selection >= opt) + { + selection = JoystickConfig.JOYCURVE_DEFAULT; + } + else + { + selection = int(OptionValues.GetValue(mValues, selection)); + } + mJoy.setAxisResponseCurve(mAxis, selection); + } +} + +//============================================================================= +// +// // //============================================================================= @@ -340,8 +471,20 @@ class OptionMenuItemJoyConfigMenu : OptionMenuItemSubmenu opt.mItems.Push(it); it = new("OptionMenuItemInverter").Init("$JOYMNU_INVERT", i, false, joy); opt.mItems.Push(it); + it = new("OptionMenuItemJoyCurve").Init("$JOYMNU_CURVE", i, "JoyAxisCurveNames", false, joy); + opt.mItems.Push(it); + it = new("OptionMenuSliderJoyCurve").Init("$JOYMNU_CURVE_X1", i, 0, 0.9, 0.05, 3, joy, 0); + opt.mItems.Push(it); + it = new("OptionMenuSliderJoyCurve").Init("$JOYMNU_CURVE_Y1", i, 0, 0.9, 0.05, 3, joy, 1); + opt.mItems.Push(it); + it = new("OptionMenuSliderJoyCurve").Init("$JOYMNU_CURVE_X2", i, 0, 0.9, 0.05, 3, joy, 2); + opt.mItems.Push(it); + it = new("OptionMenuSliderJoyCurve").Init("$JOYMNU_CURVE_Y2", i, 0, 0.9, 0.05, 3, joy, 3); + opt.mItems.Push(it); it = new("OptionMenuSliderJoyDeadZone").Init("$JOYMNU_DEADZONE", i, 0, 0.9, 0.05, 3, joy); opt.mItems.Push(it); + it = new("OptionMenuSliderJoyDigitalThreshold").Init("$JOYMNU_THRESHOLD", i, 0, 0.9, 0.05, 3, joy); + opt.mItems.Push(it); } } else diff --git a/wadsrc/static/zscript/engine/ui/menu/menu.zs b/wadsrc/static/zscript/engine/ui/menu/menu.zs index 7a0ef2036..ed98a97e9 100644 --- a/wadsrc/static/zscript/engine/ui/menu/menu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/menu.zs @@ -68,6 +68,16 @@ struct JoystickConfig native version("2.4") NUM_JOYAXIS, }; + enum EJoyCurve { + JOYCURVE_CUSTOM = -1, + JOYCURVE_DEFAULT, + JOYCURVE_LINEAR, + JOYCURVE_QUADRATIC, + JOYCURVE_CUBIC, + + NUM_JOYCURVE + }; + native float GetSensitivity(); native void SetSensitivity(float scale); @@ -77,6 +87,15 @@ struct JoystickConfig native version("2.4") native float GetAxisDeadZone(int axis); native void SetAxisDeadZone(int axis, float zone); + native float GetAxisDigitalThreshold(int axis); + native void SetAxisDigitalThreshold(int axis, float thresh); + + native int GetAxisResponseCurve(int axis); + native void SetAxisResponseCurve(int axis, int preset); + + native float GetAxisResponseCurvePoint(int axis, int point); + native void SetAxisResponseCurvePoint(int axis, int point, float value); + native int GetAxisMap(int axis); native void SetAxisMap(int axis, int gameaxis); From 8c12037ef422fb49e31775963e844def7f5332ca Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 21:38:47 -0400 Subject: [PATCH 263/384] Unified gamepad settings --- src/common/engine/m_joy.cpp | 17 +++++++++++ src/common/engine/m_joy.h | 12 ++++++++ .../platform/posix/cocoa/i_joystick.cpp | 18 ++++------- src/common/platform/posix/sdl/i_joystick.cpp | 30 +++++++++---------- src/common/platform/win32/i_dijoy.cpp | 13 ++++---- src/common/platform/win32/i_rawps2.cpp | 19 ++++++------ src/common/platform/win32/i_xinput.cpp | 18 +++++------ 7 files changed, 72 insertions(+), 55 deletions(-) diff --git a/src/common/engine/m_joy.cpp b/src/common/engine/m_joy.cpp index 9f1c3cae5..4ea016879 100644 --- a/src/common/engine/m_joy.cpp +++ b/src/common/engine/m_joy.cpp @@ -58,6 +58,23 @@ EXTERN_CVAR(Bool, joy_ps2raw) EXTERN_CVAR(Bool, joy_dinput) EXTERN_CVAR(Bool, joy_xinput) +extern const float JOYDEADZONE_DEFAULT = 0.1; // reduced from 0.25 + +extern const float JOYSENSITIVITY_DEFAULT = 1.0; + +extern const float JOYTHRESH_DEFAULT = 0.05; +extern const float JOYTHRESH_TRIGGER = 0.05; +extern const float JOYTHRESH_STICK_X = 0.65; +extern const float JOYTHRESH_STICK_Y = 0.35; + +extern const CubicBezier JOYCURVE[NUM_JOYCURVE] = { + {{0.3, 0.0, 0.7, 0.4}}, // DEFAULT -> QUADRATIC + + {{0.0, 0.0, 1.0, 1.0}}, // LINEAR + {{0.3, 0.0, 0.7, 0.4}}, // QUADRATIC + {{0.5, 0.0, 0.7, 0.2}}, // CUBIC +}; + // PUBLIC DATA DEFINITIONS ------------------------------------------------- CUSTOM_CVARD(Bool, use_joystick, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINITCALL, "enables input from the joystick if it is present") diff --git a/src/common/engine/m_joy.h b/src/common/engine/m_joy.h index 60321785a..87dca60a3 100644 --- a/src/common/engine/m_joy.h +++ b/src/common/engine/m_joy.h @@ -38,6 +38,18 @@ enum EJoyAxis NUM_JOYAXIS, }; +extern const float JOYDEADZONE_DEFAULT; + +extern const float JOYSENSITIVITY_DEFAULT; + +extern const float JOYTHRESH_DEFAULT; + +extern const float JOYTHRESH_TRIGGER; +extern const float JOYTHRESH_STICK_X; +extern const float JOYTHRESH_STICK_Y; + +extern const CubicBezier JOYCURVE[NUM_JOYCURVE]; + // Generic configuration interface for a controller. struct IJoystickConfig { diff --git a/src/common/platform/posix/cocoa/i_joystick.cpp b/src/common/platform/posix/cocoa/i_joystick.cpp index e338ad8dd..6eeb2eb56 100644 --- a/src/common/platform/posix/cocoa/i_joystick.cpp +++ b/src/common/platform/posix/cocoa/i_joystick.cpp @@ -185,10 +185,6 @@ private: io_object_t m_notification; - - static const float DEFAULT_DEADZONE; - static const float DEFAULT_SENSITIVITY; - void ProcessAxes(); bool ProcessAxis (const IOHIDEventStruct& event); bool ProcessButton(const IOHIDEventStruct& event); @@ -208,10 +204,6 @@ private: }; -const float IOKitJoystick::DEFAULT_DEADZONE = 0.25f; -const float IOKitJoystick::DEFAULT_SENSITIVITY = 1.0f; - - IOHIDDeviceInterface** CreateDeviceInterface(const io_object_t device) { IOCFPlugInInterface** plugInInterface = NULL; @@ -294,7 +286,7 @@ IOHIDQueueInterface** CreateDeviceQueue(IOHIDDeviceInterface** const interface) IOKitJoystick::IOKitJoystick(const io_object_t device) : m_interface(CreateDeviceInterface(device)) , m_queue(CreateDeviceQueue(m_interface)) -, m_sensitivity(DEFAULT_SENSITIVITY) +, m_sensitivity(JOYSENSITIVITY_DEFAULT) , m_enabled(true) , m_useAxesPolling(true) , m_notification(0) @@ -449,7 +441,7 @@ void IOKitJoystick::SetAxisResponseCurvePoint(int axis, int point, float value) bool IOKitJoystick::IsSensitivityDefault() { - return DEFAULT_SENSITIVITY == m_sensitivity; + return JOYSENSITIVITY_DEFAULT == m_sensitivity; } bool IOKitJoystick::IsAxisDeadZoneDefault(int axis) @@ -498,14 +490,14 @@ void IOKitJoystick::SetEnabled(bool enabled) void IOKitJoystick::SetDefaultConfig() { - m_sensitivity = DEFAULT_SENSITIVITY; + m_sensitivity = JOYSENSITIVITY_DEFAULT; const size_t axisCount = m_axes.Size(); for (size_t i = 0; i < axisCount; ++i) { - m_axes[i].deadZone = DEFAULT_DEADZONE; - m_axes[i].sensitivity = DEFAULT_SENSITIVITY; + m_axes[i].deadZone = JOYDEADZONE_DEFAULT; + m_axes[i].sensitivity = JOYSENSITIVITY_DEFAULT; m_axes[i].gameAxis = JOYAXIS_None; } diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index 6654bcae3..000562e30 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -40,15 +40,13 @@ #include "m_joy.h" #include "keydef.h" -#define DEFAULT_DEADZONE 0.25f; - -// Very small deadzone so that floating point magic doesn't happen -#define MIN_DEADZONE 0.000001f - class SDLInputJoystick: public IJoystickConfig { public: - SDLInputJoystick(int DeviceIndex) : DeviceIndex(DeviceIndex), Multiplier(1.0f) , Enabled(true) + SDLInputJoystick(int DeviceIndex) : + DeviceIndex(DeviceIndex), + Multiplier(JOYSENSITIVITY_DEFAULT) , + Enabled(true) { if (SDL_IsGameController(DeviceIndex)) { @@ -143,7 +141,7 @@ public: void SetAxisDeadZone(int axis, float zone) { - Axes[axis].DeadZone = clamp(zone, MIN_DEADZONE, 1.f); + Axes[axis].DeadZone = clamp(zone, 0.f, 1.f); } void SetAxisMap(int axis, EJoyAxis gameaxis) { @@ -166,11 +164,11 @@ public: // Used by the saver to not save properties that are at their defaults. bool IsSensitivityDefault() { - return Multiplier == 1.0f; + return Multiplier == JOYSENSITIVITY_DEFAULT; } bool IsAxisDeadZoneDefault(int axis) { - return Axes[axis].DeadZone <= MIN_DEADZONE; + return Axes[axis].DeadZone <= JOYDEADZONE_DEFAULT; } bool IsAxisMapDefault(int axis) { @@ -180,7 +178,7 @@ public: } bool IsAxisScaleDefault(int axis) { - return Axes[axis].Multiplier == 1.0f; + return Axes[axis].Multiplier == JOYSENSITIVITY_DEFAULT; } bool IsAxisDigitalThresholdDefault(int axis) { @@ -214,8 +212,8 @@ public: info.Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y'); } - info.DeadZone = DEFAULT_DEADZONE; - info.Multiplier = 1.0f; + info.DeadZone = JOYDEADZONE_DEFAULT; + info.Multiplier = JOYSENSITIVITY_DEFAULT; info.Value = 0.0; info.ButtonValue = 0; @@ -398,8 +396,8 @@ void SDLInputJoystick::ProcessInput() { { // GameController API available - auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > MIN_DEADZONE; - auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > MIN_DEADZONE; + auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > JOYTHRESH_DEFAULT; + auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > JOYTHRESH_DEFAULT; for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i) { @@ -409,8 +407,8 @@ void SDLInputJoystick::ProcessInput() { Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate); } - auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > MIN_DEADZONE; - auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > MIN_DEADZONE; + auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > JOYTHRESH_DEFAULT; + auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > JOYTHRESH_DEFAULT; if (lastTriggerL != currTriggerL) PostKeyEvent(currTriggerL, KEY_PAD_LTRIGGER); if (lastTriggerR != currTriggerR) PostKeyEvent(currTriggerR, KEY_PAD_RTRIGGER); diff --git a/src/common/platform/win32/i_dijoy.cpp b/src/common/platform/win32/i_dijoy.cpp index d3ada1e22..f87456606 100644 --- a/src/common/platform/win32/i_dijoy.cpp +++ b/src/common/platform/win32/i_dijoy.cpp @@ -146,8 +146,6 @@ public: // MACROS ------------------------------------------------------------------ -#define DEFAULT_DEADZONE 0.25f - // TYPES ------------------------------------------------------------------- class FDInputJoystick : public FInputDevice, IJoystickConfig @@ -769,11 +767,11 @@ void FDInputJoystick::SetDefaultConfig() { unsigned i; - Multiplier = 1; + Multiplier = JOYSENSITIVITY_DEFAULT; for (i = 0; i < Axes.Size(); ++i) { - Axes[i].DeadZone = DEFAULT_DEADZONE; - Axes[i].Multiplier = 1; + Axes[i].DeadZone = JOYDEADZONE_DEFAULT; + Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT; Axes[i].GameAxis = JOYAXIS_None; } // Triggers on a 360 controller have a much smaller deadzone. @@ -796,7 +794,8 @@ void FDInputJoystick::SetDefaultConfig() // Four axes? First two are movement, last two are looking around. if (Axes.Size() >= 4) { - Axes[3].GameAxis = JOYAXIS_Pitch; Axes[3].Multiplier = 0.75f; + Axes[3].GameAxis = JOYAXIS_Pitch; + // Axes[3].Multiplier = 0.75f; // Five axes? Use the fifth one for moving up and down. if (Axes.Size() >= 5) { @@ -857,7 +856,7 @@ void FDInputJoystick::SetSensitivity(float scale) bool FDInputJoystick::IsSensitivityDefault() { - return Multiplier == 1; + return Multiplier == JOYSENSITIVITY_DEFAULT; } //=========================================================================== diff --git a/src/common/platform/win32/i_rawps2.cpp b/src/common/platform/win32/i_rawps2.cpp index 69eea017a..a76e02f45 100644 --- a/src/common/platform/win32/i_rawps2.cpp +++ b/src/common/platform/win32/i_rawps2.cpp @@ -48,7 +48,6 @@ // MACROS ------------------------------------------------------------------ -#define DEFAULT_DEADZONE 0.25f #define STATUS_SWITCH_TIME 3 #define VID_PLAY_COM 0x0b43 @@ -114,13 +113,13 @@ public: void SetAxisDigitalThreshold(int axis, float threshold); void SetAxisResponseCurve(int axis, EJoyCurve preset); void SetAxisResponseCurvePoint(int axis, int point, float value); - bool IsAxisDigitalThresholdDefault(int axis); - bool IsAxisResponseCurveDefault(int axis); bool IsSensitivityDefault(); bool IsAxisDeadZoneDefault(int axis); bool IsAxisMapDefault(int axis); bool IsAxisScaleDefault(int axis); + bool IsAxisDigitalThresholdDefault(int axis); + bool IsAxisResponseCurveDefault(int axis); bool GetEnabled(); void SetEnabled(bool enabled); @@ -366,10 +365,10 @@ static const char *AxisNames[] = FRawPS2Controller::DefaultAxisConfig FRawPS2Controller::DefaultAxes[NUM_AXES] = { // Game axis, multiplier - { JOYAXIS_Side, 1 }, // ThumbLX - { JOYAXIS_Forward, 1 }, // ThumbLY - { JOYAXIS_Yaw, 1 }, // ThumbRX - { JOYAXIS_Pitch, 0.75 }, // ThumbRY + { JOYAXIS_Side, JOYSENSITIVITY_DEFAULT }, // ThumbLX + { JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT }, // ThumbLY + { JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT }, // ThumbRX + { JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT }, // ThumbRY }; // CODE -------------------------------------------------------------------- @@ -652,10 +651,10 @@ void FRawPS2Controller::AddAxes(float axes[NUM_JOYAXIS]) void FRawPS2Controller::SetDefaultConfig() { - Multiplier = 1; + Multiplier = JOYSENSITIVITY_DEFAULT; for (int i = 0; i < NUM_AXES; ++i) { - Axes[i].DeadZone = DEFAULT_DEADZONE; + Axes[i].DeadZone = JOYDEADZONE_DEFAULT; Axes[i].GameAxis = DefaultAxes[i].GameAxis; Axes[i].Multiplier = DefaultAxes[i].Multiplier; } @@ -720,7 +719,7 @@ void FRawPS2Controller::SetSensitivity(float scale) bool FRawPS2Controller::IsSensitivityDefault() { - return Multiplier == 1; + return Multiplier == JOYSENSITIVITY_DEFAULT; } //========================================================================== diff --git a/src/common/platform/win32/i_xinput.cpp b/src/common/platform/win32/i_xinput.cpp index f9cdbe2e5..031aebc37 100644 --- a/src/common/platform/win32/i_xinput.cpp +++ b/src/common/platform/win32/i_xinput.cpp @@ -220,12 +220,12 @@ static const char *AxisNames[] = FXInputController::DefaultAxisConfig FXInputController::DefaultAxes[NUM_AXES] = { // Dead zone, game axis, multiplier - { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, 1 }, // ThumbLX - { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, 1 }, // ThumbLY - { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, 1 }, // ThumbRX - { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, 0.75 }, // ThumbRY - { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 }, // LeftTrigger - { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, 0 } // RightTrigger + { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, JOYSENSITIVITY_DEFAULT }, // ThumbLX + { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT }, // ThumbLY + { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT }, // ThumbRX + { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT }, // ThumbRY + { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, JOYSENSITIVITY_DEFAULT }, // LeftTrigger + { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, JOYSENSITIVITY_DEFAULT } // RightTrigger }; // CODE -------------------------------------------------------------------- @@ -439,7 +439,7 @@ void FXInputController::AddAxes(float axes[NUM_JOYAXIS]) void FXInputController::SetDefaultConfig() { - Multiplier = 1; + Multiplier = JOYSENSITIVITY_DEFAULT; for (int i = 0; i < NUM_AXES; ++i) { Axes[i].DeadZone = DefaultAxes[i].DeadZone; @@ -502,7 +502,7 @@ void FXInputController::SetSensitivity(float scale) bool FXInputController::IsSensitivityDefault() { - return Multiplier == 1; + return Multiplier == JOYSENSITIVITY_DEFAULT; } //========================================================================== @@ -573,7 +573,7 @@ float FXInputController::GetAxisScale(int axis) { return Axes[axis].Multiplier; } - return 0; + return JOYSENSITIVITY_DEFAULT; } //========================================================================== From bac24118c2de01f5ce1635c00853d1c35e55f7f4 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 21:55:05 -0400 Subject: [PATCH 264/384] Implemented xinput stubs --- src/common/platform/win32/i_xinput.cpp | 72 ++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/src/common/platform/win32/i_xinput.cpp b/src/common/platform/win32/i_xinput.cpp index 031aebc37..890888a4b 100644 --- a/src/common/platform/win32/i_xinput.cpp +++ b/src/common/platform/win32/i_xinput.cpp @@ -130,12 +130,17 @@ protected: float Multiplier; EJoyAxis GameAxis; uint8_t ButtonValue; + float DigitalThreshold; + EJoyCurve ResponseCurvePreset; + CubicBezier ResponseCurve; }; struct DefaultAxisConfig { float DeadZone; EJoyAxis GameAxis; float Multiplier; + float DigitalThreshold; + EJoyCurve ResponseCurvePreset; }; enum { @@ -219,13 +224,13 @@ static const char *AxisNames[] = FXInputController::DefaultAxisConfig FXInputController::DefaultAxes[NUM_AXES] = { - // Dead zone, game axis, multiplier - { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, JOYSENSITIVITY_DEFAULT }, // ThumbLX - { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT }, // ThumbLY - { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT }, // ThumbRX - { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT }, // ThumbRY - { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, JOYSENSITIVITY_DEFAULT }, // LeftTrigger - { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, JOYSENSITIVITY_DEFAULT } // RightTrigger + // Dead zone, game axis, multiplier, digitalthreshold, curveA, curveB + { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Side, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbLX + { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE / 32768.f, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbLY + { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbRX + { XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE / 32768.f, JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbRY + { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT }, // LeftTrigger + { XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 256.f, JOYAXIS_None, JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT } // RightTrigger }; // CODE -------------------------------------------------------------------- @@ -335,11 +340,16 @@ void FXInputController::ProcessThumbstick(int value1, AxisInfo *axis1, axisval2 = (value2 - SHRT_MIN) * 2.0 / 65536 - 1.0; axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL); axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL); + axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1); + axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2); axis1->Value = float(axisval1); axis2->Value = float(axisval2); // We store all four buttons in the first axis and ignore the second. - buttonstate = Joy_XYAxesToButtons(axisval1, axisval2); + buttonstate = Joy_XYAxesToButtons( + abs(axisval1) < axis1->DigitalThreshold ? 0: axisval1, + abs(axisval2) < axis2->DigitalThreshold ? 0: axisval2 + ); Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base); axis1->ButtonValue = buttonstate; } @@ -359,6 +369,11 @@ void FXInputController::ProcessTrigger(int value, AxisInfo *axis, int base) double axisval; axisval = Joy_RemoveDeadZone(value / 256.0, axis->DeadZone, &buttonstate); + axisval = Joy_ApplyResponseCurveBezier(axis->ResponseCurve, axisval); + + // TODO: probably just put this into Joy_RemoveDeadZone + if (abs(axisval) < axis->DigitalThreshold) buttonstate = 0; + Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 1, base); axis->ButtonValue = buttonstate; axis->Value = float(axisval); @@ -445,6 +460,9 @@ void FXInputController::SetDefaultConfig() Axes[i].DeadZone = DefaultAxes[i].DeadZone; Axes[i].GameAxis = DefaultAxes[i].GameAxis; Axes[i].Multiplier = DefaultAxes[i].Multiplier; + Axes[i].DigitalThreshold = DefaultAxes[i].DigitalThreshold; + Axes[i].ResponseCurve = JOYCURVE[DefaultAxes[i].ResponseCurvePreset]; + Axes[i].ResponseCurvePreset = DefaultAxes[i].ResponseCurvePreset; } } @@ -584,7 +602,11 @@ float FXInputController::GetAxisScale(int axis) float FXInputController::GetAxisDigitalThreshold(int axis) { - return 0; + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].DigitalThreshold; + } + return JOYTHRESH_DEFAULT; } //========================================================================== @@ -595,6 +617,10 @@ float FXInputController::GetAxisDigitalThreshold(int axis) EJoyCurve FXInputController::GetAxisResponseCurve(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].ResponseCurvePreset; + } return JOYCURVE_DEFAULT; } @@ -606,6 +632,10 @@ EJoyCurve FXInputController::GetAxisResponseCurve(int axis) float FXInputController::GetAxisResponseCurvePoint(int axis, int point) { + if (unsigned(axis) < NUM_AXES && unsigned(point) < 4) + { + return Axes[axis].ResponseCurve.pts[point]; + } return 0; } @@ -659,6 +689,10 @@ void FXInputController::SetAxisScale(int axis, float scale) void FXInputController::SetAxisDigitalThreshold(int axis, float threshold) { + if (unsigned(axis) < NUM_AXES) + { + Axes[axis].DigitalThreshold = threshold; + } } //========================================================================== @@ -669,6 +703,13 @@ void FXInputController::SetAxisDigitalThreshold(int axis, float threshold) void FXInputController::SetAxisResponseCurve(int axis, EJoyCurve preset) { + if (unsigned(axis) < NUM_AXES) + { + if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurvePreset = preset; + if (preset == JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurve = JOYCURVE[preset]; + } } //========================================================================== @@ -679,6 +720,11 @@ void FXInputController::SetAxisResponseCurve(int axis, EJoyCurve preset) void FXInputController::SetAxisResponseCurvePoint(int axis, int point, float value) { + if (unsigned(axis) < NUM_AXES && unsigned(point) < 4) + { + Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM; + Axes[axis].ResponseCurve.pts[point] = value; + } } //=========================================================================== @@ -719,6 +765,10 @@ bool FXInputController::IsAxisScaleDefault(int axis) bool FXInputController::IsAxisDigitalThresholdDefault(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold; + } return true; } @@ -730,6 +780,10 @@ bool FXInputController::IsAxisDigitalThresholdDefault(int axis) bool FXInputController::IsAxisResponseCurveDefault(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset; + } return true; } From a41ced099c9a5258641ff0891a19ecf182a1c447 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 21:55:51 -0400 Subject: [PATCH 265/384] Implemented rawps2 stubs --- src/common/platform/win32/i_rawps2.cpp | 68 +++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/src/common/platform/win32/i_rawps2.cpp b/src/common/platform/win32/i_rawps2.cpp index a76e02f45..09bb314b0 100644 --- a/src/common/platform/win32/i_rawps2.cpp +++ b/src/common/platform/win32/i_rawps2.cpp @@ -137,6 +137,9 @@ protected: float Value; float DeadZone; float Multiplier; + float DigitalThreshold; + EJoyCurve ResponseCurvePreset; + CubicBezier ResponseCurve; EJoyAxis GameAxis; uint8_t ButtonValue; }; @@ -144,6 +147,8 @@ protected: { EJoyAxis GameAxis; float Multiplier; + float DigitalThreshold; + EJoyCurve ResponseCurvePreset; }; enum { @@ -364,11 +369,11 @@ static const char *AxisNames[] = FRawPS2Controller::DefaultAxisConfig FRawPS2Controller::DefaultAxes[NUM_AXES] = { - // Game axis, multiplier - { JOYAXIS_Side, JOYSENSITIVITY_DEFAULT }, // ThumbLX - { JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT }, // ThumbLY - { JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT }, // ThumbRX - { JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT }, // ThumbRY + // Game axis, multiplier, digital threshold, response curve A, response curve B + { JOYAXIS_Side, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbLX + { JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbLY + { JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT }, // ThumbRX + { JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT }, // ThumbRY }; // CODE -------------------------------------------------------------------- @@ -559,11 +564,16 @@ void FRawPS2Controller::ProcessThumbstick(int value1, AxisInfo *axis1, int value axisval2 = value2 * (2.0 / 255) - 1.0; axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL); axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL); + axisval1 = Joy_ApplyResponseCurveBezier(axis1->ResponseCurve, axisval1); + axisval2 = Joy_ApplyResponseCurveBezier(axis2->ResponseCurve, axisval2); axis1->Value = float(axisval1); axis2->Value = float(axisval2); // We store all four buttons in the first axis and ignore the second. - buttonstate = Joy_XYAxesToButtons(axisval1, axisval2); + buttonstate = Joy_XYAxesToButtons( + (abs(axisval1) < axis1->DigitalThreshold) ? 0 : axisval1, + (abs(axisval2) < axis2->DigitalThreshold) ? 0 : axisval2 + ); Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base); axis1->ButtonValue = buttonstate; } @@ -801,7 +811,11 @@ float FRawPS2Controller::GetAxisScale(int axis) float FRawPS2Controller::GetAxisDigitalThreshold(int axis) { - return 0; + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].DigitalThreshold; + } + return JOYTHRESH_DEFAULT; } //========================================================================== @@ -812,6 +826,10 @@ float FRawPS2Controller::GetAxisDigitalThreshold(int axis) EJoyCurve FRawPS2Controller::GetAxisResponseCurve(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].ResponseCurvePreset; + } return JOYCURVE_DEFAULT; } @@ -823,6 +841,10 @@ EJoyCurve FRawPS2Controller::GetAxisResponseCurve(int axis) float FRawPS2Controller::GetAxisResponseCurvePoint(int axis, int point) { + if (unsigned(axis) < NUM_AXES && unsigned(point) < 4) + { + return Axes[axis].ResponseCurve.pts[point]; + } return 0; } @@ -876,6 +898,10 @@ void FRawPS2Controller::SetAxisScale(int axis, float scale) void FRawPS2Controller::SetAxisDigitalThreshold(int axis, float threshold) { + if (unsigned(axis) < NUM_AXES) + { + Axes[axis].DigitalThreshold = threshold; + } } //========================================================================== @@ -886,6 +912,13 @@ void FRawPS2Controller::SetAxisDigitalThreshold(int axis, float threshold) void FRawPS2Controller::SetAxisResponseCurve(int axis, EJoyCurve preset) { + if (unsigned(axis) < NUM_AXES) + { + if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurvePreset = preset; + if (preset == JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurve = JOYCURVE[preset]; + } } //========================================================================== @@ -896,6 +929,11 @@ void FRawPS2Controller::SetAxisResponseCurve(int axis, EJoyCurve preset) void FRawPS2Controller::SetAxisResponseCurvePoint(int axis, int point, float value) { + if (unsigned(axis) < NUM_AXES && unsigned(point) < 4) + { + Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM; + Axes[axis].ResponseCurve.pts[point] = value; + } } //=========================================================================== @@ -906,6 +944,10 @@ void FRawPS2Controller::SetAxisResponseCurvePoint(int axis, int point, float val bool FRawPS2Controller::IsAxisDeadZoneDefault(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].DeadZone == JOYDEADZONE_DEFAULT; + } return true; } @@ -917,6 +959,10 @@ bool FRawPS2Controller::IsAxisDeadZoneDefault(int axis) bool FRawPS2Controller::IsAxisScaleDefault(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].Multiplier == DefaultAxes[axis].Multiplier; + } return true; } @@ -928,6 +974,10 @@ bool FRawPS2Controller::IsAxisScaleDefault(int axis) bool FRawPS2Controller::IsAxisDigitalThresholdDefault(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold; + } return true; } @@ -939,6 +989,10 @@ bool FRawPS2Controller::IsAxisDigitalThresholdDefault(int axis) bool FRawPS2Controller::IsAxisResponseCurveDefault(int axis) { + if (unsigned(axis) < NUM_AXES) + { + return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset; + } return true; } From e9dbf55f2e5ca4226a6776914e77bdee156a1f0a Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 21:56:28 -0400 Subject: [PATCH 266/384] Implemented dinput stubs --- src/common/platform/win32/i_dijoy.cpp | 74 +++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/src/common/platform/win32/i_dijoy.cpp b/src/common/platform/win32/i_dijoy.cpp index f87456606..2340bfc84 100644 --- a/src/common/platform/win32/i_dijoy.cpp +++ b/src/common/platform/win32/i_dijoy.cpp @@ -146,6 +146,7 @@ public: // MACROS ------------------------------------------------------------------ + // TYPES ------------------------------------------------------------------- class FDInputJoystick : public FInputDevice, IJoystickConfig @@ -207,6 +208,9 @@ protected: float Value; float DeadZone, DefaultDeadZone; float Multiplier, DefaultMultiplier; + float DigitalThreshold, DefaultDigitalThreshold; + EJoyCurve ResponseCurvePreset, DefaultResponseCurvePreset; + CubicBezier ResponseCurve; EJoyAxis GameAxis, DefaultGameAxis; uint8_t ButtonValue; }; @@ -460,16 +464,21 @@ void FDInputJoystick::ProcessInput() axisval = (value - info->Min) * 2.0 / (info->Max - info->Min) - 1.0; // Cancel out dead zone axisval = Joy_RemoveDeadZone(axisval, info->DeadZone, &buttonstate); + axisval = Joy_ApplyResponseCurveBezier(info->ResponseCurve, axisval); info->Value = float(axisval); if (i < NUM_JOYAXISBUTTONS && (i > 2 || Axes.Size() == 1)) { + if (abs(axisval) < info->DigitalThreshold) buttonstate = 0; Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2); } else if (i == 1) { // Since we sorted the axes, we know that the first two are definitely X and Y. // They are probably a single stick, so use angular position to determine buttons. - buttonstate = Joy_XYAxesToButtons(Axes[0].Value, axisval); + buttonstate = Joy_XYAxesToButtons( + (abs(Axes[0].Value) < Axes[0].DigitalThreshold) ? 0 : Axes[0].Value, + (abs(axisval) < info->DigitalThreshold) ? 0 : axisval + ); Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); } info->ButtonValue = buttonstate; @@ -773,33 +782,48 @@ void FDInputJoystick::SetDefaultConfig() Axes[i].DeadZone = JOYDEADZONE_DEFAULT; Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT; Axes[i].GameAxis = JOYAXIS_None; + Axes[i].DigitalThreshold = JOYTHRESH_DEFAULT; + Axes[i].ResponseCurvePreset = JOYCURVE_DEFAULT; + Axes[i].ResponseCurve = JOYCURVE[JOYCURVE_DEFAULT]; } // Triggers on a 360 controller have a much smaller deadzone. if (Axes.Size() == 5 && Axes[4].Guid == GUID_ZAxis) { Axes[4].DeadZone = 30 / 256.f; + Axes[4].DigitalThreshold = JOYTHRESH_TRIGGER; } // Two axes? Horizontal is yaw and vertical is forward. if (Axes.Size() == 2) { Axes[0].GameAxis = JOYAXIS_Yaw; + Axes[0].DigitalThreshold = JOYTHRESH_STICK_X; + Axes[1].GameAxis = JOYAXIS_Forward; + Axes[1].DigitalThreshold = JOYTHRESH_STICK_Y; } // Three axes? First two are movement, third is yaw. else if (Axes.Size() >= 3) { Axes[0].GameAxis = JOYAXIS_Side; + Axes[0].DigitalThreshold = JOYTHRESH_STICK_X; + Axes[1].GameAxis = JOYAXIS_Forward; + Axes[1].DigitalThreshold = JOYTHRESH_STICK_Y; + Axes[2].GameAxis = JOYAXIS_Yaw; + Axes[2].DigitalThreshold = JOYTHRESH_STICK_X; + // Four axes? First two are movement, last two are looking around. if (Axes.Size() >= 4) { Axes[3].GameAxis = JOYAXIS_Pitch; // Axes[3].Multiplier = 0.75f; + Axes[3].DigitalThreshold = JOYTHRESH_STICK_Y; // Five axes? Use the fifth one for moving up and down. if (Axes.Size() >= 5) { Axes[4].GameAxis = JOYAXIS_Up; + Axes[4].DigitalThreshold = JOYTHRESH_STICK_Y; } } } @@ -812,6 +836,8 @@ void FDInputJoystick::SetDefaultConfig() Axes[i].DefaultDeadZone = Axes[i].DeadZone; Axes[i].DefaultMultiplier = Axes[i].Multiplier; Axes[i].DefaultGameAxis = Axes[i].GameAxis; + Axes[i].DefaultDigitalThreshold = Axes[i].DigitalThreshold; + Axes[i].DefaultResponseCurvePreset = Axes[i].ResponseCurvePreset; } } @@ -938,7 +964,11 @@ float FDInputJoystick::GetAxisScale(int axis) float FDInputJoystick::GetAxisDigitalThreshold(int axis) { - return 0; + if (unsigned(axis) >= Axes.Size()) + { + return JOYTHRESH_DEFAULT; + } + return Axes[axis].DigitalThreshold; } //=========================================================================== @@ -949,7 +979,11 @@ float FDInputJoystick::GetAxisDigitalThreshold(int axis) EJoyCurve FDInputJoystick::GetAxisResponseCurve(int axis) { - return JOYCURVE_DEFAULT; + if (unsigned(axis) >= Axes.Size()) + { + return JOYCURVE_DEFAULT; + } + return Axes[axis].ResponseCurvePreset; } //=========================================================================== @@ -960,7 +994,11 @@ EJoyCurve FDInputJoystick::GetAxisResponseCurve(int axis) float FDInputJoystick::GetAxisResponseCurvePoint(int axis, int point) { - return 0; + if (unsigned(axis) >= Axes.Size() || unsigned(point) >= 4) + { + return 0; + } + return Axes[axis].ResponseCurve.pts[point]; } //=========================================================================== @@ -1013,6 +1051,10 @@ void FDInputJoystick::SetAxisScale(int axis, float scale) void FDInputJoystick::SetAxisDigitalThreshold(int axis, float threshold) { + if (unsigned(axis) < Axes.Size()) + { + Axes[axis].DigitalThreshold = threshold; + } } //=========================================================================== @@ -1023,6 +1065,13 @@ void FDInputJoystick::SetAxisDigitalThreshold(int axis, float threshold) void FDInputJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset) { + if (unsigned(axis) < Axes.Size()) + { + if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurvePreset = preset; + if (preset == JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurve = JOYCURVE[preset]; + } } //=========================================================================== @@ -1033,6 +1082,11 @@ void FDInputJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset) void FDInputJoystick::SetAxisResponseCurvePoint(int axis, int point, float value) { + if (unsigned(axis) < Axes.Size() && unsigned(point) < 4) + { + Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM; + Axes[axis].ResponseCurve.pts[point] = value; + } } //=========================================================================== @@ -1058,6 +1112,10 @@ bool FDInputJoystick::IsAxisDeadZoneDefault(int axis) bool FDInputJoystick::IsAxisScaleDefault(int axis) { + if (unsigned(axis) < Axes.Size()) + { + return Axes[axis].Multiplier == Axes[axis].DefaultMultiplier; + } return true; } @@ -1069,6 +1127,10 @@ bool FDInputJoystick::IsAxisScaleDefault(int axis) bool FDInputJoystick::IsAxisDigitalThresholdDefault(int axis) { + if (unsigned(axis) < Axes.Size()) + { + return Axes[axis].DigitalThreshold == Axes[axis].DefaultDigitalThreshold; + } return true; } @@ -1080,6 +1142,10 @@ bool FDInputJoystick::IsAxisDigitalThresholdDefault(int axis) bool FDInputJoystick::IsAxisResponseCurveDefault(int axis) { + if (unsigned(axis) < Axes.Size()) + { + return Axes[axis].ResponseCurvePreset == Axes[axis].DefaultResponseCurvePreset; + } return true; } From 4340c6df0b37561ae00050c9cb084ab31d11a3ad Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 21:57:36 -0400 Subject: [PATCH 267/384] Implemeted cocoa stubs --- .../platform/posix/cocoa/i_joystick.cpp | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/src/common/platform/posix/cocoa/i_joystick.cpp b/src/common/platform/posix/cocoa/i_joystick.cpp index 6eeb2eb56..ffb9f90a0 100644 --- a/src/common/platform/posix/cocoa/i_joystick.cpp +++ b/src/common/platform/posix/cocoa/i_joystick.cpp @@ -154,6 +154,11 @@ private: float defaultDeadZone; float sensitivity; float defaultSensitivity; + float digitalThreshold; + float defaultDigitalThreshold; + EJoyCurve responseCurvePreset; + EJoyCurve defaultResponseCurvePreset; + CubicBezier responseCurve; EJoyAxis gameAxis; EJoyAxis defaultGameAxis; @@ -388,17 +393,17 @@ float IOKitJoystick::GetAxisScale(int axis) float IOKitJoystick::GetAxisDigitalThreshold(int axis) { - return 0; + return IS_AXIS_VALID ? m_axes[axis].digitalThreshold : JOYTHRESH_DEFAULT; } EJoyCurve IOKitJoystick::GetAxisResponseCurve(int axis) { - return JOYCURVE_DEFAULT; + return IS_AXIS_VALID ? m_axes[axis].responseCurvePreset : JOYCURVE_DEFAULT; } float IOKitJoystick::GetAxisResponseCurvePoint(int axis, int point) { - return 0; + return ( IS_AXIS_VALID && unsigned(point) < 4 )? m_axes[axis].responseCurve.pts[point] : 0; } void IOKitJoystick::SetAxisDeadZone(int axis, float deadZone) @@ -429,14 +434,30 @@ void IOKitJoystick::SetAxisScale(int axis, float scale) void IOKitJoystick::SetAxisDigitalThreshold(int axis, float threshold) { + if (IS_AXIS_VALID) + { + m_axes[axis].digitalThreshold = threshold; + } } void IOKitJoystick::SetAxisResponseCurve(int axis, EJoyCurve preset) { + if (IS_AXIS_VALID) + { + if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return; + m_axes[axis].responseCurvePreset = preset; + if (preset == JOYCURVE_CUSTOM) return; + m_axes[axis].responseCurve = JOYCURVE[preset]; + } } void IOKitJoystick::SetAxisResponseCurvePoint(int axis, int point, float value) { + if (IS_AXIS_VALID && unsigned(point) < 4) + { + m_axes[axis].responseCurvePreset = JOYCURVE_CUSTOM; + m_axes[axis].responseCurve.pts[point] = value; + } } bool IOKitJoystick::IsSensitivityDefault() @@ -451,16 +472,6 @@ bool IOKitJoystick::IsAxisDeadZoneDefault(int axis) : true; } -bool IOKitJoystick::IsAxisDigitalThresholdDefault(int axis) -{ - return true; -} - -bool IOKitJoystick::IsAxisResponseCurveDefault(int axis) -{ - return true; -} - bool IOKitJoystick::IsAxisMapDefault(int axis) { return IS_AXIS_VALID @@ -475,7 +486,19 @@ bool IOKitJoystick::IsAxisScaleDefault(int axis) : true; } +bool IOKitJoystick::IsAxisDigitalThresholdDefault(int axis) +{ + return IS_AXIS_VALID + ? (m_axes[axis].digitalThreshold == m_axes[axis].defaultDigitalThreshold) + : true; +} +bool IOKitJoystick::IsAxisResponseCurveDefault(int axis) +{ + return IS_AXIS_VALID + ? m_axes[axis].responseCurvePreset == m_axes[axis].defaultResponseCurvePreset + : true; +} bool IOKitJoystick::GetEnabled() { @@ -499,6 +522,9 @@ void IOKitJoystick::SetDefaultConfig() m_axes[i].deadZone = JOYDEADZONE_DEFAULT; m_axes[i].sensitivity = JOYSENSITIVITY_DEFAULT; m_axes[i].gameAxis = JOYAXIS_None; + m_axes[i].digitalThreshold = JOYTHRESH_DEFAULT; + m_axes[i].responseCurvePreset = JOYCURVE_DEFAULT; + m_axes[i].responseCurve = JOYCURVE[JOYCURVE_DEFAULT]; } // Two axes? Horizontal is yaw and vertical is forward. @@ -506,7 +532,10 @@ void IOKitJoystick::SetDefaultConfig() if (2 == axisCount) { m_axes[0].gameAxis = JOYAXIS_Yaw; + m_axes[0].digitalThreshold = JOYTHRESH_STICK_X; + m_axes[1].gameAxis = JOYAXIS_Forward; + m_axes[1].digitalThreshold = JOYTHRESH_STICK_Y; } // Three axes? First two are movement, third is yaw. @@ -514,8 +543,13 @@ void IOKitJoystick::SetDefaultConfig() else if (axisCount >= 3) { m_axes[0].gameAxis = JOYAXIS_Side; + m_axes[0].digitalThreshold = JOYTHRESH_STICK_X; + m_axes[1].gameAxis = JOYAXIS_Forward; + m_axes[1].digitalThreshold = JOYTHRESH_STICK_Y; + m_axes[2].gameAxis = JOYAXIS_Yaw; + m_axes[2].digitalThreshold = JOYTHRESH_STICK_X; // Four axes? First two are movement, last two are looking around. @@ -523,12 +557,14 @@ void IOKitJoystick::SetDefaultConfig() { m_axes[3].gameAxis = JOYAXIS_Pitch; // ??? m_axes[3].sensitivity = 0.75f; + m_axes[3].digitalThreshold = JOYTHRESH_STICK_Y; // Five axes? Use the fifth one for moving up and down. if (axisCount >= 5) { m_axes[4].gameAxis = JOYAXIS_Up; + m_axes[4].digitalThreshold = JOYTHRESH_STICK_Y; } } } @@ -540,9 +576,11 @@ void IOKitJoystick::SetDefaultConfig() for (size_t i = 0; i < axisCount; ++i) { - m_axes[i].defaultDeadZone = m_axes[i].deadZone; - m_axes[i].defaultSensitivity = m_axes[i].sensitivity; - m_axes[i].defaultGameAxis = m_axes[i].gameAxis; + m_axes[i].defaultDeadZone = m_axes[i].deadZone; + m_axes[i].defaultSensitivity = m_axes[i].sensitivity; + m_axes[i].defaultGameAxis = m_axes[i].gameAxis; + m_axes[i].defaultDigitalThreshold = m_axes[i].digitalThreshold; + m_axes[i].defaultResponseCurvePreset = m_axes[i].responseCurvePreset; } } @@ -638,8 +676,9 @@ void IOKitJoystick::ProcessAxes() const double scaledValue = scaledMin + (event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue); const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL); + const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue); - axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity); + axis.value = static_cast(smoothedValue * m_sensitivity * axis.sensitivity); } else { @@ -671,8 +710,9 @@ bool IOKitJoystick::ProcessAxis(const IOHIDEventStruct& event) const double scaledValue = scaledMin + (event.value - axis.minValue) * (scaledMax - scaledMin) / (axis.maxValue - axis.minValue); const double filteredValue = Joy_RemoveDeadZone(scaledValue, axis.deadZone, NULL); + const double smoothedValue = Joy_ApplyResponseCurveBezier(axis.responseCurve, filteredValue); - axis.value = static_cast(filteredValue * m_sensitivity * axis.sensitivity); + axis.value = static_cast(smoothedValue * m_sensitivity * axis.sensitivity); return true; } From d7bb21e2e30bacc329e1252f5f233e58a5423878 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 22:02:53 -0400 Subject: [PATCH 268/384] Implemented sdl stubs and improved controller reconnection --- src/common/platform/posix/sdl/i_input.cpp | 24 +- src/common/platform/posix/sdl/i_input.h | 13 + src/common/platform/posix/sdl/i_joystick.cpp | 379 ++++++++++++------- 3 files changed, 267 insertions(+), 149 deletions(-) create mode 100644 src/common/platform/posix/sdl/i_input.h diff --git a/src/common/platform/posix/sdl/i_input.cpp b/src/common/platform/posix/sdl/i_input.cpp index d9c05a666..da7ac59c2 100644 --- a/src/common/platform/posix/sdl/i_input.cpp +++ b/src/common/platform/posix/sdl/i_input.cpp @@ -32,13 +32,13 @@ */ #include #include +#include "i_input.h" #include "c_cvars.h" #include "dobject.h" #include "m_argv.h" #include "m_joy.h" #include "v_video.h" -#include "d_eventbase.h" #include "d_gui.h" #include "c_buttons.h" #include "c_console.h" @@ -50,10 +50,6 @@ #include "engineerrors.h" #include "i_interface.h" - -static void I_CheckGUICapture (); -static void I_CheckNativeMouse (); - bool GUICapture; static bool NativeMouse = true; @@ -529,12 +525,12 @@ void MessagePump (const SDL_Event &sev) case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: - if (SDL_IsGameController(sev.jdevice.which)) + if (SDL_GameControllerFromInstanceID(sev.jdevice.which)) break; // let SDL_CONTROLLERBUTTON* handle this event.type = sev.type == SDL_JOYBUTTONDOWN ? EV_KeyDown : EV_KeyUp; event.data1 = KEY_FIRSTJOYBUTTON + sev.jbutton.button; if(event.data1 != 0) - D_PostEvent(&event); + I_JoyConsumeEvent(sev.jdevice.which, &event); break; case SDL_CONTROLLERBUTTONDOWN: @@ -566,14 +562,22 @@ void MessagePump (const SDL_Event &sev) default: event.data1 = 0; } if(event.data1 != 0) - D_PostEvent(&event); + I_JoyConsumeEvent(sev.cbutton.which, &event); break; case SDL_JOYDEVICEADDED: - case SDL_JOYDEVICEREMOVED: case SDL_CONTROLLERDEVICEADDED: + if (sev.type == SDL_JOYDEVICEADDED && SDL_IsGameController(sev.jdevice.which)) // DeviceIndex Here + break; // skip double event + I_UpdateDeviceList(); + event.type = EV_DeviceChange; + D_PostEvent (&event); + break; + + case SDL_JOYDEVICEREMOVED: case SDL_CONTROLLERDEVICEREMOVED: - if ((sev.type == SDL_JOYDEVICEADDED || sev.type == SDL_JOYDEVICEREMOVED) && SDL_IsGameController(sev.jdevice.which)) + case SDL_CONTROLLERDEVICEREMAPPED: + if (sev.type == SDL_JOYDEVICEREMOVED && SDL_GameControllerFromInstanceID(sev.jdevice.which)) break; // skip double event I_UpdateDeviceList(); event.type = EV_DeviceChange; diff --git a/src/common/platform/posix/sdl/i_input.h b/src/common/platform/posix/sdl/i_input.h new file mode 100644 index 000000000..6ca2e05aa --- /dev/null +++ b/src/common/platform/posix/sdl/i_input.h @@ -0,0 +1,13 @@ +#ifndef I_INPUT_H +#define I_INPUT_H + +#include "d_eventbase.h" + +extern int WaitingForKey; + +static void I_CheckGUICapture (); +static void I_CheckNativeMouse (); + +void I_JoyConsumeEvent(int instanceID, event_t * event); + +#endif diff --git a/src/common/platform/posix/sdl/i_joystick.cpp b/src/common/platform/posix/sdl/i_joystick.cpp index 000562e30..d37b4f3a6 100644 --- a/src/common/platform/posix/sdl/i_joystick.cpp +++ b/src/common/platform/posix/sdl/i_joystick.cpp @@ -32,28 +32,32 @@ */ #include #include +#include #include "basics.h" #include "cmdlib.h" #include "d_eventbase.h" +#include "i_input.h" #include "m_joy.h" -#include "keydef.h" class SDLInputJoystick: public IJoystickConfig { public: SDLInputJoystick(int DeviceIndex) : DeviceIndex(DeviceIndex), - Multiplier(JOYSENSITIVITY_DEFAULT) , - Enabled(true) + InstanceID(SDL_JoystickGetDeviceInstanceID(DeviceIndex)), + Multiplier(JOYSENSITIVITY_DEFAULT), + Enabled(true), + SettingsChanged(false) { if (SDL_IsGameController(DeviceIndex)) { Mapping = SDL_GameControllerOpen(DeviceIndex); + Device = NULL; DefaultAxes = DefaultControllerAxes; - DefaultAxesCount = sizeof(DefaultControllerAxes) / sizeof(EJoyAxis); + DefaultAxesCount = sizeof(DefaultControllerAxes) / sizeof(DefaultAxisConfig); if(Mapping != NULL) { @@ -66,8 +70,10 @@ public: else { Device = SDL_JoystickOpen(DeviceIndex); + Mapping = NULL; + DefaultAxes = DefaultJoystickAxes; - DefaultAxesCount = sizeof(DefaultJoystickAxes) / sizeof(EJoyAxis); + DefaultAxesCount = sizeof(DefaultJoystickAxes) / sizeof(DefaultAxisConfig); if(Device != NULL) { @@ -77,13 +83,16 @@ public: SetDefaultConfig(); } } + M_LoadJoystickConfig(this); } ~SDLInputJoystick() { - if(Device != NULL || Mapping != NULL) + if(IsValid() && SettingsChanged) M_SaveJoystickConfig(this); - SDL_GameControllerClose(Mapping); - SDL_JoystickClose(Device); + if (Mapping) + SDL_GameControllerClose(Mapping); + if (Device) + SDL_JoystickClose(Device); } bool IsValid() const @@ -103,6 +112,7 @@ public: } void SetSensitivity(float scale) { + SettingsChanged = true; Multiplier = scale; } @@ -128,37 +138,55 @@ public: } float GetAxisDigitalThreshold(int axis) { - return 0; + return Axes[axis].DigitalThreshold; } EJoyCurve GetAxisResponseCurve(int axis) { - return JOYCURVE_DEFAULT; + return Axes[axis].ResponseCurvePreset; } float GetAxisResponseCurvePoint(int axis, int point) { - return 0; + return unsigned(point) < 4 + ? Axes[axis].ResponseCurve.pts[point] + : 0; }; void SetAxisDeadZone(int axis, float zone) { + SettingsChanged = true; Axes[axis].DeadZone = clamp(zone, 0.f, 1.f); } void SetAxisMap(int axis, EJoyAxis gameaxis) { + SettingsChanged = true; Axes[axis].GameAxis = gameaxis; } void SetAxisScale(int axis, float scale) { + SettingsChanged = true; Axes[axis].Multiplier = scale; } void SetAxisDigitalThreshold(int axis, float threshold) { + SettingsChanged = true; + Axes[axis].DigitalThreshold = threshold; } void SetAxisResponseCurve(int axis, EJoyCurve preset) { + if (preset >= NUM_JOYCURVE || preset < JOYCURVE_CUSTOM) return; + SettingsChanged = true; + Axes[axis].ResponseCurvePreset = preset; + if (preset == JOYCURVE_CUSTOM) return; + Axes[axis].ResponseCurve = JOYCURVE[preset]; } void SetAxisResponseCurvePoint(int axis, int point, float value) { + if (unsigned(point) < 4) + { + SettingsChanged = true; + Axes[axis].ResponseCurvePreset = JOYCURVE_CUSTOM; + Axes[axis].ResponseCurve.pts[point] = value; + } } // Used by the saver to not save properties that are at their defaults. @@ -168,58 +196,85 @@ public: } bool IsAxisDeadZoneDefault(int axis) { - return Axes[axis].DeadZone <= JOYDEADZONE_DEFAULT; + if(axis >= DefaultAxesCount) + return Axes[axis].DeadZone == JOYDEADZONE_DEFAULT; + return Axes[axis].DeadZone == DefaultAxes[axis].DeadZone; } bool IsAxisMapDefault(int axis) { if(axis >= DefaultAxesCount) return Axes[axis].GameAxis == JOYAXIS_None; - return Axes[axis].GameAxis == DefaultAxes[axis]; + return Axes[axis].GameAxis == DefaultAxes[axis].GameAxis; } bool IsAxisScaleDefault(int axis) { - return Axes[axis].Multiplier == JOYSENSITIVITY_DEFAULT; + if(axis >= DefaultAxesCount) + return Axes[axis].Multiplier == JOYSENSITIVITY_DEFAULT; + return Axes[axis].Multiplier == DefaultAxes[axis].Multiplier; } bool IsAxisDigitalThresholdDefault(int axis) { - return true; + if(axis >= DefaultAxesCount) + return Axes[axis].DigitalThreshold == JOYTHRESH_DEFAULT; + return Axes[axis].DigitalThreshold == DefaultAxes[axis].DigitalThreshold; } bool IsAxisResponseCurveDefault(int axis) { - return true; - } + if(axis >= DefaultAxesCount) + return Axes[axis].ResponseCurvePreset == JOYCURVE_DEFAULT; + return Axes[axis].ResponseCurvePreset == DefaultAxes[axis].ResponseCurvePreset; + } void SetDefaultConfig() { + if (Axes.size() == 0) + { + for(int i = 0;i < GetNumAxes();i++) + { + Axes.Push({}); + } + } + for(int i = 0;i < GetNumAxes();i++) { - AxisInfo info; - if (Mapping) { switch(i) { - case SDL_CONTROLLER_AXIS_LEFTX: info.Name = "Left Stick X"; break; - case SDL_CONTROLLER_AXIS_LEFTY: info.Name = "Left Stick Y"; break; - case SDL_CONTROLLER_AXIS_RIGHTX: info.Name = "Right Stick X"; break; - case SDL_CONTROLLER_AXIS_RIGHTY: info.Name = "Right Stick Y"; break; - case SDL_CONTROLLER_AXIS_TRIGGERLEFT: info.Name = "Left Trigger"; break; - case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: info.Name = "Right Trigger"; break; - default: info.Name.Format("Axis %d", i+1); break; + case SDL_CONTROLLER_AXIS_LEFTX: Axes[i].Name = "Left Stick X"; break; + case SDL_CONTROLLER_AXIS_LEFTY: Axes[i].Name = "Left Stick Y"; break; + case SDL_CONTROLLER_AXIS_RIGHTX: Axes[i].Name = "Right Stick X"; break; + case SDL_CONTROLLER_AXIS_RIGHTY: Axes[i].Name = "Right Stick Y"; break; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: Axes[i].Name = "Left Trigger"; break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: Axes[i].Name = "Right Trigger"; break; + default: Axes[i].Name.Format("Axis %d", i+1); break; } } else { if(i < NumAxes) - info.Name.Format("Axis %d", i+1); + Axes[i].Name.Format("Axis %d", i+1); else - info.Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y'); + Axes[i].Name.Format("Hat %d (%c)", (i-NumAxes)/2 + 1, (i-NumAxes)%2 == 0 ? 'x' : 'y'); } - info.DeadZone = JOYDEADZONE_DEFAULT; - info.Multiplier = JOYSENSITIVITY_DEFAULT; - info.Value = 0.0; - info.ButtonValue = 0; + Axes[i].Value = 0.0; + Axes[i].ButtonValue = 0; - info.GameAxis = (i < DefaultAxesCount)? DefaultAxes[i]: JOYAXIS_None; - - Axes.Push(info); + if (i < DefaultAxesCount) + { + Axes[i].GameAxis = DefaultAxes[i].GameAxis; + Axes[i].DeadZone = DefaultAxes[i].DeadZone; + Axes[i].Multiplier = DefaultAxes[i].Multiplier; + Axes[i].DigitalThreshold = DefaultAxes[i].DigitalThreshold; + Axes[i].ResponseCurvePreset = DefaultAxes[i].ResponseCurvePreset; + Axes[i].ResponseCurve = JOYCURVE[DefaultAxes[i].ResponseCurvePreset]; + } + else + { + Axes[i].GameAxis = JOYAXIS_None; + Axes[i].DeadZone = JOYDEADZONE_DEFAULT; + Axes[i].Multiplier = JOYSENSITIVITY_DEFAULT; + Axes[i].DigitalThreshold = JOYTHRESH_DEFAULT; + Axes[i].ResponseCurvePreset = JOYCURVE_DEFAULT; + Axes[i].ResponseCurve = JOYCURVE[JOYCURVE_DEFAULT]; + } } } @@ -230,6 +285,7 @@ public: void SetEnabled(bool enabled) { + SettingsChanged = true; Enabled = enabled; } @@ -254,7 +310,104 @@ public: } } - void ProcessInput(); + void ProcessInput() { + uint8_t buttonstate; + + if (Mapping) + { + // GameController API available + + auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold; + auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold; + + for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i) + { + buttonstate = 0; + + Axes[i].Value = SDL_GameControllerGetAxis(Mapping, static_cast(i))/32767.0; + Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate); + Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value); + } + + auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].DigitalThreshold; + auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].DigitalThreshold; + + if (lastTriggerL != currTriggerL) Joy_GenerateButtonEvent(currTriggerL, KEY_PAD_LTRIGGER); + if (lastTriggerR != currTriggerR) Joy_GenerateButtonEvent(currTriggerR, KEY_PAD_RTRIGGER); + + // todo: right stick + buttonstate = Joy_XYAxesToButtons( + abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value, + abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value + ); + Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); + Axes[0].ButtonValue = buttonstate; + } + else + { + // Joystick API fallback + + for (int i = 0; i < NumAxes; ++i) + { + buttonstate = 0; + + Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0; + Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate); + Axes[i].Value = Joy_ApplyResponseCurveBezier(Axes[i].ResponseCurve, Axes[i].Value); + + // Map button to axis + // X and Y are handled differently so if we have 2 or more axes then we'll use that code instead. + if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS)) + { + Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2); + Axes[i].ButtonValue = buttonstate; + } + } + + if(NumAxes > 1) + { + buttonstate = Joy_XYAxesToButtons( + abs(Axes[0].Value) < Axes[0].DigitalThreshold ? 0 : Axes[0].Value, + abs(Axes[1].Value) < Axes[1].DigitalThreshold ? 0 : Axes[1].Value + ); + Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); + Axes[0].ButtonValue = buttonstate; + } + + // Map POV hats to buttons and axes. Why axes? Well apparently I have + // a gamepad where the left control stick is a POV hat (instead of the + // d-pad like you would expect, no that's pressure sensitive). Also + // KDE's joystick dialog maps them to axes as well. + for (int i = 0; i < NumHats; ++i) + { + AxisInfo &x = Axes[NumAxes + i*2]; + AxisInfo &y = Axes[NumAxes + i*2 + 1]; + + buttonstate = SDL_JoystickGetHat(Device, i); + + // If we're going to assume that we can pass SDL's value into + // Joy_GenerateButtonEvents then we might as well assume the format here. + if(buttonstate & 0x1) // Up + y.Value = -1.0; + else if(buttonstate & 0x4) // Down + y.Value = 1.0; + else + y.Value = 0.0; + if(buttonstate & 0x2) // Left + x.Value = 1.0; + else if(buttonstate & 0x8) // Right + x.Value = -1.0; + else + x.Value = 0.0; + + if(i < 4) + { + Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4); + x.ButtonValue = buttonstate; + } + } + } + } protected: struct AxisInfo @@ -262,16 +415,28 @@ protected: FString Name; float DeadZone; float Multiplier; + float DigitalThreshold; + EJoyCurve ResponseCurvePreset; + CubicBezier ResponseCurve; EJoyAxis GameAxis; double Value; uint8_t ButtonValue; }; - static const EJoyAxis DefaultJoystickAxes[5]; - static const EJoyAxis DefaultControllerAxes[6]; - const EJoyAxis * DefaultAxes; + struct DefaultAxisConfig + { + float DeadZone; + EJoyAxis GameAxis; + float Multiplier; + float DigitalThreshold; + EJoyCurve ResponseCurvePreset; + }; + static const DefaultAxisConfig DefaultJoystickAxes[5]; + static const DefaultAxisConfig DefaultControllerAxes[6]; + const DefaultAxisConfig * DefaultAxes; int DefaultAxesCount; int DeviceIndex; + int InstanceID; SDL_Joystick *Device; SDL_GameController *Mapping; @@ -280,24 +445,36 @@ protected: TArray Axes; int NumAxes; int NumHats; + bool SettingsChanged; friend class SDLInputJoystickManager; }; // [Nash 4 Feb 2024] seems like on Linux, the third axis is actually the Left Trigger, resulting in the player uncontrollably looking upwards. -const EJoyAxis SDLInputJoystick::DefaultJoystickAxes[5] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_None, JOYAXIS_Yaw, JOYAXIS_Pitch}; - -// Defaults if we have access to the Gamepad API for this device -const EJoyAxis SDLInputJoystick::DefaultControllerAxes[6] = {JOYAXIS_Side, JOYAXIS_Forward, JOYAXIS_Yaw, JOYAXIS_Pitch, JOYAXIS_None, JOYAXIS_None}; - +const SDLInputJoystick::DefaultAxisConfig SDLInputJoystick::DefaultJoystickAxes[5] = { + {JOYDEADZONE_DEFAULT, JOYAXIS_Side, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_None, JOYSENSITIVITY_DEFAULT, JOYTHRESH_DEFAULT, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT} +}; +// Defaults if we have access to the GameController API for this device +const SDLInputJoystick::DefaultAxisConfig SDLInputJoystick::DefaultControllerAxes[6] = { + {JOYDEADZONE_DEFAULT, JOYAXIS_Side, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_Forward, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_Yaw, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_X, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_Pitch, JOYSENSITIVITY_DEFAULT, JOYTHRESH_STICK_Y, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_None, JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT}, + {JOYDEADZONE_DEFAULT, JOYAXIS_None, JOYSENSITIVITY_DEFAULT, JOYTHRESH_TRIGGER, JOYCURVE_DEFAULT}, +}; class SDLInputJoystickManager { public: SDLInputJoystickManager() { - this->UpdateDeviceList(); + UpdateDeviceList(); } void UpdateDeviceList() @@ -334,6 +511,19 @@ public: if(Joysticks[i]->Enabled) Joysticks[i]->ProcessInput(); } + bool IsJoystickEnabled(int instanceID) + { + for(unsigned int i = 0; i < Joysticks.Size(); i++) + { + if (Joysticks[i]->InstanceID != instanceID) + { + continue; + } + return Joysticks[i]->Enabled; + } + return false; + } + protected: TDeletingArray Joysticks; }; @@ -381,103 +571,14 @@ void I_ProcessJoysticks() JoystickManager->ProcessInput(); } -void PostKeyEvent(bool down, EKeyCodes which) +void I_JoyConsumeEvent(int instanceID, event_t * event) { - event_t event = { 0,0,0,0,0,0,0 }; - event.type = down ? EV_KeyDown : EV_KeyUp; - event.data1 = which; - D_PostEvent(&event); -} - -void SDLInputJoystick::ProcessInput() { - uint8_t buttonstate; - - if (Mapping) + if (event->type == EV_KeyDown) { - // GameController API available - - auto lastTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > JOYTHRESH_DEFAULT; - auto lastTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > JOYTHRESH_DEFAULT; - - for (auto i = 0; i < SDL_CONTROLLER_AXIS_MAX && i < NumAxes; ++i) - { - buttonstate = 0; - - Axes[i].Value = SDL_GameControllerGetAxis(Mapping, static_cast(i))/32767.0; - Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate); - } - - auto currTriggerL = Axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT].Value > JOYTHRESH_DEFAULT; - auto currTriggerR = Axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT].Value > JOYTHRESH_DEFAULT; - - if (lastTriggerL != currTriggerL) PostKeyEvent(currTriggerL, KEY_PAD_LTRIGGER); - if (lastTriggerR != currTriggerR) PostKeyEvent(currTriggerR, KEY_PAD_RTRIGGER); - - buttonstate = Joy_XYAxesToButtons(Axes[0].Value, Axes[1].Value); - Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); - Axes[0].ButtonValue = buttonstate; - - } - else - { - // Joystick API fallback - - for (int i = 0; i < NumAxes; ++i) - { - buttonstate = 0; - - Axes[i].Value = SDL_JoystickGetAxis(Device, i)/32767.0; - Axes[i].Value = Joy_RemoveDeadZone(Axes[i].Value, Axes[i].DeadZone, &buttonstate); - - // Map button to axis - // X and Y are handled differently so if we have 2 or more axes then we'll use that code instead. - if (NumAxes == 1 || (i >= 2 && i < NUM_JOYAXISBUTTONS)) - { - Joy_GenerateButtonEvents(Axes[i].ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2); - Axes[i].ButtonValue = buttonstate; - } - } - - if(NumAxes > 1) - { - buttonstate = Joy_XYAxesToButtons(Axes[0].Value, Axes[1].Value); - Joy_GenerateButtonEvents(Axes[0].ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); - Axes[0].ButtonValue = buttonstate; - } - - // Map POV hats to buttons and axes. Why axes? Well apparently I have - // a gamepad where the left control stick is a POV hat (instead of the - // d-pad like you would expect, no that's pressure sensitive). Also - // KDE's joystick dialog maps them to axes as well. - for (int i = 0; i < NumHats; ++i) - { - AxisInfo &x = Axes[NumAxes + i*2]; - AxisInfo &y = Axes[NumAxes + i*2 + 1]; - - buttonstate = SDL_JoystickGetHat(Device, i); - - // If we're going to assume that we can pass SDL's value into - // Joy_GenerateButtonEvents then we might as well assume the format here. - if(buttonstate & 0x1) // Up - y.Value = -1.0; - else if(buttonstate & 0x4) // Down - y.Value = 1.0; - else - y.Value = 0.0; - if(buttonstate & 0x2) // Left - x.Value = 1.0; - else if(buttonstate & 0x8) // Right - x.Value = -1.0; - else - x.Value = 0.0; - - if(i < 4) - { - Joy_GenerateButtonEvents(x.ButtonValue, buttonstate, 4, KEY_JOYPOV1_UP + i*4); - x.ButtonValue = buttonstate; - } - } + bool okay = use_joystick && JoystickManager && JoystickManager->IsJoystickEnabled(instanceID); + if (!okay) return; } + D_PostEvent(event); } IJoystickConfig *I_UpdateDeviceList() From 5810faec1d3b81937b737b2774442e0f215bef4e Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Mon, 7 Jul 2025 22:20:27 -0400 Subject: [PATCH 269/384] Removed debug printout --- wadsrc/static/zscript/engine/ui/menu/optionmenu.zs | 1 - 1 file changed, 1 deletion(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index 17d52a7cb..834016238 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -299,7 +299,6 @@ class OptionMenu : Menu int rowheight = OptionMenuSettings.mLinespacing * CleanYfac_1 + 1; int maxitems = (lastrow - y) / rowheight + 1; - Console.printf("%d %d", maxitems, RemainingVisibleItems(0)); if (maxitems < RemainingVisibleItems(0)) { maxItems -= 2; From ccf178577d332f97c89418d864e45d26b8903e39 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 7 Jul 2025 23:44:47 -0400 Subject: [PATCH 270/384] Updated client-side ACS handling Moved to a new ownership system. Only clients that own the activators will be allowed to call truly client-side scripts. Server objects that attempt to do this will instead run on the server. --- src/playsim/p_acs.cpp | 75 +++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/playsim/p_acs.cpp b/src/playsim/p_acs.cpp index 63432a9eb..98c29ad9e 100644 --- a/src/playsim/p_acs.cpp +++ b/src/playsim/p_acs.cpp @@ -652,10 +652,35 @@ struct CallReturn unsigned int EntryInstrCount; }; - -static bool IsClientSideScript(const ScriptPtr& script) +// Only objects owned by the client are allowed to call client-side scripts. These same scripts must be ignored from +// objects belonging to other clients since the nature of them calling after the client is backed up will be as though +// they were never called at all. For things with no activator, assume it's from a frontend script and the modder knows +// what they're doing. +static bool CanCreateClientSideScripts(const AActor* activator) { - return (script.Flags & SCRIPTF_ClientSide); + return activator == nullptr || activator->IsClientside() || (activator->player != nullptr && activator->player->mo == activator); +} + +static AActor* GetScriptOwner(AActor& activator) +{ + return activator.player != nullptr ? activator.player->mo : &activator; +} + +static bool ShouldIgnoreClientSideScript(AActor* activator) +{ + if (activator == nullptr || activator->IsClientside()) + return false; + + AActor* owner = GetScriptOwner(*activator); + return !owner->Level->isConsolePlayer(owner); +} + +// Even if it's marked as such, don't allow things that don't belong to clients to be called locally. Treat +// them like server calls (in the future they may be ignored entirely since a proper ownership system is +// preferred). +static bool IsClientSideScript(const AActor* activator, const ScriptPtr& script) +{ + return (script.Flags & SCRIPTF_ClientSide) && CanCreateClientSideScripts(activator); } class DLevelScript : public DObject @@ -790,7 +815,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, bool clientside); + const int *args, int argcount, int flags); struct FBehavior::ArrayInfo @@ -3328,7 +3353,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, IsClientSideScript(*ptr)); + ptr, this, &arg1, 1, always ? ACS_ALWAYS : 0); if (nullptr != runningScript && runNow) { runningScript->RunScript(); @@ -10419,8 +10444,14 @@ 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, bool clientside) + const int *args, int argcount, int flags) { + // Intentionally not checking netgame status here as this should be consistent between singleplayer + // and multiplayer (this is how it will work should client/server ever get in, so force it now). + const bool clientside = IsClientSideScript(who, *code); + if (clientside && ShouldIgnoreClientSideScript(who)) + return nullptr; + DACSThinker *controller = clientside ? l->ClientSideACSThinker : l->ACSThinker; DLevelScript **running; @@ -10535,7 +10566,7 @@ void FLevelLocals::DoDeferedScripts () nullptr, def->script, scriptdata, module, def->args, 3, - def->type == acsdefered_t::defexealways ? ACS_ALWAYS : 0, IsClientSideScript(*scriptdata)); + def->type == acsdefered_t::defexealways ? ACS_ALWAYS : 0); break; case acsdefered_t::defsuspend: @@ -10581,7 +10612,9 @@ static void addDefered (level_info_t *i, acsdefered_t::EType type, int script, c } } -EXTERN_CVAR (Bool, sv_cheats) +// Allow debugging by default in singleplayer, but give the option to test request denials. +CVAR(Bool, allowsingleplayerscripts, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) +CVAR(Bool, sv_allowallscripts, false, CVAR_SERVERINFO | CVAR_NOSAVE) int P_StartScript (FLevelLocals *Level, AActor *who, line_t *where, int script, const char *map, const int *args, int argcount, int flags) { @@ -10592,30 +10625,30 @@ int P_StartScript (FLevelLocals *Level, AActor *who, line_t *where, int script, if ((scriptdata = Level->Behaviors.FindScript (script, module)) != NULL) { - if ((flags & ACS_NET) && netgame && !sv_cheats) + // Make sure only scripts flagged as Net can be ran if requesting one. + if ((flags & ACS_NET) && !(scriptdata->Flags & SCRIPTF_Net) + && !sv_allowallscripts && (netgame || !allowsingleplayerscripts)) { - // If playing multiplayer and cheats are disallowed, check to - // make sure only net scripts are run. - if (!(scriptdata->Flags & SCRIPTF_Net)) + if (who->Level->isConsolePlayer(who)) + { + Printf(PRINT_BOLD, "Non-net scripts are currently not requestable\n"); + } + else if (consoleplayer == Net_Arbitrator && !IsClientSideScript(who, *scriptdata)) { Printf(PRINT_BOLD, "%s tried to puke %s (\n", who->player->userinfo.GetName(), ScriptPresentation(script).GetChars()); for (int i = 0; i < argcount; ++i) { - Printf(PRINT_BOLD, "%d%s", args[i], i == argcount-1 ? "" : ", "); + Printf(PRINT_BOLD, "%d%s", args[i], i == argcount - 1 ? "" : ", "); } Printf(PRINT_BOLD, ")\n"); - return false; } + + return false; } - 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); - } + DLevelScript* runningScript = P_GetScriptGoing(Level, who, where, script, + scriptdata, module, args, argcount, flags); if (runningScript != NULL) { From 17f1c90d4a7bba5b0fae0560366611bbad8a5e18 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 7 Jul 2025 23:54:30 -0400 Subject: [PATCH 271/384] Always back up players even in singleplayer This way client-side actions will be consistent between multiplayer and singleplayer. --- src/playsim/p_user.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 1f109d808..2b8e9cf8d 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -1474,26 +1474,14 @@ nodetype *RestoreNodeList(AActor *act, nodetype *linktype::*otherlist, TArraymo == NULL || player != player->mo->Level->GetConsolePlayer() || - player->playerstate != PST_LIVE || - (!netgame && cl_debugprediction == 0) || - /*player->morphTics ||*/ (player->cheats & CF_PREDICTING)) { return; } - maxtic = ClientTic; - - if (gametic == maxtic) - { - return; - } - FRandom::SaveRNGState(PredictionRNG); // Save original values for restoration later @@ -1546,6 +1534,12 @@ void P_PredictPlayer (player_t *player) } act->BlockNode = NULL; + int maxtic = ClientTic; + if (gametic == maxtic || player->playerstate != PST_LIVE) + { + return; + } + // This essentially acts like a mini P_Ticker where only the stuff relevant to the client is actually // called. Call order is preserved. bool rubberband = false, rubberbandLimit = false; From a73d9f6e8f765384ec6fa0a79ceeba6485592ee4 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 8 Jul 2025 02:52:17 -0400 Subject: [PATCH 272/384] Added support for client-side Behaviors --- src/g_levellocals.h | 12 +++++++++--- src/p_setup.cpp | 1 + src/playsim/p_mobj.cpp | 5 +++++ src/scripting/vmiterators.cpp | 20 +++++++++++++++++--- wadsrc/static/zscript/actors/actor.zs | 1 + 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 9764ebb39..d34f2190e 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -762,7 +762,7 @@ public: DVisualThinker* VisualThinkerHead = nullptr; // links to global game objects - TArray ActorBehaviors; + TArray ActorBehaviors, ClientSideActorBehaviors; TArray> CorpseQueue; TObjPtr FraggleScriptThinker = MakeObjPtr(nullptr); TObjPtr ACSThinker = MakeObjPtr(nullptr); @@ -780,7 +780,10 @@ public: if (b.Level == nullptr) { b.Level = this; - ActorBehaviors.Push(&b); + if (b.IsClientside()) + ClientSideActorBehaviors.Push(&b); + else + ActorBehaviors.Push(&b); } } @@ -789,7 +792,10 @@ public: if (b.Level == this) { b.Level = nullptr; - ActorBehaviors.Delete(ActorBehaviors.Find(&b)); + if (b.IsClientside()) + ClientSideActorBehaviors.Delete(ClientSideActorBehaviors.Find(&b)); + else + ActorBehaviors.Delete(ActorBehaviors.Find(&b)); } } diff --git a/src/p_setup.cpp b/src/p_setup.cpp index b948d4734..b698b3eec 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -387,6 +387,7 @@ void FLevelLocals::ClearLevelData(bool fullgc) levelMesh = nullptr; VisualThinkerHead = nullptr; ActorBehaviors.Clear(); + ClientSideActorBehaviors.Clear(); if (screen) screen->SetAABBTree(nullptr); } diff --git a/src/playsim/p_mobj.cpp b/src/playsim/p_mobj.cpp index 0b01f2cfe..e93190c8f 100644 --- a/src/playsim/p_mobj.cpp +++ b/src/playsim/p_mobj.cpp @@ -539,6 +539,8 @@ DBehavior* AActor::AddBehavior(PClass& type) return nullptr; b->Owner = this; + b->ObjectFlags |= (ObjectFlags & (OF_ClientSide | OF_Transient)); + Behaviors[type.TypeName] = b; Level->AddActorBehavior(*b); IFOVERRIDENVIRTUALPTRNAME(b, NAME_Behavior, Initialize) @@ -653,6 +655,9 @@ void AActor::MoveBehaviors(AActor& from) if (&from == this) return; + if (IsClientside() != from.IsClientside()) + I_Error("Cannot move Behaviors between client-side and world Actors"); + // Clean these up properly before transferring. ClearBehaviors(); diff --git a/src/scripting/vmiterators.cpp b/src/scripting/vmiterators.cpp index 730f09226..f8cac10d2 100644 --- a/src/scripting/vmiterators.cpp +++ b/src/scripting/vmiterators.cpp @@ -446,9 +446,10 @@ public: Reinit(); } - DBehaviorIterator(const FLevelLocals& level, PClass* type, PClass* ownerType) + DBehaviorIterator(const FLevelLocals& level, PClass* type, PClass* ownerType, bool clientSide) { - for (auto& b : level.ActorBehaviors) + auto& list = clientSide ? level.ClientSideActorBehaviors : level.ActorBehaviors; + for (auto& b : list) { if (ownerType != nullptr && !b->Owner->IsKindOf(ownerType)) continue; @@ -498,7 +499,7 @@ DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, CreateFrom, CreateBehaviorItFro static DBehaviorIterator* CreateBehaviorIt(PClass* type, PClass* ownerType) { - return Create(*primaryLevel, type, ownerType); + return Create(*primaryLevel, type, ownerType, false); } DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, Create, CreateBehaviorIt) @@ -509,6 +510,19 @@ DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, Create, CreateBehaviorIt) ACTION_RETURN_OBJECT(CreateBehaviorIt(type, ownerType)); } +static DBehaviorIterator* CreateClientSideBehaviorIt(PClass* type, PClass* ownerType) +{ + return Create(*primaryLevel, type, ownerType, true); +} + +DEFINE_ACTION_FUNCTION_NATIVE(DBehaviorIterator, CreateClientSide, CreateClientSideBehaviorIt) +{ + PARAM_PROLOGUE; + PARAM_CLASS(type, DBehavior); + PARAM_CLASS(ownerType, AActor); + ACTION_RETURN_OBJECT(CreateClientSideBehaviorIt(type, ownerType)); +} + static DBehavior* NextBehavior(DBehaviorIterator* self) { return self->Next(); diff --git a/wadsrc/static/zscript/actors/actor.zs b/wadsrc/static/zscript/actors/actor.zs index 15bd68236..123fe91be 100644 --- a/wadsrc/static/zscript/actors/actor.zs +++ b/wadsrc/static/zscript/actors/actor.zs @@ -88,6 +88,7 @@ class BehaviorIterator native abstract final version("4.15.1") { native static BehaviorIterator CreateFrom(Actor mobj, class type = null); native static BehaviorIterator Create(class type = null, class ownerType = null); + native static BehaviorIterator CreateClientSide(class type = null, class ownerType = null); native Behavior Next(); native void Reinit(); From 7e90f3a0f3a5808bd00433981bdc1df115490002 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 8 Jul 2025 14:57:20 -0400 Subject: [PATCH 273/384] Fixed teleporters breaking view interpolation --- src/playsim/p_user.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playsim/p_user.cpp b/src/playsim/p_user.cpp index 2b8e9cf8d..d0592e9e1 100644 --- a/src/playsim/p_user.cpp +++ b/src/playsim/p_user.cpp @@ -1551,7 +1551,6 @@ void P_PredictPlayer (player_t *player) // Make sure any portal paths have been cleared from the previous movement. R_ClearInterpolationPath(); r_NoInterpolate = false; - // Because we're always predicting, this will get set by teleporters and then can never unset itself in the renderer properly. player->mo->renderflags &= ~RF_NOINTERPOLATEVIEW; // Got snagged on something. Start correcting towards the player's final predicted position. We're @@ -1710,6 +1709,7 @@ void P_UnPredictPlayer () } act->UpdateRenderSectorList(); + act->renderflags &= ~RF_NOINTERPOLATEVIEW; actInvSel = InvSel; player->inventorytics = inventorytics; From ca98f33f42c4c15c3c88202a41e5ea71ecdb0779 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 8 Jul 2025 14:19:29 -0400 Subject: [PATCH 274/384] Added support for client-side VisualThinkers --- src/d_net.cpp | 2 +- src/playsim/p_effect.cpp | 11 ++++++----- src/playsim/p_visualthinker.h | 2 +- wadsrc/static/zscript/doombase.zs | 2 +- wadsrc/static/zscript/visualthinker.zs | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 084943e19..5565b1871 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -2714,7 +2714,7 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player) if (typeinfo && typeinfo->IsDescendantOf("VisualThinker")) { DVector3 spawnpos = source->Vec3Angle(source->radius * 4, source->Angles.Yaw, 8.); - auto vt = DVisualThinker::NewVisualThinker(source->Level, typeinfo); + auto vt = DVisualThinker::NewVisualThinker(source->Level, typeinfo, false); if (vt) { vt->PT.Pos = spawnpos; diff --git a/src/playsim/p_effect.cpp b/src/playsim/p_effect.cpp index 62db58742..5ad982159 100644 --- a/src/playsim/p_effect.cpp +++ b/src/playsim/p_effect.cpp @@ -1033,7 +1033,7 @@ DVisualThinker* DVisualThinker::GetNext() const return _next; } -DVisualThinker* DVisualThinker::NewVisualThinker(FLevelLocals* Level, PClass* type) +DVisualThinker* DVisualThinker::NewVisualThinker(FLevelLocals* Level, PClass* type, bool clientSide) { if (type == nullptr) { @@ -1050,7 +1050,7 @@ DVisualThinker* DVisualThinker::NewVisualThinker(FLevelLocals* Level, PClass* ty return nullptr; } - auto zs = static_cast(Level->CreateThinker(type, DVisualThinker::DEFAULT_STAT)); + auto zs = static_cast(clientSide ? Level->CreateClientsideThinker(type, DVisualThinker::DEFAULT_STAT) : Level->CreateThinker(type, DVisualThinker::DEFAULT_STAT)); zs->Construct(); IFOVERRIDENVIRTUALPTRNAME(zs, NAME_VisualThinker, BeginPlay) @@ -1065,9 +1065,9 @@ DVisualThinker* DVisualThinker::NewVisualThinker(FLevelLocals* Level, PClass* ty return zs; } -static DVisualThinker* SpawnVisualThinker(FLevelLocals* Level, PClass* type) +static DVisualThinker* SpawnVisualThinker(FLevelLocals* Level, PClass* type, bool clientSide) { - return DVisualThinker::NewVisualThinker(Level, type); + return DVisualThinker::NewVisualThinker(Level, type, clientSide); } void DVisualThinker::UpdateSector(subsector_t * newSubsector) @@ -1101,7 +1101,8 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, SpawnVisualThinker, SpawnVisualThink { PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); PARAM_CLASS_NOT_NULL(type, DVisualThinker); - DVisualThinker* zs = SpawnVisualThinker(self, type); + PARAM_BOOL(clientSide); + DVisualThinker* zs = SpawnVisualThinker(self, type, clientSide); ACTION_RETURN_OBJECT(zs); } diff --git a/src/playsim/p_visualthinker.h b/src/playsim/p_visualthinker.h index 69faada9f..d6317e92d 100644 --- a/src/playsim/p_visualthinker.h +++ b/src/playsim/p_visualthinker.h @@ -55,7 +55,7 @@ public: void OnDestroy() override; DVisualThinker* GetNext() const; - static DVisualThinker* NewVisualThinker(FLevelLocals* Level, PClass* type); + static DVisualThinker* NewVisualThinker(FLevelLocals* Level, PClass* type, bool clientSide); void SetTranslation(FName trname); int GetRenderStyle() const; bool isFrozen(); diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 79a96c30d..ca4361d7c 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -624,7 +624,7 @@ struct LevelLocals native native String GetEpisodeName(); native void SpawnParticle(FSpawnParticleParams p); - native VisualThinker SpawnVisualThinker(Class type); + native VisualThinker SpawnVisualThinker(Class type, bool clientSide = false); clearscope native static bool WorldPaused(); } diff --git a/wadsrc/static/zscript/visualthinker.zs b/wadsrc/static/zscript/visualthinker.zs index 34c4643f5..a926243b6 100644 --- a/wadsrc/static/zscript/visualthinker.zs +++ b/wadsrc/static/zscript/visualthinker.zs @@ -37,11 +37,11 @@ Class VisualThinker : Thinker native native protected void UpdateSpriteInfo(); // needs to be called every time the texture is updated if the thinker uses SPF_LOCAL_ANIM and is set to a non-ticking statnum (or if Tick is overriden and doesn't call Super.Tick()) static VisualThinker Spawn(Class type, TextureID tex, Vector3 pos, Vector3 vel = (0,0,0), double alpha = 1.0, int flags = 0, - double roll = 0.0, Vector2 scale = (1,1), Vector2 offset = (0,0), int style = STYLE_Normal, TranslationID trans = 0, int VisualThinkerFlags = 0) + double roll = 0.0, Vector2 scale = (1,1), Vector2 offset = (0,0), int style = STYLE_Normal, TranslationID trans = 0, int VisualThinkerFlags = 0, bool clientSide = false) { if (!Level) return null; - let p = level.SpawnVisualThinker(type); + let p = level.SpawnVisualThinker(type, clientSide); if (p) { p.Texture = tex; From 5bd8c7f3beb280126b3cbf797d8c2bb625c77d36 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Tue, 8 Jul 2025 13:16:06 -0400 Subject: [PATCH 275/384] Unified name capitalization: *menu -> *Menu --- src/common/engine/namedef.h | 20 ++--- src/common/menu/menu.cpp | 4 +- src/g_game.cpp | 2 +- src/intermission/intermission.cpp | 2 +- src/menu/doommenu.cpp | 84 +++++++++---------- src/namedef_custom.h | 10 +-- .../static/zscript/ui/menu/playerdisplay.zs | 4 +- 7 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/common/engine/namedef.h b/src/common/engine/namedef.h index 5e8c9a711..e2b31a1b6 100644 --- a/src/common/engine/namedef.h +++ b/src/common/engine/namedef.h @@ -249,19 +249,19 @@ xx(snd_resampler) xx(AlwaysRun) // menu names -xx(Mainmenu) -xx(Episodemenu) -xx(Skillmenu) +xx(MainMenu) +xx(EpisodeMenu) +xx(SkillMenu) xx(Startgame) xx(StartgameConfirm) xx(StartgameConfirmed) -xx(Loadgamemenu) -xx(Savegamemenu) -xx(Optionsmenu) -xx(OptionsmenuSimple) -xx(OptionsmenuFull) -xx(Quitmenu) -xx(Savemenu) +xx(LoadgameMenu) +xx(SavegameMenu) +xx(OptionsMenu) +xx(OptionsMenuSimple) +xx(OptionsMenuFull) +xx(QuitMenu) +xx(SaveMenu) xx(EndGameMenu) xx(HelpMenu) xx(SoundMenu) diff --git a/src/common/menu/menu.cpp b/src/common/menu/menu.cpp index 6fbe1dd1b..b97118b98 100644 --- a/src/common/menu/menu.cpp +++ b/src/common/menu/menu.cpp @@ -774,7 +774,7 @@ bool M_Responder (event_t *ev) if (ev->data1 == KEY_ESCAPE) { M_StartControlPanel(true); - M_SetMenu(NAME_Mainmenu, -1); + M_SetMenu(NAME_MainMenu, -1); return true; } return false; @@ -783,7 +783,7 @@ bool M_Responder (event_t *ev) ConsoleState != c_down && gamestate != GS_LEVEL && m_use_mouse) { M_StartControlPanel(true); - M_SetMenu(NAME_Mainmenu, -1); + M_SetMenu(NAME_MainMenu, -1); return true; } } diff --git a/src/g_game.cpp b/src/g_game.cpp index 558a592e1..963437552 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -1023,7 +1023,7 @@ bool G_Responder (event_t *ev) stricmp (cmd, "screenshot"))) { M_StartControlPanel(true); - M_SetMenu(NAME_Mainmenu, -1); + M_SetMenu(NAME_MainMenu, -1); return true; } else diff --git a/src/intermission/intermission.cpp b/src/intermission/intermission.cpp index 0ae8e41da..46f36a8a9 100644 --- a/src/intermission/intermission.cpp +++ b/src/intermission/intermission.cpp @@ -1006,7 +1006,7 @@ bool DIntermissionController::Responder (FInputEvent *ev) else if (!stricmp(cmd, "menu_main") || !stricmp(cmd, "pause")) { M_StartControlPanel(true); - M_SetMenu(NAME_Mainmenu, -1); + M_SetMenu(NAME_MainMenu, -1); return true; } } diff --git a/src/menu/doommenu.cpp b/src/menu/doommenu.cpp index e21f977db..48a03a039 100644 --- a/src/menu/doommenu.cpp +++ b/src/menu/doommenu.cpp @@ -152,17 +152,17 @@ bool M_SetSpecialMenu(FName& menu, int param) // some menus need some special treatment switch (menu.GetIndex()) { - case NAME_Mainmenu: + case NAME_MainMenu: if (gameinfo.gametype & GAME_DoomStrifeChex) // Raven's games always used text based menus { if (gameinfo.forcetextinmenus) // If text is forced, this overrides any check. { - menu = NAME_MainmenuTextOnly; + menu = NAME_MainMenuTextOnly; } else if (cl_gfxlocalization != 0 && !gameinfo.forcenogfxsubstitution) { // For these games we must check up-front if they get localized because in that case another template must be used. - DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_Mainmenu); + DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_MainMenu); if (desc != nullptr) { if ((*desc)->IsKindOf(RUNTIME_CLASS(DListMenuDescriptor))) @@ -175,7 +175,7 @@ bool M_SetSpecialMenu(FName& menu, int param) FTextureID texid = TexMan.CheckForTexture("M_NGAME", ETextureType::MiscPatch); if (!OkForLocalization(texid, "$MNU_NEWGAME")) { - menu = NAME_MainmenuTextOnly; + menu = NAME_MainMenuTextOnly; } } } @@ -183,7 +183,7 @@ bool M_SetSpecialMenu(FName& menu, int param) } } break; - case NAME_Episodemenu: + case NAME_EpisodeMenu: // sent from the player class menu NewGameStartupInfo.Skill = -1; NewGameStartupInfo.Episode = -1; @@ -201,7 +201,7 @@ bool M_SetSpecialMenu(FName& menu, int param) M_StartupEpisodeMenu(&NewGameStartupInfo); // needs player class name from class menu (later) break; - case NAME_Skillmenu: + case NAME_SkillMenu: // sent from the episode menu if ((gameinfo.flags & GI_SHAREWARE) && param > 0) @@ -244,7 +244,7 @@ bool M_SetSpecialMenu(FName& menu, int param) M_ClearMenus (); return false; - case NAME_Savegamemenu: + case NAME_SavegameMenu: if (!usergame || (players[consoleplayer].health <= 0 && !multiplayer) || gamestate != GS_LEVEL) { // cannot save outside the game. @@ -253,7 +253,7 @@ bool M_SetSpecialMenu(FName& menu, int param) } break; - case NAME_Quitmenu: + case NAME_QuitMenu: // The separate menu class no longer exists but the name still needs support for existing mods. C_DoCommand("menu_quit"); return false; @@ -264,19 +264,19 @@ bool M_SetSpecialMenu(FName& menu, int param) ActivateEndGameMenu(); return false; - case NAME_Playermenu: + case NAME_PlayerMenu: menu = NAME_NewPlayerMenu; // redirect the old player menu to the new one. break; - case NAME_Optionsmenu: - if (m_simpleoptions) menu = NAME_OptionsmenuSimple; + case NAME_OptionsMenu: + if (m_simpleoptions) menu = NAME_OptionsMenuSimple; break; - case NAME_OptionsmenuFull: - menu = NAME_Optionsmenu; + case NAME_OptionsMenuFull: + menu = NAME_OptionsMenu; break; - case NAME_Readthismenu: + case NAME_ReadthisMenu: // [MK] allow us to override the ReadThisMenu class menu = gameinfo.HelpMenuClass; break; @@ -487,7 +487,7 @@ CCMD (quicksave) { S_Sound(CHAN_VOICE, CHANF_UI, "menu/activate", snd_menuvolume, ATTN_NONE); M_StartControlPanel(false); - M_SetMenu(NAME_Savegamemenu); + M_SetMenu(NAME_SavegameMenu); return; } @@ -533,7 +533,7 @@ CCMD (quickload) M_StartControlPanel(true); // signal that whatever gets loaded should be the new quicksave savegameManager.quickSaveSlot = (FSaveGameNode *)1; - M_SetMenu(NAME_Loadgamemenu); + M_SetMenu(NAME_LoadgameMenu); return; } @@ -638,7 +638,7 @@ void M_StartupEpisodeMenu(FNewGameStartup *gs) // Build episode menu bool success = false; bool isOld = false; - DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_Episodemenu); + DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_EpisodeMenu); if (desc != nullptr) { if ((*desc)->IsKindOf(RUNTIME_CLASS(DListMenuDescriptor))) @@ -649,7 +649,7 @@ void M_StartupEpisodeMenu(FNewGameStartup *gs) for(unsigned i=0; imItems.Size(); i++) { FName n = ld->mItems[i]->mAction; - if (n == NAME_Skillmenu) + if (n == NAME_SkillMenu) { isOld = true; ld->mItems.Resize(i); @@ -724,12 +724,12 @@ void M_StartupEpisodeMenu(FNewGameStartup *gs) { FTextureID tex = GetMenuTexture(AllEpisodes[i].mPicName.GetChars()); if (AllEpisodes[i].mEpisodeName.IsEmpty() || OkForLocalization(tex, AllEpisodes[i].mEpisodeName.GetChars())) - it = CreateListMenuItemPatch(posx, posy, spacing, AllEpisodes[i].mShortcut, tex, NAME_Skillmenu, i); + it = CreateListMenuItemPatch(posx, posy, spacing, AllEpisodes[i].mShortcut, tex, NAME_SkillMenu, i); } if (it == nullptr) { it = CreateListMenuItemText(posx, posy, spacing, AllEpisodes[i].mShortcut, - AllEpisodes[i].mEpisodeName.GetChars(), ld->mFont, ld->mFontColor, ld->mFontColor2, NAME_Skillmenu, i); + AllEpisodes[i].mEpisodeName.GetChars(), ld->mFont, ld->mFontColor, ld->mFontColor2, NAME_SkillMenu, i); } ld->mItems.Push(it); posy += spacing; @@ -752,8 +752,8 @@ void M_StartupEpisodeMenu(FNewGameStartup *gs) // Couldn't create the episode menu, either because there's too many episodes or some error occured // Create an option menu for episode selection instead. DOptionMenuDescriptor *od = Create(); - MenuDescriptors[NAME_Episodemenu] = od; - od->mMenuName = NAME_Episodemenu; + MenuDescriptors[NAME_EpisodeMenu] = od; + od->mMenuName = NAME_EpisodeMenu; od->mFont = gameinfo.gametype == GAME_Doom ? BigUpper : BigFont; od->mTitle = "$MNU_EPISODE"; od->mSelectedItem = 0; @@ -769,7 +769,7 @@ void M_StartupEpisodeMenu(FNewGameStartup *gs) GC::WriteBarrier(od); for(unsigned i = 0; i < AllEpisodes.Size(); i++) { - auto it = CreateOptionMenuItemSubmenu(AllEpisodes[i].mEpisodeName.GetChars(), "Skillmenu", i); + auto it = CreateOptionMenuItemSubmenu(AllEpisodes[i].mEpisodeName.GetChars(), "SkillMenu", i); od->mItems.Push(it); GC::WriteBarrier(od, it); } @@ -787,7 +787,7 @@ static void BuildPlayerclassMenu() bool success = false; // Build player class menu - DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_Playerclassmenu); + DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_PlayerclassMenu); if (desc != nullptr) { if ((*desc)->IsKindOf(RUNTIME_CLASS(DListMenuDescriptor))) @@ -828,7 +828,7 @@ static void BuildPlayerclassMenu() { // create a dummy item that auto-chooses the default class. auto it = CreateListMenuItemText(0, 0, 0, 'p', "player", - ld->mFont,ld->mFontColor, ld->mFontColor2, NAME_Episodemenu, -1000); + ld->mFont,ld->mFontColor, ld->mFontColor2, NAME_EpisodeMenu, -1000); ld->mAutoselect = ld->mItems.Push(it); success = true; } @@ -854,7 +854,7 @@ static void BuildPlayerclassMenu() if (pname != nullptr) { auto it = CreateListMenuItemText(ld->mXpos, ld->mYpos, ld->mLinespacing, *pname, - pname, ld->mFont,ld->mFontColor,ld->mFontColor2, NAME_Episodemenu, i); + pname, ld->mFont,ld->mFontColor,ld->mFontColor2, NAME_EpisodeMenu, i); ld->mItems.Push(it); ld->mYpos += ld->mLinespacing; n++; @@ -864,7 +864,7 @@ static void BuildPlayerclassMenu() if (n > 1 && !gameinfo.norandomplayerclass) { auto it = CreateListMenuItemText(ld->mXpos, ld->mYpos, ld->mLinespacing, 'r', - "$MNU_RANDOM", ld->mFont,ld->mFontColor,ld->mFontColor2, NAME_Episodemenu, -1); + "$MNU_RANDOM", ld->mFont,ld->mFontColor,ld->mFontColor2, NAME_EpisodeMenu, -1); ld->mItems.Push(it); } if (n == 0) @@ -873,7 +873,7 @@ static void BuildPlayerclassMenu() if (pname != nullptr) { auto it = CreateListMenuItemText(ld->mXpos, ld->mYpos, ld->mLinespacing, *pname, - pname, ld->mFont,ld->mFontColor,ld->mFontColor2, NAME_Episodemenu, 0); + pname, ld->mFont,ld->mFontColor,ld->mFontColor2, NAME_EpisodeMenu, 0); ld->mItems.Push(it); } } @@ -890,8 +890,8 @@ static void BuildPlayerclassMenu() // Couldn't create the playerclass menu, either because there's too many episodes or some error occured // Create an option menu for class selection instead. DOptionMenuDescriptor *od = Create(); - MenuDescriptors[NAME_Playerclassmenu] = od; - od->mMenuName = NAME_Playerclassmenu; + MenuDescriptors[NAME_PlayerclassMenu] = od; + od->mMenuName = NAME_PlayerclassMenu; od->mFont = gameinfo.gametype == GAME_Doom ? BigUpper : BigFont; od->mTitle = "$MNU_CHOOSECLASS"; od->mSelectedItem = 0; @@ -913,13 +913,13 @@ static void BuildPlayerclassMenu() const char *pname = GetPrintableDisplayName(PlayerClasses[i].Type).GetChars(); if (pname != nullptr) { - auto it = CreateOptionMenuItemSubmenu(pname, "Episodemenu", i); + auto it = CreateOptionMenuItemSubmenu(pname, "EpisodeMenu", i); od->mItems.Push(it); GC::WriteBarrier(od, it); } } } - auto it = CreateOptionMenuItemSubmenu("Random", "Episodemenu", -1); + auto it = CreateOptionMenuItemSubmenu("Random", "EpisodeMenu", -1); od->mItems.Push(it); GC::WriteBarrier(od, it); } @@ -1170,7 +1170,7 @@ void M_StartupSkillMenu(FNewGameStartup *gs) } } - DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_Skillmenu); + DMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_SkillMenu); if (desc != nullptr) { if ((*desc)->IsKindOf(RUNTIME_CLASS(DListMenuDescriptor))) @@ -1316,8 +1316,8 @@ fail: if (desc == nullptr) { od = Create(); - MenuDescriptors[NAME_Skillmenu] = od; - od->mMenuName = NAME_Skillmenu; + MenuDescriptors[NAME_SkillMenu] = od; + od->mMenuName = NAME_SkillMenu; od->mFont = gameinfo.gametype == GAME_Doom ? BigUpper : BigFont; od->mTitle = "$MNU_CHOOSESKILL"; od->mSelectedItem = defindex; @@ -1425,43 +1425,43 @@ CCMD (menu_main) { if (gamestate == GS_FULLCONSOLE) gamestate = GS_MENUSCREEN; M_StartControlPanel(true); - M_SetMenu(NAME_Mainmenu, -1); + M_SetMenu(NAME_MainMenu, -1); } CCMD (menu_load) { // F3 M_StartControlPanel (true); - M_SetMenu(NAME_Loadgamemenu, -1); + M_SetMenu(NAME_LoadgameMenu, -1); } CCMD (menu_save) { // F2 M_StartControlPanel (true); - M_SetMenu(NAME_Savegamemenu, -1); + M_SetMenu(NAME_SavegameMenu, -1); } CCMD (menu_help) { // F1 M_StartControlPanel (true); - M_SetMenu(NAME_Readthismenu, -1); + M_SetMenu(NAME_ReadthisMenu, -1); } CCMD (menu_game) { M_StartControlPanel (true); - M_SetMenu(NAME_Playerclassmenu, -1); // The playerclass menu is the first in the 'start game' chain + M_SetMenu(NAME_PlayerclassMenu, -1); // The playerclass menu is the first in the 'start game' chain } CCMD (menu_options) { M_StartControlPanel (true); - M_SetMenu(NAME_Optionsmenu, -1); + M_SetMenu(NAME_OptionsMenu, -1); } CCMD (menu_player) { M_StartControlPanel (true); - M_SetMenu(NAME_Playermenu, -1); + M_SetMenu(NAME_PlayerMenu, -1); } CCMD (menu_messages) diff --git a/src/namedef_custom.h b/src/namedef_custom.h index 212fdd6f5..a8ae6414c 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -446,11 +446,11 @@ xx(Inter_Strife_MAP10) xx(BuiltinCallLineSpecial) -xx(MainmenuTextOnly) -xx(Playerclassmenu) -xx(HexenDefaultPlayerclassmenu) -xx(Readthismenu) -xx(Playermenu) +xx(MainMenuTextOnly) +xx(PlayerclassMenu) +xx(HexenDefaultPlayerclassMenu) +xx(ReadthisMenu) +xx(PlayerMenu) // more stuff xx(Behavior) diff --git a/wadsrc/static/zscript/ui/menu/playerdisplay.zs b/wadsrc/static/zscript/ui/menu/playerdisplay.zs index acb2f3b05..38ba84d3e 100644 --- a/wadsrc/static/zscript/ui/menu/playerdisplay.zs +++ b/wadsrc/static/zscript/ui/menu/playerdisplay.zs @@ -161,7 +161,7 @@ class ListMenuItemPlayerDisplay : ListMenuItem [seltype, classnum] = mOwner.mItems[mOwner.mSelectedItem].GetAction(); - if (seltype != 'Episodemenu') return false; + if (seltype != 'EpisodeMenu') return false; if (PlayerClasses.Size() == 0) return false; SetPlayerClass(classnum); @@ -371,4 +371,4 @@ class PlayerMenuPlayerDisplay : ListMenuItemPlayerDisplay } } } -} \ No newline at end of file +} From 42a3ca3d560084a60b2f18828b25d32b87a6913a Mon Sep 17 00:00:00 2001 From: Boondorl Date: Wed, 9 Jul 2025 20:34:29 -0400 Subject: [PATCH 276/384] Move OnLoad to after everything is done serializing Makes behavior much more consistent and safer. --- src/g_level.cpp | 4 ++++ src/playsim/dthinker.cpp | 52 ++++++++++++++++++++++++++++++---------- src/playsim/dthinker.h | 3 ++- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/g_level.cpp b/src/g_level.cpp index f5921948e..9ea40c929 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1538,6 +1538,10 @@ void FLevelLocals::DoLoadLevel(const FString &nextmapname, int position, bool au { I_Error("no start for player %d found.", pnumerr); } + + // If loading in from existing data, allow things to reinitialize if needed. + if (FromSnapshot || savegamerestore) + Thinkers.OnLoad(); } diff --git a/src/playsim/dthinker.cpp b/src/playsim/dthinker.cpp index 2d37ffcfd..a5cffd67a 100644 --- a/src/playsim/dthinker.cpp +++ b/src/playsim/dthinker.cpp @@ -362,6 +362,20 @@ void FThinkerCollection::DestroyAllThinkers(bool fullgc) // //========================================================================== +void FThinkerCollection::OnLoad() +{ + for (auto& list : FreshThinkers) + list.OnLoad(); + for (auto& list : Thinkers) + list.OnLoad(); +} + +//========================================================================== +// +// +// +//========================================================================== + void FThinkerCollection::SerializeThinkers(FSerializer &arc, bool hubLoad) { //DThinker *thinker; @@ -415,12 +429,12 @@ void FThinkerCollection::SerializeThinkers(FSerializer &arc, bool hubLoad) else if (thinker->ObjectFlags & OF_JustSpawned) { FreshThinkers[i].AddTail(thinker); - thinker->CallPostSerialize(); + thinker->PostSerialize(); } else { Thinkers[i].AddTail(thinker); - thinker->CallPostSerialize(); + thinker->PostSerialize(); } } } @@ -645,6 +659,30 @@ void FThinkerList::SaveList(FSerializer &arc) // //========================================================================== +void FThinkerList::OnLoad() +{ + DThinker* node = GetHead(); + if (node == nullptr) + return; + + while (node != Sentinel) + { + NextToThink = node->NextThinker; + if (!(node->ObjectFlags & OF_EuthanizeMe)) + { + IFOVERRIDENVIRTUALPTRNAME(node, NAME_Thinker, OnLoad) + VMCallVoid(func, node); + } + node = NextToThink; + } +} + +//========================================================================== +// +// +// +//========================================================================== + int FThinkerList::TickThinkers(FThinkerList *dest) { int count = 0; @@ -833,16 +871,6 @@ void DThinker::PostSerialize() { } -void DThinker::CallPostSerialize() -{ - PostSerialize(); - IFOVERRIDENVIRTUALPTRNAME(this, NAME_Thinker, OnLoad) - { - VMValue params[] = { this }; - VMCall(func, params, 1, nullptr, 0); - } -} - //========================================================================== // // diff --git a/src/playsim/dthinker.h b/src/playsim/dthinker.h index 95c2bb4bc..8a446e461 100644 --- a/src/playsim/dthinker.h +++ b/src/playsim/dthinker.h @@ -60,6 +60,7 @@ struct FThinkerList bool IsEmpty() const; void DestroyThinkers(); bool DoDestroyThinkers(); + void OnLoad(); int TickThinkers(FThinkerList *dest); // Returns: # of thinkers ticked int ProfileThinkers(FThinkerList *dest); void SaveList(FSerializer &arc); @@ -83,6 +84,7 @@ struct FThinkerCollection void DestroyAllThinkers(bool fullgc = true); void SerializeThinkers(FSerializer &arc, bool keepPlayers); void MarkRoots(); + void OnLoad(); DThinker *FirstThinker(int statnum); void Link(DThinker *thinker, int statnum); @@ -105,7 +107,6 @@ public: virtual void PostBeginPlay (); // Called just before the first tick virtual void CallPostBeginPlay(); // different in actor. virtual void PostSerialize(); - void CallPostSerialize(); void Serialize(FSerializer &arc) override; size_t PropagateMark(); From 13a8b0e5bab477ab0a07a1aee5b6e38b4df04cb9 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Fri, 27 Jun 2025 19:27:50 -0700 Subject: [PATCH 277/384] Fixed incorrect buffer grow calls Fixes minor mistake introduced in 94be307225. --- src/d_net.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d_net.cpp b/src/d_net.cpp index 5565b1871..5a1f64c1c 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -248,7 +248,7 @@ private: DPrintf(DMSG_NOTIFY, "Expanding special size to %zu\n", MaxSize); for (auto& stream : Streams) - Streams->Grow(MaxSize); + stream.Grow(MaxSize); CurrentStream = Streams[CurrentClientTic % BACKUPTICS].Stream + CurrentSize; } From ab7b1642bcd37fd56cff24b51a7da7a7d774ccbb Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Fri, 27 Jun 2025 19:27:22 -0700 Subject: [PATCH 278/384] Prevent buffer overflows when using streams --- src/common/console/c_cvars.cpp | 13 +- src/common/console/c_cvars.h | 10 +- src/common/engine/i_net.cpp | 29 +-- src/common/utility/zstring.cpp | 5 + src/common/utility/zstring.h | 2 + src/d_net.cpp | 190 +++++++-------- src/d_net.h | 5 +- src/d_netinf.h | 4 +- src/d_netinfo.cpp | 18 +- src/d_protocol.cpp | 413 +++++++++++++++++++++++++-------- src/d_protocol.h | 68 ++++-- src/g_game.cpp | 203 ++++++++-------- src/gamedata/a_weapons.cpp | 6 +- src/gamedata/a_weapons.h | 6 +- src/p_conversation.cpp | 2 +- src/p_conversation.h | 2 +- src/playsim/bots/b_bot.h | 4 +- src/playsim/bots/b_game.cpp | 8 +- 18 files changed, 600 insertions(+), 388 deletions(-) diff --git a/src/common/console/c_cvars.cpp b/src/common/console/c_cvars.cpp index fc8c715b3..eb76f136a 100644 --- a/src/common/console/c_cvars.cpp +++ b/src/common/console/c_cvars.cpp @@ -40,6 +40,7 @@ #include "c_console.h" #include "c_dispatch.h" #include "c_cvars.h" +#include "d_protocol.h" #include "engineerrors.h" #include "printf.h" #include "palutil.h" @@ -1265,12 +1266,10 @@ void FilterCompactCVars (TArray &cvars, uint32_t filter) } } -void C_WriteCVars (uint8_t **demo_p, uint32_t filter, bool compact) +void C_WriteCVars (TArrayView& demo_p, uint32_t filter, bool compact) { FString dump = C_GetMassCVarString(filter, compact); - size_t dumplen = dump.Len() + 1; // include terminating \0 - memcpy(*demo_p, dump.GetChars(), dumplen); - *demo_p += dumplen; + WriteFString(dump, demo_p); } FString C_GetMassCVarString (uint32_t filter, bool compact) @@ -1306,9 +1305,9 @@ FString C_GetMassCVarString (uint32_t filter, bool compact) return dump; } -void C_ReadCVars (uint8_t **demo_p) +void C_ReadCVars (TArrayView& demo_p) { - char *ptr = *((char **)demo_p); + char *ptr = (char *)demo_p.Data(); char *breakpt; if (*ptr++ != '\\') @@ -1371,7 +1370,7 @@ void C_ReadCVars (uint8_t **demo_p) } } } - *demo_p += strlen (*((char **)demo_p)) + 1; + AdvanceStream(demo_p, strlen((char*)demo_p.Data()) + 1); } struct FCVarBackup diff --git a/src/common/console/c_cvars.h b/src/common/console/c_cvars.h index 27cf8aa38..48678bbdf 100644 --- a/src/common/console/c_cvars.h +++ b/src/common/console/c_cvars.h @@ -269,7 +269,7 @@ private: // These need to go away! friend FString C_GetMassCVarString (uint32_t filter, bool compact); friend void C_SerializeCVars(FSerializer& arc, const char* label, uint32_t filter); - friend void C_ReadCVars (uint8_t **demo_p); + friend void C_ReadCVars (TArrayView& demo_p); friend void C_BackupCVars (void); friend FBaseCVar *FindCVar (const char *var_name, FBaseCVar **prev); friend FBaseCVar *FindCVarSub (const char *var_name, int namelen); @@ -288,12 +288,12 @@ private: // the cvar names are omitted to save space. FString C_GetMassCVarString (uint32_t filter, bool compact=false); -// Writes all cvars that could effect demo sync to *demo_p. These are +// Writes all cvars that could effect demo sync to demo_p. These are // cvars that have either CVAR_SERVERINFO or CVAR_DEMOSAVE set. -void C_WriteCVars (uint8_t **demo_p, uint32_t filter, bool compact=false); +void C_WriteCVars (TArrayView& demo_p, uint32_t filter, bool compact=false); -// Read all cvars from *demo_p and set them appropriately. -void C_ReadCVars (uint8_t **demo_p); +// Read all cvars from demo_p and set them appropriately. +void C_ReadCVars (TArrayView& demo_p); void C_InstallHandlers(ConsoleCallbacks* cb); diff --git a/src/common/engine/i_net.cpp b/src/common/engine/i_net.cpp index f665271ad..5ddd0f69f 100644 --- a/src/common/engine/i_net.cpp +++ b/src/common/engine/i_net.cpp @@ -191,10 +191,10 @@ size_t Net_SetEngineInfo(uint8_t*& stream); bool Net_VerifyEngine(uint8_t*& stream); void Net_SetupUserInfo(); const char* Net_GetClientName(int client, unsigned int charLimit); -size_t Net_SetUserInfo(int client, uint8_t*& stream); -size_t Net_ReadUserInfo(int client, uint8_t*& stream); -size_t Net_ReadGameInfo(uint8_t*& stream); -size_t Net_SetGameInfo(uint8_t*& stream); +void Net_SetUserInfo(int client, TArrayView& stream); +void Net_ReadUserInfo(int client, TArrayView& stream); +void Net_ReadGameInfo(TArrayView& stream); +void Net_SetGameInfo(TArrayView& stream); static SOCKET CreateUDPSocket() { @@ -833,7 +833,7 @@ static bool Host_CheckForConnections(void* connected) { if (Connected[RemoteClient].Status == CSTAT_CONNECTING) { - uint8_t* stream = &NetBuffer[2]; + TArrayView stream = TArrayView(&NetBuffer[2], MAX_MSGLEN-2); Net_ReadUserInfo(RemoteClient, stream); Connected[RemoteClient].Status = CSTAT_WAITING; I_NetClientConnected(RemoteClient, 16u); @@ -886,8 +886,9 @@ static bool Host_CheckForConnections(void* connected) memcpy(&NetBuffer[3], GameID, 8); NetBufferLength = 11u; - uint8_t* stream = &NetBuffer[NetBufferLength]; - NetBufferLength += Net_SetGameInfo(stream); + TArrayView stream = TArrayView(&NetBuffer[NetBufferLength], MAX_MSGLEN - NetBufferLength); + Net_SetGameInfo(stream); + NetBufferLength += stream.Data() - &NetBuffer[NetBufferLength]; SendPacket(con.Address); clientReady = false; } @@ -911,8 +912,9 @@ static bool Host_CheckForConnections(void* connected) NetBufferLength += addrSize; } - uint8_t* stream = &NetBuffer[NetBufferLength]; - NetBufferLength += Net_SetUserInfo(i, stream); + TArrayView stream = TArrayView(&NetBuffer[NetBufferLength], MAX_MSGLEN - NetBufferLength); + Net_SetUserInfo(i, stream); + NetBufferLength += stream.Data() - &NetBuffer[NetBufferLength]; SendPacket(con.Address); } clientReady = false; @@ -1131,7 +1133,7 @@ static bool Guest_ContactHost(void* unused) { TicDup = clamp(NetBuffer[2], 1, MAXTICDUP); memcpy(GameID, &NetBuffer[3], 8); - uint8_t* stream = &NetBuffer[11]; + TArrayView stream = TArrayView(&NetBuffer[11], MAX_MSGLEN - 11); Net_ReadGameInfo(stream); Connected[consoleplayer].bHasGameInfo = true; } @@ -1158,7 +1160,7 @@ static bool Guest_ContactHost(void* unused) { Connected[c].Status = CSTAT_READY; } - uint8_t* stream = &NetBuffer[byte]; + TArrayView stream = TArrayView(&NetBuffer[byte], MAX_MSGLEN - byte); Net_ReadUserInfo(c, stream); SetClientAck(consoleplayer, c, true); @@ -1199,8 +1201,9 @@ static bool Guest_ContactHost(void* unused) NetBuffer[1] = PRE_USER_INFO; NetBufferLength = 2u; - uint8_t* stream = &NetBuffer[NetBufferLength]; - NetBufferLength += Net_SetUserInfo(consoleplayer, stream); + TArrayView stream = TArrayView(&NetBuffer[NetBufferLength], MAX_MSGLEN - NetBufferLength); + Net_SetUserInfo(consoleplayer, stream); + NetBufferLength += stream.Data() - &NetBuffer[NetBufferLength]; SendPacket(Connected[0].Address); } else if (con.Status == CSTAT_WAITING) diff --git a/src/common/utility/zstring.cpp b/src/common/utility/zstring.cpp index 0a5405e43..47bfd15fe 100644 --- a/src/common/utility/zstring.cpp +++ b/src/common/utility/zstring.cpp @@ -265,6 +265,11 @@ FString &FString::operator = (const char *copyStr) return *this; } +TArrayView FString::GetTArrayView() +{ + return TArrayView((uint8_t*)Chars, Len() + 1); +} + void FString::Format (const char *fmt, ...) { va_list arglist; diff --git a/src/common/utility/zstring.h b/src/common/utility/zstring.h index 2bc27d7b3..e51950a47 100644 --- a/src/common/utility/zstring.h +++ b/src/common/utility/zstring.h @@ -166,6 +166,8 @@ public: const char *GetChars() const { return Chars; } + TArrayView GetTArrayView(); + const char &operator[] (int index) const { return Chars[index]; } #if defined(_WIN32) && !defined(_WIN64) && defined(_MSC_VER) // Compiling 32-bit Windows source with MSVC: size_t is typedefed to an diff --git a/src/d_net.cpp b/src/d_net.cpp index 5a1f64c1c..9e89a5966 100644 --- a/src/d_net.cpp +++ b/src/d_net.cpp @@ -158,7 +158,7 @@ void D_ProcessEvents(void); void G_BuildTiccmd(usercmd_t *cmd); void D_DoAdvanceDemo(void); -static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always); +static void RunScript(TArrayView& stream, AActor *pawn, int snum, int argn, int always); extern bool advancedemo; @@ -297,7 +297,7 @@ public: if (CurrentStream != nullptr) { AddBytes(1); - WriteInt8(it, &CurrentStream); + UncheckedWriteInt8(it, &CurrentStream); } return *this; } @@ -307,7 +307,7 @@ public: if (CurrentStream != nullptr) { AddBytes(2); - WriteInt16(it, &CurrentStream); + UncheckedWriteInt16(it, &CurrentStream); } return *this; } @@ -317,7 +317,7 @@ public: if (CurrentStream != nullptr) { AddBytes(4); - WriteInt32(it, &CurrentStream); + UncheckedWriteInt32(it, &CurrentStream); } return *this; } @@ -327,7 +327,7 @@ public: if (CurrentStream != nullptr) { AddBytes(8); - WriteInt64(it, &CurrentStream); + UncheckedWriteInt64(it, &CurrentStream); } return *this; } @@ -337,7 +337,7 @@ public: if (CurrentStream != nullptr) { AddBytes(4); - WriteFloat(it, &CurrentStream); + UncheckedWriteFloat(it, &CurrentStream); } return *this; } @@ -347,7 +347,7 @@ public: if (CurrentStream != nullptr) { AddBytes(8); - WriteDouble(it, &CurrentStream); + UncheckedWriteDouble(it, &CurrentStream); } return *this; } @@ -357,7 +357,7 @@ public: if (CurrentStream != nullptr) { AddBytes(strlen(it) + 1); - WriteString(it, &CurrentStream); + UncheckedWriteString(it, &CurrentStream); } return *this; } @@ -603,24 +603,24 @@ static size_t GetNetBufferSize() if (NetBufferLength < totalBytes + playerCount * padding) return totalBytes + playerCount * padding; - uint8_t* skipper = &NetBuffer[totalBytes]; + TArrayView skipper = TArrayView(&NetBuffer[totalBytes], MAX_MSGLEN - totalBytes); for (int p = 0; p < playerCount; ++p) { - ++skipper; + AdvanceStream(skipper, 1); if (NetMode == NET_PacketServer && RemoteClient == Net_Arbitrator) - skipper += 2; + AdvanceStream(skipper, 2); for (int i = 0; i < ranTics; ++i) - skipper += 3; + AdvanceStream(skipper, 3); for (int i = 0; i < numTics; ++i) { - ++skipper; + AdvanceStream(skipper, 1); SkipUserCmdMessage(skipper); } } - return int(skipper - NetBuffer); + return int(skipper.Data() - NetBuffer); } // @@ -992,14 +992,19 @@ static void GetPackets() // Each tic within a given packet is given a sequence number to ensure that things were put // back together correctly. Normally this wouldn't matter as much but since we need to keep // clients in lock step a misordered packet will instantly cause a desync. - TArray data = {}; + TArray> data = {}; // each contained TArrayView represents a packet. for (int t = 0; t < totalTics; ++t) { // Try and reorder the tics if they're all there but end up out of order. const int ofs = NetBuffer[curByte++]; - data.Insert(ofs, &NetBuffer[curByte]); - uint8_t* skipper = &NetBuffer[curByte]; - curByte += SkipUserCmdMessage(skipper); + + TArrayView skipper = TArrayView(&NetBuffer[curByte], MAX_MSGLEN - curByte); + SkipUserCmdMessage(skipper); + + TArrayView packet = TArrayView(&NetBuffer[curByte], skipper.Data() - &NetBuffer[curByte]); + data.Insert(ofs, packet); + + curByte += skipper.Data() - &NetBuffer[curByte]; } // If it's from a previous waiting period, the commands are no longer relevant. @@ -1722,38 +1727,29 @@ void NetUpdate(int tics) // Client commands. - uint8_t* cmd = &NetBuffer[size]; + TArrayView cmd = TArrayView(&NetBuffer[size], MAX_MSGLEN - size); for (int i = 0; i < playerCount; ++i) { - cmd[0] = playerNums[i]; - ++cmd; + WriteInt8(playerNums[i], cmd); auto& clientState = ClientStates[playerNums[i]]; // Time used to track latency since in packet server mode we want each // client's latency to the server itself. if (NetMode == NET_PacketServer && consoleplayer == Net_Arbitrator) { - cmd[0] = (clientState.AverageLatency >> 8); - ++cmd; - cmd[0] = clientState.AverageLatency; - ++cmd; + WriteInt16(clientState.AverageLatency, cmd); } for (int r = 0; r < sendCon; ++r) { - cmd[0] = r; - ++cmd; + WriteInt8(r, cmd); const int tic = (baseConsistency + curTicOfs + r) % BACKUPTICS; - cmd[0] = (clientState.LocalConsistency[tic] >> 8); - ++cmd; - cmd[0] = clientState.LocalConsistency[tic]; - ++cmd; + WriteInt16(clientState.LocalConsistency[tic], cmd); } for (int t = 0; t < sendTics; ++t) { - cmd[0] = t; - ++cmd; + WriteInt8(t, cmd); int curTic = sequenceNum + curTicOfs + t, lastTic = curTic - 1; if (playerNums[i] == consoleplayer) @@ -1763,11 +1759,7 @@ void NetUpdate(int tics) // Write out the net events before the user commands so inputs can // be used as a marker for when the given command ends. auto& stream = NetEvents.Streams[curTic % BACKUPTICS]; - if (stream.Used) - { - memcpy(cmd, stream.Stream, stream.Used); - cmd += stream.Used; - } + WriteBytes(TArrayView(stream.Stream, stream.Used), cmd); WriteUserCmdMessage(LocalCmds[realTic], realLastTic >= 0 ? &LocalCmds[realLastTic] : nullptr, cmd); @@ -1776,13 +1768,8 @@ void NetUpdate(int tics) { auto& netTic = clientState.Tics[curTic % BACKUPTICS]; - int len; - uint8_t* data = netTic.Data.GetData(&len); - if (data != nullptr) - { - memcpy(cmd, data, len); - cmd += len; - } + auto data = netTic.Data.GetTArrayView(); + WriteBytes(data, cmd); WriteUserCmdMessage(netTic.Command, lastTic >= 0 ? &clientState.Tics[lastTic % BACKUPTICS].Command : nullptr, cmd); @@ -1790,9 +1777,9 @@ void NetUpdate(int tics) } } - HSendPacket(client, int(cmd - NetBuffer)); + HSendPacket(client, int(cmd.Data() - NetBuffer)); if (net_extratic && !isSelf) - HSendPacket(client, int(cmd - NetBuffer)); + HSendPacket(client, int(cmd.Data() - NetBuffer)); } } } @@ -1834,70 +1821,52 @@ const char* Net_GetClientName(int client, unsigned int charLimit = 0u) return players[client].userinfo.GetName(charLimit); } -size_t Net_SetUserInfo(int client, uint8_t*& stream) +void Net_SetUserInfo(int client, TArrayView& stream) { auto str = D_GetUserInfoStrings(client, true); - const size_t userSize = str.Len() + 1; - memcpy(stream, str.GetChars(), userSize); - return userSize; + WriteFString(str, stream); } -size_t Net_ReadUserInfo(int client, uint8_t*& stream) +void Net_ReadUserInfo(int client, TArrayView& stream) { - const uint8_t* start = stream; - D_ReadUserInfoStrings(client, &stream, false); - return stream - start; + D_ReadUserInfoStrings(client, stream, false); } -size_t Net_SetGameInfo(uint8_t*& stream) +void Net_SetGameInfo(TArrayView& stream) { - const uint8_t* start = stream; - WriteString(startmap.GetChars(), &stream); - WriteInt32(rngseed, &stream); - C_WriteCVars(&stream, CVAR_SERVERINFO, true); + WriteFString(startmap, stream); + WriteInt32(rngseed, stream); + C_WriteCVars(stream, CVAR_SERVERINFO, true); auto load = Args->CheckValue("-loadgame"); if (load != nullptr) { - stream[0] = true; - const size_t len = strlen(load) + 1; - memcpy(&stream[1], load, len); - stream += len; + WriteInt8(1, stream); + WriteString(load, stream); } else { - stream[0] = false; + WriteInt8(0, stream); } - - return (stream + 1) - start; } -size_t Net_ReadGameInfo(uint8_t*& stream) +void Net_ReadGameInfo(TArrayView& stream) { - const uint8_t* start = stream; - startmap = ReadStringConst(&stream); - rngseed = ReadInt32(&stream); - C_ReadCVars(&stream); + startmap = ReadStringConst(stream); + rngseed = ReadInt32(stream); + C_ReadCVars(stream); - if (stream[0]) + if (ReadInt8(stream)) { - ++stream; - const size_t len = strlen((char*)stream) + 1; + auto load = ReadString(stream); // Don't override the existing argument in case they need to use // a custom savefile name. if (!Args->CheckParm("-loadgame")) { Args->AppendArg("-loadgame"); - Args->AppendArg((char*)stream); + Args->AppendArg(load); } - stream += len; } - else - { - ++stream; - } - - return stream - start; } // Connects players to each other if needed. @@ -2412,6 +2381,11 @@ uint8_t *FDynamicBuffer::GetData(int *len) return m_Len ? m_Data : nullptr; } +TArrayView FDynamicBuffer::GetTArrayView() +{ + return TArrayView(m_Data, m_Len); +} + static int RemoveClass(FLevelLocals *Level, const PClass *cls) { AActor *actor; @@ -2447,7 +2421,7 @@ static int RemoveClass(FLevelLocals *Level, const PClass *cls) // [RH] Execute a special "ticcmd". The type byte should // have already been read, and the stream is positioned // at the beginning of the command's actual data. -void Net_DoCommand(int cmd, uint8_t **stream, int player) +void Net_DoCommand(int cmd, TArrayView& stream, int player) { uint8_t pos = 0; const char* s = nullptr; @@ -3040,7 +3014,7 @@ void Net_DoCommand(int cmd, uint8_t **stream, int player) } // Used by DEM_RUNSCRIPT, DEM_RUNSCRIPT2, and DEM_RUNNAMEDSCRIPT -static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int always) +static void RunScript(TArrayView& stream, AActor *pawn, int snum, int argn, int always) { // Scripts can be invoked without a level loaded, e.g. via puke(name) CCMD in fullscreen console if (pawn == nullptr) @@ -3061,44 +3035,44 @@ static void RunScript(uint8_t **stream, AActor *pawn, int snum, int argn, int al // not to execute them. Right now this is making setting up net commands a nightmare. // Reads through the network stream but doesn't actually execute any command. Used for getting the size of a stream. // The skip amount is the number of bytes the command possesses. This should mirror the bytes in Net_DoCommand(). -void Net_SkipCommand(int cmd, uint8_t **stream) +void Net_SkipCommand(int cmd, TArrayView& stream) { size_t skip = 0; switch (cmd) { case DEM_SAY: - skip = strlen((char *)(*stream + 1)) + 2; + skip = strlen((char *)(stream.Data() + 1)) + 2; break; case DEM_ADDBOT: - skip = strlen((char *)(*stream + 1)) + 6; + skip = strlen((char *)(stream.Data() + 1)) + 6; break; case DEM_GIVECHEAT: case DEM_TAKECHEAT: - skip = strlen((char *)(*stream)) + 5; + skip = strlen((char *)(stream.Data())) + 5; break; case DEM_SETINV: - skip = strlen((char *)(*stream)) + 6; + skip = strlen((char *)(stream.Data())) + 6; break; case DEM_NETEVENT: - skip = strlen((char *)(*stream)) + 15; + skip = strlen((char *)(stream.Data())) + 15; break; case DEM_ZSC_CMD: - skip = strlen((char*)(*stream)) + 1; - skip += (((*stream)[skip] << 8) | (*stream)[skip + 1]) + 2; + skip = strlen((char*)(stream.Data())) + 1; + skip += (stream[skip] << 8) | (stream[skip + 1]) + 2; break; case DEM_SUMMON2: case DEM_SUMMONFRIEND2: case DEM_SUMMONFOE2: - skip = strlen((char *)(*stream)) + 26; + skip = strlen((char *)(stream.Data())) + 26; break; case DEM_CHANGEMAP2: - skip = strlen((char *)(*stream + 1)) + 2; + skip = strlen((char *)(stream.Data() + 1)) + 2; break; case DEM_MUSICCHANGE: case DEM_PRINT: @@ -3114,7 +3088,7 @@ void Net_SkipCommand(int cmd, uint8_t **stream) case DEM_MORPHEX: case DEM_KILLCLASSCHEAT: case DEM_MDK: - skip = strlen((char *)(*stream)) + 1; + skip = strlen((char *)(stream.Data())) + 1; break; case DEM_WARPCHEAT: @@ -3141,14 +3115,14 @@ void Net_SkipCommand(int cmd, uint8_t **stream) break; case DEM_SAVEGAME: - skip = strlen((char *)(*stream)) + 1; - skip += strlen((char *)(*stream) + skip) + 1; + skip = strlen((char *)(stream.Data())) + 1; + skip += strlen((char *)(stream.Data()) + skip) + 1; break; case DEM_SINFCHANGEDXOR: case DEM_SINFCHANGED: { - uint8_t t = **stream; + uint8_t t = stream[0]; skip = 1 + (t & 63); if (cmd == DEM_SINFCHANGED) { @@ -3162,7 +3136,7 @@ void Net_SkipCommand(int cmd, uint8_t **stream) skip += 4; break; case CVAR_String: - skip += strlen((char*)(*stream + skip)) + 1; + skip += strlen((char*)(stream.Data() + skip)) + 1; break; } } @@ -3175,16 +3149,16 @@ void Net_SkipCommand(int cmd, uint8_t **stream) case DEM_RUNSCRIPT: case DEM_RUNSCRIPT2: - skip = 3 + *(*stream + 2) * 4; + skip = 3 + *(stream.Data() + 2) * 4; break; case DEM_RUNNAMEDSCRIPT: - skip = strlen((char *)(*stream)) + 2; - skip += ((*(*stream + skip - 1)) & 127) * 4; + skip = strlen((char *)(stream.Data())) + 2; + skip += ((*(stream.Data() + skip - 1)) & 127) * 4; break; case DEM_RUNSPECIAL: - skip = 3 + *(*stream + 2) * 4; + skip = 3 + *(stream.Data() + 2) * 4; break; case DEM_CONVREPLY: @@ -3195,14 +3169,14 @@ void Net_SkipCommand(int cmd, uint8_t **stream) case DEM_SETSLOTPNUM: { skip = 2 + (cmd == DEM_SETSLOTPNUM); - for (int numweapons = (*stream)[skip-1]; numweapons > 0; --numweapons) - skip += 1 + ((*stream)[skip] >> 7); + for (int numweapons = stream[skip-1]; numweapons > 0; --numweapons) + skip += 1 + (stream[skip] >> 7); } break; case DEM_ADDSLOT: case DEM_ADDSLOTDEFAULT: - skip = 2 + ((*stream)[1] >> 7); + skip = 2 + (stream[1] >> 7); break; case DEM_SETPITCHLIMIT: @@ -3210,7 +3184,7 @@ void Net_SkipCommand(int cmd, uint8_t **stream) break; } - *stream += skip; + AdvanceStream(stream, skip); } // This was taken out of shared_hud, because UI code shouldn't do low level calculations that may change if the backing implementation changes. diff --git a/src/d_net.h b/src/d_net.h index 8938f0c42..11e847bd9 100644 --- a/src/d_net.h +++ b/src/d_net.h @@ -65,6 +65,7 @@ public: void SetData(const uint8_t* data, int len); uint8_t* GetData(int* len = nullptr); + TArrayView GetTArrayView(); private: uint8_t* m_Data; @@ -149,8 +150,8 @@ void Net_WriteDouble(double); void Net_WriteString(const char *); void Net_WriteBytes(const uint8_t *, int len); -void Net_DoCommand(int cmd, uint8_t **stream, int player); -void Net_SkipCommand(int cmd, uint8_t **stream); +void Net_DoCommand(int cmd, TArrayView& stream, int player); +void Net_SkipCommand(int cmd, TArrayView& stream); bool Net_CheckCutsceneReady(); void Net_AdvanceCutscene(); diff --git a/src/d_netinf.h b/src/d_netinf.h index 56f955e06..92b9f1d21 100644 --- a/src/d_netinf.h +++ b/src/d_netinf.h @@ -56,10 +56,10 @@ void D_UserInfoChanged (FBaseCVar *info); bool D_SendServerInfoChange (FBaseCVar *cvar, UCVarValue value, ECVarType type); bool D_SendServerFlagChange (FBaseCVar *cvar, int bitnum, bool set, bool silent); -void D_DoServerInfoChange (uint8_t **stream, bool singlebit); +void D_DoServerInfoChange (TArrayView& stream, bool singlebit); FString D_GetUserInfoStrings(int pnum, bool compact = false); -void D_ReadUserInfoStrings (int player, uint8_t **stream, bool update); +void D_ReadUserInfoStrings (int player, TArrayView& stream, bool update); struct FPlayerColorSet; void D_GetPlayerColor (int player, float *h, float *s, float *v, FPlayerColorSet **colorset); diff --git a/src/d_netinfo.cpp b/src/d_netinfo.cpp index e319f9234..c4973a6e7 100644 --- a/src/d_netinfo.cpp +++ b/src/d_netinfo.cpp @@ -249,9 +249,9 @@ void D_PickRandomTeam (int player) { static char teamline[8] = "\\team\\X"; - uint8_t *foo = (uint8_t *)teamline; + auto foo = TArrayView((uint8_t *)teamline, sizeof(teamline)); teamline[6] = (char)D_PickRandomTeam() + '0'; - D_ReadUserInfoStrings (player, &foo, teamplay); + D_ReadUserInfoStrings (player, foo, teamplay); } int D_PickRandomTeam () @@ -581,7 +581,7 @@ void D_UserInfoChanged (FBaseCVar *cvar) Net_WriteString (foo); } -static const char *SetServerVar (char *name, ECVarType type, uint8_t **stream, bool singlebit) +static const char *SetServerVar (char *name, ECVarType type, TArrayView& stream, bool singlebit) { FBaseCVar *var = FindCVar (name, NULL); UCVarValue value; @@ -711,7 +711,7 @@ bool D_SendServerFlagChange (FBaseCVar *cvar, int bitnum, bool set, bool silent) return false; } -void D_DoServerInfoChange (uint8_t **stream, bool singlebit) +void D_DoServerInfoChange (TArrayView& stream, bool singlebit) { const char *value; char name[64]; @@ -723,8 +723,8 @@ void D_DoServerInfoChange (uint8_t **stream, bool singlebit) len &= 0x3f; if (len == 0) return; - memcpy (name, *stream, len); - *stream += len; + auto dst = TArrayView((uint8_t*)name, len); + ReadBytes(dst, stream); name[len] = 0; if ( (value = SetServerVar (name, (ECVarType)type, stream, singlebit)) && netgame) @@ -808,12 +808,12 @@ FString D_GetUserInfoStrings(int pnum, bool compact) return result; } -void D_ReadUserInfoStrings (int pnum, uint8_t **stream, bool update) +void D_ReadUserInfoStrings (int pnum, TArrayView& stream, bool update) { userinfo_t *info = &players[pnum].userinfo; TArray compact_names(info->CountUsed()); FBaseCVar **cvar_ptr; - const char *ptr = *((const char **)stream); + const char *ptr = (const char *)stream.Data(); const char *breakpt; FString value; bool compact; @@ -944,7 +944,7 @@ void D_ReadUserInfoStrings (int pnum, uint8_t **stream, bool update) } } } - *stream += strlen (*((char **)stream)) + 1; + AdvanceStream(stream, strlen((char*)stream.Data()) + 1); } void WriteUserInfo(FSerializer &arc, userinfo_t &info) diff --git a/src/d_protocol.cpp b/src/d_protocol.cpp index 2ecc713bc..de56e2ba3 100644 --- a/src/d_protocol.cpp +++ b/src/d_protocol.cpp @@ -38,7 +38,10 @@ #include "cmdlib.h" #include "serializer.h" -char* ReadString(uint8_t **stream) +// Unchecked stream functions. +// Use the checked versions instead unless you're checking the stream size yourself! + +char* UncheckedReadString(uint8_t **stream) { char *string = *((char **)stream); @@ -46,65 +49,65 @@ char* ReadString(uint8_t **stream) return copystring(string); } -const char* ReadStringConst(uint8_t **stream) +const char* UncheckedReadStringConst(uint8_t **stream) { const char *string = *((const char **)stream); *stream += strlen(string) + 1; return string; } -uint8_t ReadInt8(uint8_t **stream) +uint8_t UncheckedReadInt8(uint8_t **stream) { uint8_t v = **stream; *stream += 1; return v; } -int16_t ReadInt16(uint8_t **stream) +int16_t UncheckedReadInt16(uint8_t **stream) { int16_t v = (((*stream)[0]) << 8) | (((*stream)[1])); *stream += 2; return v; } -int32_t ReadInt32(uint8_t **stream) +int32_t UncheckedReadInt32(uint8_t **stream) { int32_t v = (((*stream)[0]) << 24) | (((*stream)[1]) << 16) | (((*stream)[2]) << 8) | (((*stream)[3])); *stream += 4; return v; } -int64_t ReadInt64(uint8_t** stream) +int64_t UncheckedReadInt64(uint8_t **stream) { int64_t v = (int64_t((*stream)[0]) << 56) | (int64_t((*stream)[1]) << 48) | (int64_t((*stream)[2]) << 40) | (int64_t((*stream)[3]) << 32) - | (int64_t((*stream)[4]) << 24) | (int64_t((*stream)[5]) << 16) | (int64_t((*stream)[6]) << 8) | (int64_t((*stream)[7])); + | (int64_t((*stream)[4]) << 24) | (int64_t((*stream)[5]) << 16) | (int64_t((*stream)[6]) << 8) | (int64_t((*stream)[7])); *stream += 8; return v; } -float ReadFloat(uint8_t **stream) +float UncheckedReadFloat(uint8_t **stream) { union { int32_t i; float f; } fakeint; - fakeint.i = ReadInt32 (stream); + fakeint.i = UncheckedReadInt32(stream); return fakeint.f; } -double ReadDouble(uint8_t** stream) +double UncheckedReadDouble(uint8_t **stream) { union { int64_t i; double f; } fakeint; - fakeint.i = ReadInt64(stream); + fakeint.i = UncheckedReadInt64(stream); return fakeint.f; } -void WriteString(const char *string, uint8_t **stream) +void UncheckedWriteString(const char *string, uint8_t **stream) { char *p = *((char **)stream); @@ -116,20 +119,20 @@ void WriteString(const char *string, uint8_t **stream) *stream = (uint8_t *)p; } -void WriteInt8(uint8_t v, uint8_t **stream) +void UncheckedWriteInt8(uint8_t v, uint8_t **stream) { **stream = v; *stream += 1; } -void WriteInt16(int16_t v, uint8_t **stream) +void UncheckedWriteInt16(int16_t v, uint8_t **stream) { (*stream)[0] = v >> 8; (*stream)[1] = v & 255; *stream += 2; } -void WriteInt32(int32_t v, uint8_t **stream) +void UncheckedWriteInt32(int32_t v, uint8_t **stream) { (*stream)[0] = v >> 24; (*stream)[1] = (v >> 16) & 255; @@ -138,7 +141,7 @@ void WriteInt32(int32_t v, uint8_t **stream) *stream += 4; } -void WriteInt64(int64_t v, uint8_t** stream) +void UncheckedWriteInt64(int64_t v, uint8_t **stream) { (*stream)[0] = v >> 56; (*stream)[1] = (v >> 48) & 255; @@ -151,7 +154,231 @@ void WriteInt64(int64_t v, uint8_t** stream) *stream += 8; } -void WriteFloat(float v, uint8_t **stream) +void UncheckedWriteFloat(float v, uint8_t **stream) +{ + union + { + int32_t i; + float f; + } fakeint; + fakeint.f = v; + UncheckedWriteInt32(fakeint.i, stream); +} + +void UncheckedWriteDouble(double v, uint8_t **stream) +{ + union + { + int64_t i; + double f; + } fakeint; + fakeint.f = v; + UncheckedWriteInt64(fakeint.i, stream); +} + +void AdvanceStream(TArrayView& stream, size_t bytes) +{ + assert(bytes <= stream.Size()); + stream = TArrayView(stream.Data() + bytes, stream.Size() - bytes); +} + +// Checked stream functions + +char* ReadString(TArrayView& stream) +{ + char *string = (char*)stream.Data(); + size_t len = strnlen(string, stream.Size()); + if (len == stream.Size()) + { + I_Error("Attempted to read past end of stream"); + } + AdvanceStream(stream, len + 1); + return copystring(string); +} + +const char* ReadStringConst(TArrayView& stream) +{ + const char* string = (const char*)stream.Data(); + size_t len = strnlen(string, stream.Size()); + if (len == stream.Size()) + { + I_Error("Attempted to read past end of stream"); + } + AdvanceStream(stream, len + 1); + return string; +} + +void ReadBytes(TArrayView& dst, TArrayView& stream) +{ + if (dst.Size() > stream.Size()) + { + I_Error("Attempted to read past end of stream"); + } + if (dst.Size()) + { + memcpy(dst.Data(), stream.Data(), dst.Size()); + AdvanceStream(stream, dst.Size()); + } +} + +uint8_t ReadInt8(TArrayView& stream) +{ + if (stream.Size() < 1) + { + I_Error("Attempted to read past end of stream"); + } + uint8_t v = stream[0]; + AdvanceStream(stream, 1); + return v; +} + +int16_t ReadInt16(TArrayView& stream) +{ + if (stream.Size() < 2) + { + I_Error("Attempted to read past end of stream"); + } + int16_t v = ((stream[0]) << 8) | stream[1]; + AdvanceStream(stream, 2); + return v; +} + +int32_t ReadInt32(TArrayView& stream) +{ + if (stream.Size() < 4) + { + I_Error("Attempted to read past end of stream"); + } + int32_t v = (stream[0] << 24) | (stream[1] << 16) | (stream[2] << 8) | stream[3]; + AdvanceStream(stream, 4); + return v; +} + +int64_t ReadInt64(TArrayView& stream) +{ + if (stream.Size() < 8) + { + I_Error("Attempted to read past end of stream"); + } + int64_t v = (int64_t(stream[0]) << 56) | (int64_t(stream[1]) << 48) | (int64_t(stream[2]) << 40) | (int64_t(stream[3]) << 32) + | (int64_t(stream[4]) << 24) | (int64_t(stream[5]) << 16) | (int64_t(stream[6]) << 8) | int64_t(stream[7]); + AdvanceStream(stream, 8); + return v; +} + +float ReadFloat(TArrayView& stream) +{ + union + { + int32_t i; + float f; + } fakeint; + fakeint.i = ReadInt32 (stream); + return fakeint.f; +} + +double ReadDouble(TArrayView& stream) +{ + union + { + int64_t i; + double f; + } fakeint; + fakeint.i = ReadInt64(stream); + return fakeint.f; +} + +void WriteString(const char *string, TArrayView& stream) +{ + char *p = (char *)stream.Data(); + unsigned int remaining = stream.Size(); + + while (*string) { + if (remaining-- == 0) + { + I_Error("Attempted to write past end of stream"); + } + *p++ = *string++; + } + + if (remaining == 0) + { + I_Error("Attempted to write past end of stream"); + } + *p++ = 0; + AdvanceStream(stream, p - (char*)stream.Data()); +} + +void WriteBytes(const TArrayView& source, TArrayView& stream) +{ + if (source.Size() > stream.Size()) + { + I_Error("Attempted to write past end of stream"); + } + if (source.Size()) + { + memcpy(stream.Data(), source.Data(), source.Size()); + AdvanceStream(stream, source.Size()); + } +} + +void WriteFString(FString& string, TArrayView& stream) +{ + WriteBytes(string.GetTArrayView(), stream); +} + +void WriteInt8(uint8_t v, TArrayView& stream) +{ + if (stream.Size() < 1) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v; + AdvanceStream(stream, 1); +} + +void WriteInt16(int16_t v, TArrayView& stream) +{ + if (stream.Size() < 2) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v >> 8; + stream[1] = v & 255; + AdvanceStream(stream, 2); +} + +void WriteInt32(int32_t v, TArrayView& stream) +{ + if (stream.Size() < 4) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v >> 24; + stream[1] = (v >> 16) & 255; + stream[2] = (v >> 8) & 255; + stream[3] = v & 255; + AdvanceStream(stream, 4); +} + +void WriteInt64(int64_t v, TArrayView& stream) +{ + if (stream.Size() < 8) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v >> 56; + stream[1] = (v >> 48) & 255; + stream[2] = (v >> 40) & 255; + stream[3] = (v >> 32) & 255; + stream[4] = (v >> 24) & 255; + stream[5] = (v >> 16) & 255; + stream[6] = (v >> 8) & 255; + stream[7] = v & 255; + AdvanceStream(stream, 8); +} + +void WriteFloat(float v, TArrayView& stream) { union { @@ -162,7 +389,7 @@ void WriteFloat(float v, uint8_t **stream) WriteInt32 (fakeint.i, stream); } -void WriteDouble(double v, uint8_t** stream) +void WriteDouble(double v, TArrayView& stream) { union { @@ -193,11 +420,8 @@ FSerializer& Serialize(FSerializer& arc, const char* key, usercmd_t& cmd, usercm return arc; } -// Returns the number of bytes read -int UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +void UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, TArrayView& stream) { - const uint8_t* start = stream; - if (basis != nullptr) { if (basis != &cmd) @@ -208,7 +432,7 @@ int UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) memset(&cmd, 0, sizeof(usercmd_t)); } - uint8_t flags = ReadInt8(&stream); + uint8_t flags = ReadInt8(stream); if (flags) { // We can support up to 29 buttons using 1 to 4 bytes to store them. The most @@ -216,19 +440,19 @@ int UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) // should be read in excluding the last one which supports all 8 bits. if (flags & UCMDF_BUTTONS) { - uint8_t in = ReadInt8(&stream); + uint8_t in = ReadInt8(stream); uint32_t buttons = (cmd.buttons & ~0x7F) | (in & 0x7F); if (in & MoreButtons) { - in = ReadInt8(&stream); + in = ReadInt8(stream); buttons = (buttons & ~(0x7F << 7)) | ((in & 0x7F) << 7); if (in & MoreButtons) { - in = ReadInt8(&stream); + in = ReadInt8(stream); buttons = (buttons & ~(0x7F << 14)) | ((in & 0x7F) << 14); if (in & MoreButtons) { - in = ReadInt8(&stream); + in = ReadInt8(stream); buttons = (buttons & ~(0xFF << 21)) | (in << 21); } } @@ -236,26 +460,24 @@ int UnpackUserCmd(usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) cmd.buttons = buttons; } if (flags & UCMDF_PITCH) - cmd.pitch = ReadInt16(&stream); + cmd.pitch = ReadInt16(stream); if (flags & UCMDF_YAW) - cmd.yaw = ReadInt16(&stream); + cmd.yaw = ReadInt16(stream); if (flags & UCMDF_FORWARDMOVE) - cmd.forwardmove = ReadInt16(&stream); + cmd.forwardmove = ReadInt16(stream); if (flags & UCMDF_SIDEMOVE) - cmd.sidemove = ReadInt16(&stream); + cmd.sidemove = ReadInt16(stream); if (flags & UCMDF_UPMOVE) - cmd.upmove = ReadInt16(&stream); + cmd.upmove = ReadInt16(stream); if (flags & UCMDF_ROLL) - cmd.roll = ReadInt16(&stream); + cmd.roll = ReadInt16(stream); } - - return int(stream - start); } -int PackUserCmd(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +void PackUserCmd(const usercmd_t& cmd, const usercmd_t* basis, TArrayView& stream) { uint8_t flags = 0; - uint8_t* start = stream; + auto flagsPosition = TArrayView(stream.Data(), 1); usercmd_t blank; if (basis == nullptr) @@ -264,7 +486,7 @@ int PackUserCmd(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) basis = ␣ } - ++stream; // Make room for the flags. + AdvanceStream(stream, 1); // Make room for the flags. uint32_t buttons_changed = cmd.buttons ^ basis->buttons; if (buttons_changed != 0) { @@ -284,56 +506,54 @@ int PackUserCmd(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) bytes[2] |= MoreButtons; } } - WriteInt8(bytes[0], &stream); + WriteInt8(bytes[0], stream); if (bytes[0] & MoreButtons) { - WriteInt8(bytes[1], &stream); + WriteInt8(bytes[1], stream); if (bytes[1] & MoreButtons) { - WriteInt8(bytes[2], &stream); + WriteInt8(bytes[2], stream); if (bytes[2] & MoreButtons) - WriteInt8(bytes[3], &stream); + WriteInt8(bytes[3], stream); } } } if (cmd.pitch != basis->pitch) { flags |= UCMDF_PITCH; - WriteInt16(cmd.pitch, &stream); + WriteInt16(cmd.pitch, stream); } if (cmd.yaw != basis->yaw) { flags |= UCMDF_YAW; - WriteInt16 (cmd.yaw, &stream); + WriteInt16 (cmd.yaw, stream); } if (cmd.forwardmove != basis->forwardmove) { flags |= UCMDF_FORWARDMOVE; - WriteInt16 (cmd.forwardmove, &stream); + WriteInt16 (cmd.forwardmove, stream); } if (cmd.sidemove != basis->sidemove) { flags |= UCMDF_SIDEMOVE; - WriteInt16(cmd.sidemove, &stream); + WriteInt16(cmd.sidemove, stream); } if (cmd.upmove != basis->upmove) { flags |= UCMDF_UPMOVE; - WriteInt16(cmd.upmove, &stream); + WriteInt16(cmd.upmove, stream); } if (cmd.roll != basis->roll) { flags |= UCMDF_ROLL; - WriteInt16(cmd.roll, &stream); + WriteInt16(cmd.roll, stream); } // Write the packing bits - WriteInt8(flags, &start); - - return int(stream - start); + WriteInt8(flags, flagsPosition); } -int WriteUserCmdMessage(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& stream) +void WriteUserCmdMessage(const usercmd_t& cmd, const usercmd_t* basis, TArrayView& stream) { if (basis == nullptr) { @@ -341,58 +561,60 @@ int WriteUserCmdMessage(const usercmd_t& cmd, const usercmd_t* basis, uint8_t*& || cmd.pitch || cmd.yaw || cmd.roll || cmd.forwardmove || cmd.sidemove || cmd.upmove) { - WriteInt8(DEM_USERCMD, &stream); - return PackUserCmd(cmd, basis, stream) + 1; + WriteInt8(DEM_USERCMD, stream); + PackUserCmd(cmd, basis, stream); + return; } } else if (cmd.buttons != basis->buttons || cmd.yaw != basis->yaw || cmd.pitch != basis->pitch || cmd.roll != basis->roll || cmd.forwardmove != basis->forwardmove || cmd.sidemove != basis->sidemove || cmd.upmove != basis->upmove) { - WriteInt8(DEM_USERCMD, &stream); - return PackUserCmd(cmd, basis, stream) + 1; + WriteInt8(DEM_USERCMD, stream); + PackUserCmd(cmd, basis, stream); + return; } - WriteInt8(DEM_EMPTYUSERCMD, &stream); - return 1; + WriteInt8(DEM_EMPTYUSERCMD, stream); } // Reads through the user command without actually setting any of its info. Used to get the size // of the command when getting the length of the stream. -int SkipUserCmdMessage(uint8_t*& stream) +void SkipUserCmdMessage(TArrayView& stream) { - const uint8_t* start = stream; - while (true) { - const uint8_t type = *stream++; + const uint8_t type = ReadInt8(stream); if (type == DEM_USERCMD) { int skip = 1; - if (*stream & UCMDF_PITCH) + if (stream[0] & UCMDF_PITCH) skip += 2; - if (*stream & UCMDF_YAW) + if (stream[0] & UCMDF_YAW) skip += 2; - if (*stream & UCMDF_FORWARDMOVE) + if (stream[0] & UCMDF_FORWARDMOVE) skip += 2; - if (*stream & UCMDF_SIDEMOVE) + if (stream[0] & UCMDF_SIDEMOVE) skip += 2; - if (*stream & UCMDF_UPMOVE) + if (stream[0] & UCMDF_UPMOVE) skip += 2; - if (*stream & UCMDF_ROLL) + if (stream[0] & UCMDF_ROLL) skip += 2; - if (*stream & UCMDF_BUTTONS) + if (stream[0] & UCMDF_BUTTONS) { - if (*++stream & MoreButtons) + AdvanceStream(stream, 1); + if (stream[0] & MoreButtons) { - if (*++stream & MoreButtons) + AdvanceStream(stream, 1); + if (stream[0] & MoreButtons) { - if (*++stream & MoreButtons) - ++stream; + AdvanceStream(stream, 1); + if (stream[0] & MoreButtons) + AdvanceStream(stream, 1); } } } - stream += skip; + AdvanceStream(stream, skip); break; } else if (type == DEM_EMPTYUSERCMD) @@ -401,31 +623,29 @@ int SkipUserCmdMessage(uint8_t*& stream) } else { - Net_SkipCommand(type, &stream); + Net_SkipCommand(type, stream); } } - - return int(stream - start); } -int ReadUserCmdMessage(uint8_t*& stream, int player, int tic) +void ReadUserCmdMessage(TArrayView& stream, int player, int tic) { const int ticMod = tic % BACKUPTICS; auto& curTic = ClientStates[player].Tics[ticMod]; usercmd_t& ticCmd = curTic.Command; - const uint8_t* start = stream; + const uint8_t* start = stream.Data(); // Skip until we reach the player command. Event data will get read off once the // tick is actually executed. int type; - while ((type = ReadInt8(&stream)) != DEM_USERCMD && type != DEM_EMPTYUSERCMD) - Net_SkipCommand(type, &stream); + while ((type = ReadInt8(stream)) != DEM_USERCMD && type != DEM_EMPTYUSERCMD) + Net_SkipCommand(type, stream); // Subtract a byte to account for the fact the stream head is now sitting on the // user command. - curTic.Data.SetData(start, int(stream - start - 1)); + curTic.Data.SetData(start, int(stream.Data() - start - 1)); if (type == DEM_USERCMD) { @@ -439,8 +659,6 @@ int ReadUserCmdMessage(uint8_t*& stream, int player, int tic) else memset(&ticCmd, 0, sizeof(ticCmd)); } - - return int(stream - start); } void RunPlayerCommands(int player, int tic) @@ -449,14 +667,12 @@ void RunPlayerCommands(int player, int tic) if (gametic % TicDup) return; - int len; auto& data = ClientStates[player].Tics[tic % BACKUPTICS].Data; - uint8_t* stream = data.GetData(&len); - if (stream != nullptr) + auto stream = data.GetTArrayView(); + if (stream.Size()) { - uint8_t* end = stream + len; - while (stream < end) - Net_DoCommand(ReadInt8(&stream), &stream, player); + while (stream.Size() > 0) + Net_DoCommand(ReadInt8(stream), stream, player); if (!demorecording) data.SetData(nullptr, 0); @@ -469,22 +685,23 @@ uint8_t* streamPos = nullptr; // Write the header of an IFF chunk and leave space // for the length field. -void StartChunk(int id, uint8_t **stream) +void StartChunk(int id, TArrayView& stream) { WriteInt32(id, stream); - streamPos = *stream; - *stream += 4; + streamPos = stream.Data(); + AdvanceStream(stream, 4); } // Write the length field for the chunk and insert // pad byte if the chunk is odd-sized. -void FinishChunk(uint8_t **stream) +void FinishChunk(TArrayView& stream) { if (streamPos == nullptr) return; - int len = int(*stream - streamPos - 4); - WriteInt32(len, &streamPos); + int len = int(stream.Data() - streamPos - 4); + auto streamPosView = TArrayView(streamPos, 4); + WriteInt32(len, streamPosView); if (len & 1) WriteInt8(0, stream); @@ -493,8 +710,8 @@ void FinishChunk(uint8_t **stream) // Skip past an unknown chunk. *stream should be // pointing to the chunk's length field. -void SkipChunk(uint8_t **stream) +void SkipChunk(TArrayView& stream) { int len = ReadInt32(stream); - *stream += len + (len & 1); + AdvanceStream(stream, len + (len & 1)); } diff --git a/src/d_protocol.h b/src/d_protocol.h index f4388d3bf..6a9717043 100644 --- a/src/d_protocol.h +++ b/src/d_protocol.h @@ -229,33 +229,53 @@ enum ECheatCommand CHT_MASSACRE2 }; -void StartChunk (int id, uint8_t **stream); -void FinishChunk (uint8_t **stream); -void SkipChunk (uint8_t **stream); +void StartChunk (int id, TArrayView& stream); +void FinishChunk (TArrayView& stream); +void SkipChunk (TArrayView& stream); -// Returns the number of bytes written to the stream -int UnpackUserCmd(usercmd_t& ucmd, const usercmd_t* basis, uint8_t*& stream); -int PackUserCmd(const usercmd_t& ucmd, const usercmd_t* basis, uint8_t*& stream); -int WriteUserCmdMessage(const usercmd_t& ucmd, const usercmd_t *basis, uint8_t*& stream); +void UnpackUserCmd(usercmd_t& ucmd, const usercmd_t* basis, TArrayView& stream); +void PackUserCmd(const usercmd_t& ucmd, const usercmd_t* basis, TArrayView& stream); +void WriteUserCmdMessage(const usercmd_t& ucmd, const usercmd_t *basis, TArrayView& stream); -int SkipUserCmdMessage(uint8_t*& stream); -int ReadUserCmdMessage(uint8_t*& stream, int player, int tic); +void SkipUserCmdMessage(TArrayView& stream); +void ReadUserCmdMessage(TArrayView& stream, int player, int tic); void RunPlayerCommands(int player, int tic); -uint8_t ReadInt8 (uint8_t **stream); -int16_t ReadInt16 (uint8_t **stream); -int32_t ReadInt32 (uint8_t **stream); -int64_t ReadInt64(uint8_t** stream); -float ReadFloat (uint8_t **stream); -double ReadDouble(uint8_t** stream); -char *ReadString (uint8_t **stream); -const char *ReadStringConst(uint8_t **stream); -void WriteInt8 (uint8_t val, uint8_t **stream); -void WriteInt16 (int16_t val, uint8_t **stream); -void WriteInt32 (int32_t val, uint8_t **stream); -void WriteInt64(int64_t val, uint8_t** stream); -void WriteFloat (float val, uint8_t **stream); -void WriteDouble(double val, uint8_t** stream); -void WriteString (const char *string, uint8_t **stream); +uint8_t UncheckedReadInt8(uint8_t** stream); +int16_t UncheckedReadInt16(uint8_t** stream); +int32_t UncheckedReadInt32(uint8_t** stream); +int64_t UncheckedReadInt64(uint8_t** stream); +float UncheckedReadFloat(uint8_t** stream); +double UncheckedReadDouble(uint8_t** stream); +char* UncheckedReadString(uint8_t** stream); +const char* UncheckedReadStringConst(uint8_t** stream); +void UncheckedWriteInt8(uint8_t val, uint8_t** stream); +void UncheckedWriteInt16(int16_t val, uint8_t** stream); +void UncheckedWriteInt32(int32_t val, uint8_t** stream); +void UncheckedWriteInt64(int64_t val, uint8_t** stream); +void UncheckedWriteFloat(float val, uint8_t** stream); +void UncheckedWriteDouble(double val, uint8_t** stream); +void UncheckedWriteString(const char* string, uint8_t** stream); + +void AdvanceStream(TArrayView& stream, size_t bytes); + +uint8_t ReadInt8(TArrayView& stream); +int16_t ReadInt16(TArrayView& stream); +int32_t ReadInt32(TArrayView& stream); +int64_t ReadInt64(TArrayView& stream); +float ReadFloat(TArrayView& stream); +double ReadDouble(TArrayView& stream); +char* ReadString(TArrayView& stream); +const char* ReadStringConst(TArrayView& stream); +void ReadBytes(TArrayView& dst, TArrayView& stream); +void WriteInt8(uint8_t val, TArrayView& stream); +void WriteInt16(int16_t val, TArrayView& stream); +void WriteInt32(int32_t val, TArrayView& stream); +void WriteInt64(int64_t val, TArrayView& stream); +void WriteFloat(float val, TArrayView& stream); +void WriteDouble(double val, TArrayView& stream); +void WriteString(const char* string, TArrayView& stream); +void WriteBytes(const TArrayView& source, TArrayView& stream); +void WriteFString(FString& string, TArrayView& stream); #endif //__D_PROTOCOL_H__ diff --git a/src/g_game.cpp b/src/g_game.cpp index 963437552..1d236328c 100644 --- a/src/g_game.cpp +++ b/src/g_game.cpp @@ -178,8 +178,8 @@ bool demorecording; bool demoplayback; bool demonew; // [RH] Only used around G_InitNew for demos int demover; -uint8_t* demobuffer; -uint8_t* demo_p; +TArray demobuffer; +TArrayView demo_p; uint8_t* democompspot; uint8_t* demobodyspot; size_t maxdemosize; @@ -2089,8 +2089,8 @@ void G_DoLoadGame () arc("importantcvars", cvar); if (!cvar.IsEmpty()) { - uint8_t *vars_p = (uint8_t *)cvar.GetChars(); - C_ReadCVars(&vars_p); + auto vars_p = cvar.GetTArrayView(); + C_ReadCVars(vars_p); } else { @@ -2488,14 +2488,14 @@ void G_ReadDemoTiccmd (usercmd_t *cmd, int player) while (id != DEM_USERCMD && id != DEM_EMPTYUSERCMD) { - if (!demorecording && demo_p >= zdembodyend) + if (!demorecording && demo_p.Data() >= zdembodyend) { // nothing left in the BODY chunk, so end playback. G_CheckDemoStatus (); break; } - id = ReadInt8 (&demo_p); + id = ReadInt8 (demo_p); switch (id) { @@ -2514,7 +2514,7 @@ void G_ReadDemoTiccmd (usercmd_t *cmd, int player) case DEM_DROPPLAYER: { - uint8_t i = ReadInt8 (&demo_p); + uint8_t i = ReadInt8 (demo_p); if (i < MAXPLAYERS) { playeringame[i] = false; @@ -2523,7 +2523,7 @@ void G_ReadDemoTiccmd (usercmd_t *cmd, int player) break; default: - Net_DoCommand (id, &demo_p, player); + Net_DoCommand (id, demo_p, player); break; } } @@ -2556,8 +2556,7 @@ void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf) // [RH] Write any special "ticcmds" for this player to the demo if ((specdata = ClientStates[player].Tics[buf % BACKUPTICS].Data.GetData (&speclen)) && !(gametic % TicDup)) { - memcpy (demo_p, specdata, speclen); - demo_p += speclen; + WriteBytes(TArrayView(specdata, speclen), demo_p); ClientStates[player].Tics[buf % BACKUPTICS].Data.SetData(nullptr, 0); } @@ -2566,19 +2565,19 @@ void G_WriteDemoTiccmd (usercmd_t *cmd, int player, int buf) WriteUserCmdMessage (*cmd, &players[player].cmd, demo_p); // [RH] Bigger safety margin - if (demo_p > demobuffer + maxdemosize - 64) + if (demo_p.Data() > demobuffer.Data() + demobuffer.Size() - 64) { - ptrdiff_t pos = demo_p - demobuffer; - ptrdiff_t spot = streamPos - demobuffer; - ptrdiff_t comp = democompspot - demobuffer; - ptrdiff_t body = demobodyspot - demobuffer; + ptrdiff_t pos = demo_p.Data() - demobuffer.Data(); + ptrdiff_t spot = streamPos - demobuffer.Data(); + ptrdiff_t comp = democompspot - demobuffer.Data(); + ptrdiff_t body = demobodyspot - demobuffer.Data(); // [RH] Allocate more space for the demo maxdemosize += 0x20000; - demobuffer = (uint8_t *)M_Realloc (demobuffer, maxdemosize); - demo_p = demobuffer + pos; - streamPos = demobuffer + spot; - democompspot = demobuffer + comp; - demobodyspot = demobuffer + body; + demobuffer.Resize(maxdemosize); + demo_p = TArrayView(demobuffer.Data() + pos, demobuffer.Size() - pos); + streamPos = demobuffer.Data() + spot; + democompspot = demobuffer.Data() + comp; + demobodyspot = demobuffer.Data() + body; } } @@ -2594,7 +2593,7 @@ void G_RecordDemo (const char* name) FixPathSeperator (demoname); DefaultExtension (demoname, ".lmp"); maxdemosize = 0x20000; - demobuffer = (uint8_t *)M_Malloc (maxdemosize); + demobuffer.Resize(maxdemosize); demorecording = true; } @@ -2611,35 +2610,33 @@ void G_BeginRecording (const char *startmap) { startmap = primaryLevel->MapName.GetChars(); } - demo_p = demobuffer; + demo_p = TArrayView(demobuffer.Data(), demobuffer.Size()); - WriteInt32 (FORM_ID, &demo_p); // Write FORM ID - demo_p += 4; // Leave space for len - WriteInt32 (ZDEM_ID, &demo_p); // Write ZDEM ID + WriteInt32 (FORM_ID, demo_p); // Write FORM ID + AdvanceStream(demo_p, 4); // Leave space for len + WriteInt32 (ZDEM_ID, demo_p); // Write ZDEM ID // Write header chunk - StartChunk (ZDHD_ID, &demo_p); - WriteInt16 (DEMOGAMEVERSION, &demo_p); // Write ZDoom version - *demo_p++ = 2; // Write minimum version needed to use this demo. - *demo_p++ = 3; // (Useful?) + StartChunk (ZDHD_ID, demo_p); + WriteInt16 (DEMOGAMEVERSION, demo_p); // Write ZDoom version + WriteInt8(2, demo_p); // Write minimum version needed to use this demo. + WriteInt8(3, demo_p); // (Useful?) - strcpy((char*)demo_p, startmap); // Write name of map demo was recorded on. - demo_p += strlen(startmap) + 1; - WriteInt32(rngseed, &demo_p); // Write RNG seed - *demo_p++ = consoleplayer; - FinishChunk (&demo_p); + WriteString(startmap, demo_p); // Write name of map demo was recorded on. + WriteInt32(rngseed, demo_p); // Write RNG seed + WriteInt8(consoleplayer, demo_p); + FinishChunk (demo_p); // Write player info chunks for (i = 0; i < MAXPLAYERS; i++) { if (playeringame[i]) { - StartChunk(UINF_ID, &demo_p); - WriteInt8((uint8_t)i, &demo_p); + StartChunk(UINF_ID, demo_p); + WriteInt8((uint8_t)i, demo_p); auto str = D_GetUserInfoStrings(i); - memcpy(demo_p, str.GetChars(), str.Len() + 1); - demo_p += str.Len() + 1; - FinishChunk(&demo_p); + WriteFString(str, demo_p); + FinishChunk(demo_p); } } @@ -2648,29 +2645,29 @@ void G_BeginRecording (const char *startmap) // enough. if (multiplayer) { - StartChunk (NETD_ID, &demo_p); - FinishChunk (&demo_p); + StartChunk (NETD_ID, demo_p); + FinishChunk (demo_p); } // Write cvars chunk - StartChunk (VARS_ID, &demo_p); - C_WriteCVars (&demo_p, CVAR_SERVERINFO|CVAR_DEMOSAVE); - FinishChunk (&demo_p); + StartChunk (VARS_ID, demo_p); + C_WriteCVars (demo_p, CVAR_SERVERINFO|CVAR_DEMOSAVE); + FinishChunk (demo_p); // Write weapon ordering chunk - StartChunk (WEAP_ID, &demo_p); - P_WriteDemoWeaponsChunk(&demo_p); - FinishChunk (&demo_p); + StartChunk (WEAP_ID, demo_p); + P_WriteDemoWeaponsChunk(demo_p); + FinishChunk (demo_p); // Indicate body is compressed - StartChunk (COMP_ID, &demo_p); - democompspot = demo_p; - WriteInt32 (0, &demo_p); - FinishChunk (&demo_p); + StartChunk (COMP_ID, demo_p); + democompspot = demo_p.Data(); + WriteInt32 (0, demo_p); + FinishChunk (demo_p); // Begin BODY chunk - StartChunk (BODY_ID, &demo_p); - demobodyspot = demo_p; + StartChunk (BODY_ID, demo_p); + demobodyspot = demo_p.Data(); } @@ -2730,13 +2727,13 @@ bool G_ProcessIFFDemo (FString &mapname) for (i = 0; i < MAXPLAYERS; i++) playeringame[i] = 0; - len = ReadInt32 (&demo_p); - zdemformend = demo_p + len + (len & 1); + len = ReadInt32 (demo_p); + zdemformend = demo_p.Data() + len + (len & 1); // Check to make sure this is a ZDEM chunk file. // TODO: Support multiple FORM ZDEMs in a CAT. Might be useful. - id = ReadInt32 (&demo_p); + id = ReadInt32 (demo_p); if (id != ZDEM_ID) { Printf ("Not a " GAMENAME " demo file!\n"); @@ -2745,11 +2742,11 @@ bool G_ProcessIFFDemo (FString &mapname) // Process all chunks until a BODY chunk is encountered. - while (demo_p < zdemformend && !bodyHit) + while (demo_p.Data() < zdemformend && !bodyHit) { - id = ReadInt32 (&demo_p); - len = ReadInt32 (&demo_p); - nextchunk = demo_p + len + (len & 1); + id = ReadInt32 (demo_p); + len = ReadInt32 (demo_p); + nextchunk = demo_p.Data() + len + (len & 1); if (nextchunk > zdemformend) { Printf ("Demo is mangled!\n"); @@ -2761,48 +2758,47 @@ bool G_ProcessIFFDemo (FString &mapname) case ZDHD_ID: headerHit = true; - demover = ReadInt16 (&demo_p); // ZDoom version demo was created with + demover = ReadInt16 (demo_p); // ZDoom version demo was created with if (demover < MINDEMOVERSION) { Printf ("Demo requires an older version of " GAMENAME "!\n"); //return true; } - if (ReadInt16 (&demo_p) > DEMOGAMEVERSION) // Minimum ZDoom version + if (ReadInt16 (demo_p) > DEMOGAMEVERSION) // Minimum ZDoom version { Printf ("Demo requires a newer version of " GAMENAME "!\n"); return true; } if (demover >= 0x21a) { - mapname = (char*)demo_p; - demo_p += mapname.Len() + 1; + mapname = ReadStringConst(demo_p); } else { - mapname = FString((char*)demo_p, 8); - demo_p += 8; + mapname = FString((char*)demo_p.Data(), 8); + AdvanceStream(demo_p, 8); } - rngseed = ReadInt32 (&demo_p); + rngseed = ReadInt32 (demo_p); // Only reset the RNG if this demo is not in conjunction with a savegame. if (mapname[0] != 0) { FRandom::StaticClearRandom (); } - consoleplayer = *demo_p++; + consoleplayer = ReadInt8(demo_p); break; case VARS_ID: - C_ReadCVars (&demo_p); + C_ReadCVars (demo_p); break; case UINF_ID: - i = ReadInt8 (&demo_p); + i = ReadInt8 (demo_p); if (!playeringame[i]) { playeringame[i] = 1; numPlayers++; } - D_ReadUserInfoStrings (i, &demo_p, false); + D_ReadUserInfoStrings (i, demo_p, false); break; case NETD_ID: @@ -2810,21 +2806,21 @@ bool G_ProcessIFFDemo (FString &mapname) break; case WEAP_ID: - P_ReadDemoWeaponsChunk(&demo_p); + P_ReadDemoWeaponsChunk(demo_p); break; case BODY_ID: bodyHit = true; - zdembodyend = demo_p + len; + zdembodyend = demo_p.Data() + len; break; case COMP_ID: - uncompSize = ReadInt32 (&demo_p); + uncompSize = ReadInt32 (demo_p); break; } if (!bodyHit) - demo_p = nextchunk; + demo_p = TArrayView(nextchunk, demo_p.Size() + demo_p.Data() - nextchunk); } if (!headerHit) @@ -2851,17 +2847,16 @@ bool G_ProcessIFFDemo (FString &mapname) if (uncompSize > 0) { - uint8_t *uncompressed = (uint8_t*)M_Malloc(uncompSize); - int r = uncompress (uncompressed, &uncompSize, demo_p, uLong(zdembodyend - demo_p)); + auto uncompressed = TArray(uncompSize, true); + int r = uncompress (uncompressed.Data(), &uncompSize, demo_p.Data(), uLong(zdembodyend - demo_p.Data())); if (r != Z_OK) { Printf ("Could not decompress demo! %s\n", M_ZLibError(r).GetChars()); - M_Free(uncompressed); return true; } - M_Free (demobuffer); - zdembodyend = uncompressed + uncompSize; - demobuffer = demo_p = uncompressed; + zdembodyend = uncompressed.Data() + uncompSize; + demobuffer = std::move(uncompressed); + demo_p = TArrayView(demobuffer.Data(), uncompSize); } return false; @@ -2878,9 +2873,9 @@ void G_DoPlayDemo (void) demolump = fileSystem.CheckNumForFullName (defdemoname.GetChars(), true); if (demolump >= 0) { - int demolen = fileSystem.FileLength (demolump); - demobuffer = (uint8_t *)M_Malloc(demolen); - fileSystem.ReadFile (demolump, demobuffer); + size_t demolen = fileSystem.FileLength (demolump); + demobuffer.Resize(demolen); + fileSystem.ReadFile (demolump, demobuffer.Data()); } else { @@ -2891,26 +2886,26 @@ void G_DoPlayDemo (void) { I_Error("Unable to open demo '%s'", defdemoname.GetChars()); } - auto len = fr.GetLength(); - demobuffer = (uint8_t*)M_Malloc(len); - if (fr.Read(demobuffer, len) != len) + size_t demolen = fr.GetLength(); + demobuffer.Resize(demolen); + if (fr.Read(demobuffer.Data(), demolen) != demolen) { I_Error("Unable to read demo '%s'", defdemoname.GetChars()); } } - demo_p = demobuffer; + demo_p = TArrayView(demobuffer.Data(), demobuffer.Size()); if (singledemo) Printf ("Playing demo %s\n", defdemoname.GetChars()); C_BackupCVars (); // [RH] Save cvars that might be affected by demo - if (ReadInt32 (&demo_p) != FORM_ID) + if (ReadInt32 (demo_p) != FORM_ID) { const char *eek = "Cannot play non-" GAMENAME " demos.\n"; C_ForgetCVars(); - M_Free(demobuffer); - demo_p = demobuffer = NULL; + demobuffer.Reset(); + demo_p = NULL; if (singledemo) { I_Error ("%s", eek); @@ -2990,8 +2985,7 @@ bool G_CheckDemoStatus (void) endtime = I_GetTime () - starttime; C_RestoreCVars (); // [RH] Restore cvars demo might have changed - M_Free (demobuffer); - demobuffer = NULL; + demobuffer.Reset(); P_SetupWeapons_ntohton(); demoplayback = false; @@ -3034,9 +3028,7 @@ bool G_CheckDemoStatus (void) if (demorecording) { - uint8_t *formlen; - - WriteInt8 (DEM_STOP, &demo_p); + WriteInt8 (DEM_STOP, demo_p); if (demo_compress) { @@ -3044,32 +3036,31 @@ bool G_CheckDemoStatus (void) // a compressed version. If the BODY successfully compresses, the // contents of the COMP chunk will be changed to indicate the // uncompressed size of the BODY. - uLong len = uLong(demo_p - demobodyspot); + uLong len = uLong(demo_p.Data() - demobodyspot); uLong outlen = (len + len/100 + 12); TArray compressed(outlen, true); int r = compress2 (compressed.Data(), &outlen, demobodyspot, len, 9); if (r == Z_OK && outlen < len) { - formlen = democompspot; - WriteInt32 (len, &democompspot); + UncheckedWriteInt32 (len, &democompspot); memcpy (demobodyspot, compressed.Data(), outlen); - demo_p = demobodyspot + outlen; + demo_p = TArrayView(demobodyspot + outlen, demo_p.Size() + len - outlen); } } - FinishChunk (&demo_p); - formlen = demobuffer + 4; - WriteInt32 (int(demo_p - demobuffer - 8), &formlen); + FinishChunk (demo_p); + uint8_t* formlen = demobuffer.Data() + 4; + UncheckedWriteInt32 (int(demo_p.Data() - demobuffer.Data() - 8), &formlen); auto fw = FileWriter::Open(demoname.GetChars()); bool saved = false; if (fw != nullptr) { - const size_t size = demo_p - demobuffer; - saved = fw->Write(demobuffer, size) == size; + const size_t size = demo_p.Data() - demobuffer.Data(); + saved = fw->Write(demobuffer.Data(), size) == size; delete fw; if (!saved) RemoveFile(demoname.GetChars()); } - M_Free (demobuffer); + demobuffer.Reset(); demorecording = false; stoprecording = false; if (saved) diff --git a/src/gamedata/a_weapons.cpp b/src/gamedata/a_weapons.cpp index 93768fd54..be8c01df5 100644 --- a/src/gamedata/a_weapons.cpp +++ b/src/gamedata/a_weapons.cpp @@ -855,7 +855,7 @@ static int ntoh_cmp(const void *a, const void *b) // //=========================================================================== -void P_WriteDemoWeaponsChunk(uint8_t **demo) +void P_WriteDemoWeaponsChunk(TArrayView& demo) { WriteInt16(Weapons_ntoh.Size(), demo); for (unsigned int i = 1; i < Weapons_ntoh.Size(); ++i) @@ -873,7 +873,7 @@ void P_WriteDemoWeaponsChunk(uint8_t **demo) // //=========================================================================== -void P_ReadDemoWeaponsChunk(uint8_t **demo) +void P_ReadDemoWeaponsChunk(TArrayView& demo) { int count, i; PClassActor *type; @@ -938,7 +938,7 @@ void Net_WriteWeapon(PClassActor *type) // //=========================================================================== -PClassActor *Net_ReadWeapon(uint8_t **stream) +PClassActor *Net_ReadWeapon(TArrayView& stream) { int index; diff --git a/src/gamedata/a_weapons.h b/src/gamedata/a_weapons.h index a626959d8..610e2502f 100644 --- a/src/gamedata/a_weapons.h +++ b/src/gamedata/a_weapons.h @@ -120,11 +120,11 @@ public: void P_PlaybackKeyConfWeapons(FWeaponSlots *slots); void Net_WriteWeapon(PClassActor *type); -PClassActor *Net_ReadWeapon(uint8_t **stream); +PClassActor *Net_ReadWeapon(TArrayView& stream); void P_SetupWeapons_ntohton(); -void P_WriteDemoWeaponsChunk(uint8_t **demo); -void P_ReadDemoWeaponsChunk(uint8_t **demo); +void P_WriteDemoWeaponsChunk(TArrayView& demo); +void P_ReadDemoWeaponsChunk(TArrayView& demo); enum class EBobStyle diff --git a/src/p_conversation.cpp b/src/p_conversation.cpp index 417b0e49f..a43204e64 100644 --- a/src/p_conversation.cpp +++ b/src/p_conversation.cpp @@ -667,7 +667,7 @@ static void HandleReply(player_t *player, bool isconsole, int nodenum, int reply // //============================================================================ -void P_ConversationCommand (int netcode, int pnum, uint8_t **stream) +void P_ConversationCommand (int netcode, int pnum, TArrayView& stream) { player_t *player = &players[pnum]; diff --git a/src/p_conversation.h b/src/p_conversation.h index d674b3d53..8f55da7b4 100644 --- a/src/p_conversation.h +++ b/src/p_conversation.h @@ -70,7 +70,7 @@ void P_FreeStrifeConversations (); void P_StartConversation (AActor *npc, AActor *pc, bool facetalker, bool saveangle); void P_ResumeConversation (); -void P_ConversationCommand (int netcode, int player, uint8_t **stream); +void P_ConversationCommand (int netcode, int player, TArrayView& stream); #endif diff --git a/src/playsim/bots/b_bot.h b/src/playsim/bots/b_bot.h index 9e9cadeb8..bbcd4f2b0 100644 --- a/src/playsim/bots/b_bot.h +++ b/src/playsim/bots/b_bot.h @@ -120,7 +120,7 @@ public: void Init (); void End(); bool SpawnBot (const char *name, int color = NOCOLOR); - void TryAddBot (FLevelLocals *Level, uint8_t **stream, int player); + void TryAddBot (FLevelLocals *Level, TArrayView& stream, int player); void RemoveAllBots (FLevelLocals *Level, bool fromlist); bool LoadBots (); void ForgetBots (); @@ -154,7 +154,7 @@ public: private: //(b_game.cpp) - bool DoAddBot (FLevelLocals *Level, uint8_t *info, botskill_t skill); + bool DoAddBot (FLevelLocals *Level, TArrayView info, botskill_t skill); protected: bool ctf; diff --git a/src/playsim/bots/b_game.cpp b/src/playsim/bots/b_game.cpp index 19f212a61..ba35b580d 100644 --- a/src/playsim/bots/b_game.cpp +++ b/src/playsim/bots/b_game.cpp @@ -319,7 +319,7 @@ bool FCajunMaster::SpawnBot (const char *name, int color) return true; } -void FCajunMaster::TryAddBot (FLevelLocals *Level, uint8_t **stream, int player) +void FCajunMaster::TryAddBot (FLevelLocals *Level, TArrayView& stream, int player) { int botshift = ReadInt8 (stream); char *info = ReadString (stream); @@ -342,7 +342,7 @@ void FCajunMaster::TryAddBot (FLevelLocals *Level, uint8_t **stream, int player) } } - if (DoAddBot (Level,(uint8_t *)info, skill)) + if (DoAddBot (Level, TArrayView((uint8_t*)info, strlen(info)+1), skill)) { //Increment this. botnum++; @@ -363,7 +363,7 @@ void FCajunMaster::TryAddBot (FLevelLocals *Level, uint8_t **stream, int player) delete[] info; } -bool FCajunMaster::DoAddBot (FLevelLocals *Level, uint8_t *info, botskill_t skill) +bool FCajunMaster::DoAddBot (FLevelLocals *Level, TArrayView info, botskill_t skill) { int bnum; @@ -381,7 +381,7 @@ bool FCajunMaster::DoAddBot (FLevelLocals *Level, uint8_t *info, botskill_t skil return false; } - D_ReadUserInfoStrings (bnum, &info, false); + D_ReadUserInfoStrings (bnum, info, false); multiplayer = true; //Prevents cheating and so on; emulates real netgame (almost). players[bnum].Bot = Level->CreateThinker(); From 8a020fbe9d34797fb57d444e52dfeec56b3653e5 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 10 Jul 2025 04:23:25 -0400 Subject: [PATCH 279/384] Mark offensive items and ammo as WEAPONSPAWN Better filters these when playing in co-op mode with multiplayer things enabled. --- wadsrc/static/zscript/actors/heretic/hereticartifacts.zs | 2 ++ wadsrc/static/zscript/actors/hexen/blastradius.zs | 1 + wadsrc/static/zscript/actors/hexen/flechette.zs | 1 + wadsrc/static/zscript/actors/hexen/mana.zs | 2 ++ wadsrc/static/zscript/actors/hexen/summon.zs | 1 + wadsrc/static/zscript/actors/hexen/teleportother.zs | 1 + wadsrc/static/zscript/actors/inventory/ammo.zs | 6 ++++++ wadsrc/static/zscript/actors/raven/artiegg.zs | 2 ++ 8 files changed, 16 insertions(+) diff --git a/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs b/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs index 117d37586..7d15b7603 100644 --- a/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs +++ b/wadsrc/static/zscript/actors/heretic/hereticartifacts.zs @@ -54,6 +54,7 @@ Class ArtiTomeOfPower : PowerupGiver { +COUNTITEM +FLOATBOB + +WEAPONSPAWN Inventory.PickupFlash "PickupFlash"; Inventory.Icon "ARTIPWBK"; Powerup.Type "PowerWeaponlevel2"; @@ -129,6 +130,7 @@ Class ArtiTimeBomb : Inventory { +COUNTITEM +FLOATBOB + +WEAPONSPAWN Inventory.PickupFlash "PickupFlash"; +INVENTORY.INVBAR +INVENTORY.FANCYPICKUPSOUND diff --git a/wadsrc/static/zscript/actors/hexen/blastradius.zs b/wadsrc/static/zscript/actors/hexen/blastradius.zs index b66ca1418..3abbc8d42 100644 --- a/wadsrc/static/zscript/actors/hexen/blastradius.zs +++ b/wadsrc/static/zscript/actors/hexen/blastradius.zs @@ -4,6 +4,7 @@ class ArtiBlastRadius : CustomInventory Default { +FLOATBOB + +WEAPONSPAWN Inventory.DefMaxAmount; Inventory.PickupFlash "PickupFlash"; +INVENTORY.INVBAR +INVENTORY.FANCYPICKUPSOUND diff --git a/wadsrc/static/zscript/actors/hexen/flechette.zs b/wadsrc/static/zscript/actors/hexen/flechette.zs index 0526cebe5..1a4e1ce81 100644 --- a/wadsrc/static/zscript/actors/hexen/flechette.zs +++ b/wadsrc/static/zscript/actors/hexen/flechette.zs @@ -157,6 +157,7 @@ class ArtiPoisonBag : Inventory Default { +FLOATBOB + +WEAPONSPAWN Inventory.DefMaxAmount; Inventory.PickupFlash "PickupFlash"; +INVENTORY.INVBAR +INVENTORY.FANCYPICKUPSOUND diff --git a/wadsrc/static/zscript/actors/hexen/mana.zs b/wadsrc/static/zscript/actors/hexen/mana.zs index ad3d6d66b..0e01bb971 100644 --- a/wadsrc/static/zscript/actors/hexen/mana.zs +++ b/wadsrc/static/zscript/actors/hexen/mana.zs @@ -57,6 +57,7 @@ class Mana3 : CustomInventory Radius 8; Height 8; +FLOATBOB + +WEAPONSPAWN Inventory.PickupMessage "$TXT_MANA_BOTH"; } States @@ -79,6 +80,7 @@ class ArtiBoostMana : CustomInventory { +FLOATBOB +COUNTITEM + +WEAPONSPAWN +INVENTORY.INVBAR +INVENTORY.FANCYPICKUPSOUND Inventory.PickupFlash "PickupFlash"; diff --git a/wadsrc/static/zscript/actors/hexen/summon.zs b/wadsrc/static/zscript/actors/hexen/summon.zs index 3e8246a89..c1c7585df 100644 --- a/wadsrc/static/zscript/actors/hexen/summon.zs +++ b/wadsrc/static/zscript/actors/hexen/summon.zs @@ -7,6 +7,7 @@ class ArtiDarkServant : Inventory { +COUNTITEM +FLOATBOB + +WEAPONSPAWN Inventory.RespawnTics 4230; Inventory.DefMaxAmount; Inventory.PickupFlash "PickupFlash"; diff --git a/wadsrc/static/zscript/actors/hexen/teleportother.zs b/wadsrc/static/zscript/actors/hexen/teleportother.zs index 200eece06..dea7369c4 100644 --- a/wadsrc/static/zscript/actors/hexen/teleportother.zs +++ b/wadsrc/static/zscript/actors/hexen/teleportother.zs @@ -7,6 +7,7 @@ class ArtiTeleportOther : Inventory { +COUNTITEM +FLOATBOB + +WEAPONSPAWN +INVENTORY.INVBAR +INVENTORY.FANCYPICKUPSOUND Inventory.PickupFlash "PickupFlash"; diff --git a/wadsrc/static/zscript/actors/inventory/ammo.zs b/wadsrc/static/zscript/actors/inventory/ammo.zs index f260d8b24..06388ff2f 100644 --- a/wadsrc/static/zscript/actors/inventory/ammo.zs +++ b/wadsrc/static/zscript/actors/inventory/ammo.zs @@ -48,6 +48,7 @@ class Ammo : Inventory Default { +INVENTORY.KEEPDEPLETED + +WEAPONSPAWN Inventory.PickupSound "misc/ammo_pkup"; Ammo.DropAmmoFactorMultiplier 1; @@ -241,6 +242,11 @@ class Ammo : Inventory class BackpackItem : Inventory { bool bDepleted; + + Default + { + +WEAPONSPAWN + } //=========================================================================== // diff --git a/wadsrc/static/zscript/actors/raven/artiegg.zs b/wadsrc/static/zscript/actors/raven/artiegg.zs index a51e2e1f5..96c70a670 100644 --- a/wadsrc/static/zscript/actors/raven/artiegg.zs +++ b/wadsrc/static/zscript/actors/raven/artiegg.zs @@ -35,6 +35,7 @@ class ArtiEgg : CustomInventory +INVENTORY.INVBAR Inventory.PickupFlash "PickupFlash"; +INVENTORY.FANCYPICKUPSOUND + +WEAPONSPAWN Inventory.Icon "ARTIEGGC"; Inventory.PickupSound "misc/p_pkup"; Inventory.PickupMessage "$TXT_ARTIEGG"; @@ -90,6 +91,7 @@ class ArtiPork : CustomInventory +INVENTORY.INVBAR Inventory.PickupFlash "PickupFlash"; +INVENTORY.FANCYPICKUPSOUND + +WEAPONSPAWN Inventory.Icon "ARTIPORK"; Inventory.PickupSound "misc/p_pkup"; Inventory.PickupMessage "$TXT_ARTIEGG2"; From 29b2253bdbcfb138f0e4e06f0611802a878e731e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Mon, 7 Jul 2025 13:39:13 -0300 Subject: [PATCH 280/384] TArray list constructor --- src/common/utility/tarray.h | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/common/utility/tarray.h b/src/common/utility/tarray.h index ac9d8beb1..3a6bd76e9 100644 --- a/src/common/utility/tarray.h +++ b/src/common/utility/tarray.h @@ -231,6 +231,29 @@ public: ConstructEmpty(0, Count - 1); } } + + TArray (std::initializer_list list) + { + Most = list.size; + Count = list.size; + + if (Count > 0) + { + Array = (T *)M_Malloc (sizeof(T) * Count); + + const T* it = list.begin(); + + for (unsigned int i = 0; i < Count; ++i) + { + ::new(&Array[i]) T(*it++); + } + } + else + { + Array = nullptr; + } + } + TArray (const TArray &other) { DoCopy (other); From 269689703ddcc573c6e456281eb081c034f45a90 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Mon, 7 Jul 2025 21:23:16 -0400 Subject: [PATCH 281/384] Clear references to map data on level change These shouldn't be left as they'll now point towards potentially invalid memory and also cause errors with serializing. Arrays and maps holding them are cleared. Also unlinks and relinks inventory items correctly from the hashmap on traveling. --- src/common/objects/dobject.cpp | 61 ++++++++++++++++++++++++++++++++++ src/common/objects/dobject.h | 3 ++ src/common/utility/tarray.h | 4 +-- src/g_level.cpp | 8 ++++- src/namedef_custom.h | 7 ++++ src/p_setup.cpp | 7 ++++ 6 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/common/objects/dobject.cpp b/src/common/objects/dobject.cpp index 5b2658a70..b9817a919 100644 --- a/src/common/objects/dobject.cpp +++ b/src/common/objects/dobject.cpp @@ -417,6 +417,67 @@ size_t DObject::PropagateMark() // //========================================================================== +void DObject::ClearNativePointerFields(const TArray& types) +{ + auto cls = GetClass(); + if (cls->VMType == nullptr) + return; + + auto it = cls->VMType->Symbols.GetIterator(); + TMap::Pair* sym = nullptr; + while (it.NextPair(sym)) + { + auto field = dyn_cast(sym->Value); + if (field == nullptr) + continue; + + PType* base = field->Type; + PType* t = base; + if (base->isArray() && !base->isStaticArray()) + t = static_cast(base)->ElementType; + else if (base->isDynArray()) + t = static_cast(base)->ElementType; + else if (base->isMap()) + t = static_cast(base)->ValueType; + + if (!t->isRealPointer()) + continue; + + auto pType = static_cast(t)->PointedType; + if (!pType->isStruct() || !static_cast(pType)->isNative || types.Find(static_cast(pType)->TypeName) >= types.Size()) + continue; + + if (base->isArray() && !base->isStaticArray()) + { + auto arr = (void**)ScriptVar(sym->Key, nullptr); + const size_t count = static_cast(base)->ElementCount; + for (size_t i = 0u; i < count; ++i) + arr[i] = nullptr; + } + else if (base->isDynArray()) + { + static_cast*>(ScriptVar(sym->Key, nullptr))->Clear(); + } + else if (base->isMap()) + { + if (static_cast(base)->BackingClass == PMap::MAP_I32_PTR) + static_cast*>(ScriptVar(sym->Key, nullptr))->Clear(); + else + static_cast*>(ScriptVar(sym->Key, nullptr))->Clear(); + } + else + { + PointerVar(sym->Key) = nullptr; + } + } +} + +//========================================================================== +// +// +// +//========================================================================== + template static void MapPointerSubstitution(M *map, size_t &changed, DObject *old, DObject *notOld, const bool shouldSwap) { diff --git a/src/common/objects/dobject.h b/src/common/objects/dobject.h index 9925cb88e..3b414bdd9 100644 --- a/src/common/objects/dobject.h +++ b/src/common/objects/dobject.h @@ -245,6 +245,9 @@ public: template T*& PointerVar(FName field); inline int* IntArray(FName field); + // Make sure native data is wiped correctly since it has no read barriers. + void ClearNativePointerFields(const TArray& types); + // This is only needed for swapping out PlayerPawns and absolutely nothing else! virtual size_t PointerSubstitution (DObject *old, DObject *notOld, bool nullOnFail); diff --git a/src/common/utility/tarray.h b/src/common/utility/tarray.h index 3a6bd76e9..f1cfc4f7f 100644 --- a/src/common/utility/tarray.h +++ b/src/common/utility/tarray.h @@ -234,8 +234,8 @@ public: TArray (std::initializer_list list) { - Most = list.size; - Count = list.size; + Most = list.size(); + Count = list.size(); if (Count > 0) { diff --git a/src/g_level.cpp b/src/g_level.cpp index 9ea40c929..438d6ce29 100644 --- a/src/g_level.cpp +++ b/src/g_level.cpp @@ -1646,6 +1646,9 @@ void FLevelLocals::StartTravel () inv->UnlinkFromWorld (nullptr); inv->UnlinkBehaviorsFromLevel(); inv->DeleteAttachedLights(); + tid = inv->tid; + inv->SetTID(0); + inv->tid = tid; } } } @@ -1751,7 +1754,7 @@ int FLevelLocals::FinishTravel () pawn->LinkBehaviorsToLevel(); pawn->ClearInterpolation(); pawn->ClearFOVInterpolation(); - const int tid = pawn->tid; // Save TID (actor isn't linked into the hash chain yet) + int tid = pawn->tid; // Save TID (actor isn't linked into the hash chain yet) pawn->tid = 0; // Reset TID pawn->SetTID(tid); // Set TID (and link actor into the hash chain) pawn->SetState(pawn->SpawnState); @@ -1763,6 +1766,9 @@ int FLevelLocals::FinishTravel () inv->LinkToWorld (nullptr); P_FindFloorCeiling(inv, FFCF_ONLYSPAWNPOS); inv->LinkBehaviorsToLevel(); + tid = inv->tid; + inv->tid = 0; + inv->SetTID(tid); IFVIRTUALPTRNAME(inv, NAME_Inventory, Travelled) { diff --git a/src/namedef_custom.h b/src/namedef_custom.h index a8ae6414c..87675346b 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -568,6 +568,13 @@ xx(Offsety) xx(Texturetop) xx(Texturebottom) xx(Texturemiddle) +xx(SectorPortal) +xx(LinePortal) +xx(Vertex) +xx(Side) +xx(Line) +xx(SecPlane) +xx(F3DFloor) xx(Sector) xx(Heightfloor) xx(Heightceiling) diff --git a/src/p_setup.cpp b/src/p_setup.cpp index b698b3eec..070de05e4 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -293,9 +293,16 @@ void FLevelLocals::ClearLevelData(bool fullgc) auto it = GetThinkerIterator(NAME_None, STAT_TRAVELLING); for (AActor *actor = it.Next(); actor != nullptr; actor = it.Next()) { + actor->BlockingMobj = nullptr; actor->BlockingLine = actor->MovementBlockingLine = nullptr; actor->BlockingFloor = actor->BlockingCeiling = actor->Blocking3DFloor = nullptr; } + + // Make sure map data gets cleared appropriately so any leftover Objects aren't pointing + // towards anything invalid. + TArray fieldTypes = { NAME_SectorPortal, NAME_LinePortal, NAME_Vertex, NAME_Side, NAME_Line, NAME_SecPlane, NAME_F3DFloor, NAME_Sector }; + for (DObject* probe = GC::Root; probe != nullptr; probe = probe->ObjNext) + probe->ClearNativePointerFields(fieldTypes); } interpolator.ClearInterpolations(); // [RH] Nothing to interpolate on a fresh level. From c284aa366d2538bd436ac49679aad21b00a8b47e Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 8 Jul 2025 13:24:40 -0400 Subject: [PATCH 282/384] Use TArrayView instead of TArray for ClearNativePointerFields --- src/common/objects/dobject.cpp | 2 +- src/common/objects/dobject.h | 2 +- src/p_setup.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/objects/dobject.cpp b/src/common/objects/dobject.cpp index b9817a919..aaf7884bc 100644 --- a/src/common/objects/dobject.cpp +++ b/src/common/objects/dobject.cpp @@ -417,7 +417,7 @@ size_t DObject::PropagateMark() // //========================================================================== -void DObject::ClearNativePointerFields(const TArray& types) +void DObject::ClearNativePointerFields(const TArrayView& types) { auto cls = GetClass(); if (cls->VMType == nullptr) diff --git a/src/common/objects/dobject.h b/src/common/objects/dobject.h index 3b414bdd9..6aba45da3 100644 --- a/src/common/objects/dobject.h +++ b/src/common/objects/dobject.h @@ -246,7 +246,7 @@ public: inline int* IntArray(FName field); // Make sure native data is wiped correctly since it has no read barriers. - void ClearNativePointerFields(const TArray& types); + void ClearNativePointerFields(const TArrayView& types); // This is only needed for swapping out PlayerPawns and absolutely nothing else! virtual size_t PointerSubstitution (DObject *old, DObject *notOld, bool nullOnFail); diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 070de05e4..1c0b78bcf 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -300,9 +300,9 @@ void FLevelLocals::ClearLevelData(bool fullgc) // Make sure map data gets cleared appropriately so any leftover Objects aren't pointing // towards anything invalid. - TArray fieldTypes = { NAME_SectorPortal, NAME_LinePortal, NAME_Vertex, NAME_Side, NAME_Line, NAME_SecPlane, NAME_F3DFloor, NAME_Sector }; + FName fieldTypes[] = { NAME_SectorPortal, NAME_LinePortal, NAME_Vertex, NAME_Side, NAME_Line, NAME_SecPlane, NAME_F3DFloor, NAME_Sector }; for (DObject* probe = GC::Root; probe != nullptr; probe = probe->ObjNext) - probe->ClearNativePointerFields(fieldTypes); + probe->ClearNativePointerFields({ fieldTypes, std::size(fieldTypes) }); } interpolator.ClearInterpolations(); // [RH] Nothing to interpolate on a fresh level. From 23b69769e8b68fd038f051454ff04a70ea2dd0dc Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Fri, 11 Jul 2025 15:38:07 -0400 Subject: [PATCH 283/384] Fixed optionmenu regression Option menu would index into a negative item if natigating up and wrapping around in a menu with exactly N items where only N items can be displayed. --- .../zscript/engine/ui/menu/optionmenu.zs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index 834016238..bc0b850e9 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -276,7 +276,6 @@ class OptionMenu : Menu break; } - int previousSelection = mDesc.mSelectedItem; do { mDesc.mSelectedItem--; @@ -285,13 +284,14 @@ class OptionMenu : Menu mDesc.mSelectedItem = mDesc.mItems.Size() - 1; } } - while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != previousSelection); + while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != startedAt); - if (mDesc.mSelectedItem != previousSelection) + if (mDesc.mSelectedItem != startedAt) { int viewTop = mDesc.mScrollTop + mDesc.mScrollPos; + int lastItem = LastVisibleItem(); - if (previousSelection == FirstSelectable() && mDesc.mSelectedItem == LastVisibleItem()) + if (startedAt == FirstSelectable() && mDesc.mSelectedItem == lastItem) { int y = mDesc.mPosition; if (y <= 0) y = DrawCaption(mDesc.mTitle, -y, false); @@ -305,7 +305,6 @@ class OptionMenu : Menu } if (maxitems <= 0) maxitems = 1; - int lastItem = LastVisibleItem(); int newTopIndex = 0; int visibleItemsOnPage = 0; for (int i = lastItem; i >= 0; i--) @@ -322,11 +321,12 @@ class OptionMenu : Menu } mDesc.mScrollPos = newTopIndex - mDesc.mScrollTop; + if (mDesc.mScrollPos <= 0) mDesc.mScrollPos = 0; } else if (mDesc.mSelectedItem < viewTop) { int visibleLinesJumped = 0; - for (int i = previousSelection - 1; i >= mDesc.mSelectedItem; i--) + for (int i = startedAt - 1; i >= mDesc.mSelectedItem; i--) { if (mDesc.mItems[i].Visible()) { @@ -358,25 +358,24 @@ class OptionMenu : Menu break; } - int previousSelection = mDesc.mSelectedItem; do { mDesc.mSelectedItem++; if (mDesc.mSelectedItem >= mDesc.mItems.Size()) mDesc.mSelectedItem = 0; } - while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != previousSelection); + while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != startedAt); - if (mDesc.mSelectedItem != previousSelection) + if (mDesc.mSelectedItem != startedAt) { - if (previousSelection == LastVisibleItem()) + if (startedAt == LastVisibleItem()) { mDesc.mScrollPos = 0; } else if (mDesc.mSelectedItem > VisBottom && VisBottom != -1) { int visibleLinesJumped = 0; - for (int i = previousSelection + 1; i <= mDesc.mSelectedItem; i++) + for (int i = startedAt + 1; i <= mDesc.mSelectedItem; i++) { if (mDesc.mItems[i].Visible()) { From 2923afc99a1a630fac41979f405e6cf30480fe65 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sat, 12 Jul 2025 11:55:24 -0400 Subject: [PATCH 284/384] Refactored. Scrolling now jumps 2 items again --- .../zscript/engine/ui/menu/optionmenu.zs | 112 ++++++++++++++---- 1 file changed, 90 insertions(+), 22 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index bc0b850e9..07ce9b54c 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -223,11 +223,19 @@ class OptionMenu : Menu { if (ev.type == UIEvent.Type_WheelUp) { - return MenuEvent(MKEY_Up, false); + if (MenuMoveCursor(-2) != 0) + { + MenuSound ("menu/cursor"); + } + return true; } else if (ev.type == UIEvent.Type_WheelDown) { - return MenuEvent(MKEY_Down, false); + if (MenuMoveCursor(2) != 0) + { + MenuSound ("menu/cursor"); + } + return true; } else if (ev.type == UIEvent.Type_Char) { @@ -258,24 +266,58 @@ class OptionMenu : Menu //============================================================================= // - // + // Moves the cursor by the specified number of selectable items. + // Stops immediately after wrapping + // Returns number of lines moved // //============================================================================= - override bool MenuEvent (int mkey, bool fromcontroller) + int MenuMoveCursor(int items) { + // trivial case + if (items == 0) + { + return 0; + } + int startedAt = mDesc.mSelectedItem; - switch (mkey) + // trivial case + if (startedAt == -1) { - case MKEY_Up: - { - if (mDesc.mSelectedItem == -1) + if (items < 0) { mDesc.mSelectedItem = LastVisibleItem(); - break; + } + else + { + mDesc.mSelectedItem = FirstSelectable(); } + return mDesc.mSelectedItem - startedAt; + } + + if (items < -1) + { + // extended case up + do + { + MenuMoveCursor(-1); + items++; + } while (startedAt > mDesc.mSelectedItem && items < 0) + } + else if (items > 1) + { + // extended case down + do + { + MenuMoveCursor(1); + items--; + } while (startedAt <= mDesc.mSelectedItem && items > 0) + } + else if (items == -1) + { + // base case up do { mDesc.mSelectedItem--; @@ -284,7 +326,11 @@ class OptionMenu : Menu mDesc.mSelectedItem = mDesc.mItems.Size() - 1; } } - while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != startedAt); + while ( + !(mDesc.mItems[mDesc.mSelectedItem].Selectable() + && mDesc.mItems[mDesc.mSelectedItem].Visible()) + && mDesc.mSelectedItem != startedAt + ); if (mDesc.mSelectedItem != startedAt) { @@ -339,7 +385,10 @@ class OptionMenu : Menu while (visibleLinesToScroll < visibleLinesJumped && newScrollPos > 0) { newScrollPos--; - if ((newScrollPos + mDesc.mScrollTop) >= 0 && mDesc.mItems[newScrollPos + mDesc.mScrollTop].Visible()) + if ( + (newScrollPos + mDesc.mScrollTop) >= 0 + && mDesc.mItems[newScrollPos + mDesc.mScrollTop].Visible() + ) { visibleLinesToScroll++; } @@ -347,24 +396,21 @@ class OptionMenu : Menu mDesc.mScrollPos = newScrollPos; } } - break; } - - case MKEY_Down: + else if (items == 1) { - if (mDesc.mSelectedItem == -1) - { - mDesc.mSelectedItem = FirstSelectable(); - break; - } - + // base case down do { mDesc.mSelectedItem++; if (mDesc.mSelectedItem >= mDesc.mItems.Size()) mDesc.mSelectedItem = 0; } - while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible()) && mDesc.mSelectedItem != startedAt); + while ( + !(mDesc.mItems[mDesc.mSelectedItem].Selectable() + && mDesc.mItems[mDesc.mSelectedItem].Visible()) + && mDesc.mSelectedItem != startedAt + ); if (mDesc.mSelectedItem != startedAt) { @@ -395,9 +441,31 @@ class OptionMenu : Menu mDesc.mScrollPos = newScrollPos; } } - break; } + return mDesc.mSelectedItem - startedAt; + } + + //============================================================================= + // + // + // + //============================================================================= + + override bool MenuEvent (int mkey, bool fromcontroller) + { + int startedAt = mDesc.mSelectedItem; + + switch (mkey) + { + case MKEY_Up: + MenuMoveCursor(-1); + break; + + case MKEY_Down: + MenuMoveCursor(1); + break; + case MKEY_PageUp: if (mDesc.mScrollPos > 0) { From 37ba8a6a4d2f7b2093b7f6cc65e32cd36e36adac Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sat, 12 Jul 2025 13:40:51 -0400 Subject: [PATCH 285/384] Refactored. Scrolling no longer always moves cursor --- .../zscript/engine/ui/menu/optionmenu.zs | 198 +++++++++++------- 1 file changed, 118 insertions(+), 80 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index 07ce9b54c..f5856b1a8 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -97,6 +97,8 @@ class OptionMenu : Menu bool CanScrollUp; bool CanScrollDown; int VisBottom; + int LastRow; + int MaxItems; OptionMenuItem mFocusControl; //============================================================================= @@ -223,18 +225,12 @@ class OptionMenu : Menu { if (ev.type == UIEvent.Type_WheelUp) { - if (MenuMoveCursor(-2) != 0) - { - MenuSound ("menu/cursor"); - } + MenuScrollViewport(-2, true); return true; } else if (ev.type == UIEvent.Type_WheelDown) { - if (MenuMoveCursor(2) != 0) - { - MenuSound ("menu/cursor"); - } + MenuScrollViewport(2, true); return true; } else if (ev.type == UIEvent.Type_Char) @@ -274,16 +270,14 @@ class OptionMenu : Menu int MenuMoveCursor(int items) { - // trivial case - if (items == 0) + if (items == 0) // trivial case { return 0; } int startedAt = mDesc.mSelectedItem; - // trivial case - if (startedAt == -1) + if (startedAt == -1) // trivial case { if (items < 0) { @@ -297,27 +291,24 @@ class OptionMenu : Menu return mDesc.mSelectedItem - startedAt; } - if (items < -1) + if (items < -1) // extended case up { - // extended case up do { MenuMoveCursor(-1); items++; } while (startedAt > mDesc.mSelectedItem && items < 0) } - else if (items > 1) + else if (items > 1) // extended case down { - // extended case down do { MenuMoveCursor(1); items--; } while (startedAt <= mDesc.mSelectedItem && items > 0) } - else if (items == -1) + else if (items == -1) // base case up { - // base case up do { mDesc.mSelectedItem--; @@ -341,15 +332,12 @@ class OptionMenu : Menu { int y = mDesc.mPosition; if (y <= 0) y = DrawCaption(mDesc.mTitle, -y, false); - int lastrow = screen.GetHeight() - OptionHeight() * CleanYfac_1; - int rowheight = OptionMenuSettings.mLinespacing * CleanYfac_1 + 1; - - int maxitems = (lastrow - y) / rowheight + 1; - if (maxitems < RemainingVisibleItems(0)) + int maxItemsInternal = MaxItems; + if (maxItemsInternal < RemainingVisibleItems(0)) { - maxItems -= 2; + maxItemsInternal -= 2; } - if (maxitems <= 0) maxitems = 1; + if (maxItemsInternal <= 0) maxItemsInternal = 1; int newTopIndex = 0; int visibleItemsOnPage = 0; @@ -358,7 +346,7 @@ class OptionMenu : Menu if (mDesc.mItems[i].Visible()) { visibleItemsOnPage++; - if (visibleItemsOnPage >= maxitems) + if (visibleItemsOnPage >= maxItemsInternal) { newTopIndex = i; break; @@ -397,9 +385,8 @@ class OptionMenu : Menu } } } - else if (items == 1) + else if (items == 1) // base case down { - // base case down do { mDesc.mSelectedItem++; @@ -446,6 +433,99 @@ class OptionMenu : Menu return mDesc.mSelectedItem - startedAt; } + //============================================================================= + // + // Moves the viewport by the specified number of lines + // Keeps cursor in view if cursor is true + // Does not wrap + // Returns number of lines moved + // + //============================================================================= + + int MenuScrollViewport(int lines, bool cursor) + { + if (lines == 0) // trivial case + { + return 0; + } + + int startedAtScroll = mDesc.mScrollPos; + int startedAt = mDesc.mSelectedItem; + + if (lines > 0 && !CanScrollDown) // trivial case + { + return 0; + } + + if (lines < 0 && startedAtScroll <= 0) // trivial case + { + return 0; + } + + if (lines < 0) // base case up + { + mDesc.mScrollPos += lines; + + // backtrack if we overshot + if (mDesc.mScrollPos < 0) + { + mDesc.mScrollPos = 0; + } + + // ensure cursor is visible (if possible) + int lastItem = LastVisibleItem(); + int lastSelectable = -1; + int visible = 0; + for (int i = mDesc.mScrollPos; visible <= MaxItems && i < lastItem; i++) + { + if (!mDesc.mItems[i].Visible()) continue; + visible++; + if (!mDesc.mItems[i].Selectable()) continue; + lastSelectable = i; + } + if (lastSelectable != -1 && mDesc.mSelectedItem > lastSelectable) + { + mDesc.mSelectedItem = lastSelectable; + } + } + else if (lines > 0) // base case down + { + mDesc.mScrollPos += lines; + + // backtrack if we overshot + int visible = RemainingVisibleItems(mDesc.mScrollPos); + if (visible < MaxItems) + { + mDesc.mScrollPos = MAX(0, LastVisibleItem() - MaxItems); + visible = RemainingVisibleItems(mDesc.mScrollPos); + while (visible < MaxItems && mDesc.mScrollPos > 0) + { + if (mDesc.mItems[mDesc.mScrollPos].Visible()) + { + visible++; + } + mDesc.mScrollPos--; + } + } + + // ensure cursor is visible (if possible) + if (cursor && mDesc.mSelectedItem < mDesc.mScrollPos + mDesc.mScrollTop) + { + int temp = mDesc.mScrollPos + mDesc.mScrollTop; + for (int i = 0, v = 0; v < visible; i++) + { + if (!mDesc.mItems[temp + i].Visible()) continue; + v++; + if (!mDesc.mItems[temp + i].Selectable()) continue; + mDesc.mSelectedItem = temp + i; + break; + } + } + } + + return mDesc.mSelectedItem - startedAt; + } + //============================================================================= // // @@ -467,58 +547,11 @@ class OptionMenu : Menu break; case MKEY_PageUp: - if (mDesc.mScrollPos > 0) - { - mDesc.mScrollPos -= VisBottom - mDesc.mScrollPos - mDesc.mScrollTop; - if (mDesc.mScrollPos < 0) - { - mDesc.mScrollPos = 0; - } - if (mDesc.mSelectedItem != -1) - { - mDesc.mSelectedItem = mDesc.mScrollTop + mDesc.mScrollPos + 1; - while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible())) - { - if (++mDesc.mSelectedItem >= RemainingVisibleItems(0)) - { - mDesc.mSelectedItem = 0; - } - } - if (mDesc.mScrollPos > mDesc.mSelectedItem) - { - mDesc.mScrollPos = mDesc.mSelectedItem; - } - } - } + MenuScrollViewport(-MaxItems, true); break; case MKEY_PageDown: - if (CanScrollDown) - { - int pagesize = VisBottom - mDesc.mScrollPos - mDesc.mScrollTop; - if (pagesize > 0) mDesc.mScrollPos += pagesize; - else mDesc.mScrollPos++; - - if (mDesc.mScrollPos + mDesc.mScrollTop + pagesize > mDesc.mItems.Size()) - { - mDesc.mScrollPos = mDesc.mItems.Size() - mDesc.mScrollTop - pagesize; - } - if (mDesc.mSelectedItem != -1) - { - mDesc.mSelectedItem = mDesc.mScrollTop + mDesc.mScrollPos; - while (!(mDesc.mItems[mDesc.mSelectedItem].Selectable() && mDesc.mItems[mDesc.mSelectedItem].Visible())) - { - if (++mDesc.mSelectedItem >= mDesc.mItems.Size()) - { - mDesc.mSelectedItem = 0; - } - } - if (mDesc.mScrollPos > mDesc.mSelectedItem) - { - mDesc.mScrollPos = mDesc.mSelectedItem; - } - } - } + MenuScrollViewport(MaxItems, true); break; case MKEY_Enter: @@ -526,7 +559,10 @@ class OptionMenu : Menu { return true; } - // fall through to default + else + { + // fall through to default + } default: if (mDesc.mSelectedItem >= 0 && mDesc.mItems[mDesc.mSelectedItem].MenuEvent(mkey, fromcontroller)) return true; @@ -667,11 +703,13 @@ class OptionMenu : Menu int indent = GetIndent(); int ytop = y + mDesc.mScrollTop * 8 * CleanYfac_1; - int lastrow = screen.GetHeight() - OptionHeight() * CleanYfac_1; + LastRow = screen.GetHeight() - OptionHeight() * CleanYfac_1; + int rowheight = OptionMenuSettings.mLinespacing * CleanYfac_1 + 1; + MaxItems = (LastRow - y) / rowheight + 1; int i; int lastDrawnItemIndex = -1; - for (i = 0; i < mDesc.mItems.Size() && y <= lastrow; i++) + for (i = 0; i < mDesc.mItems.Size() && y <= LastRow; i++) { // Don't scroll the uppermost items if (i == mDesc.mScrollTop) From bef78dbe19533dfa70defb0450efcd5bad560505 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sat, 12 Jul 2025 13:41:44 -0400 Subject: [PATCH 286/384] Scrolling now makes sound if cursor moves --- wadsrc/static/zscript/engine/ui/menu/optionmenu.zs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index f5856b1a8..5dc8643a3 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -225,12 +225,18 @@ class OptionMenu : Menu { if (ev.type == UIEvent.Type_WheelUp) { - MenuScrollViewport(-2, true); + if (MenuScrollViewport(-2, true)) + { + MenuSound ("menu/cursor"); + } return true; } else if (ev.type == UIEvent.Type_WheelDown) { - MenuScrollViewport(2, true); + if (MenuScrollViewport(2, true)) + { + MenuSound ("menu/cursor"); + } return true; } else if (ev.type == UIEvent.Type_Char) From 32cf838ecf98550e079cf4a604951d4885488ca0 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sat, 12 Jul 2025 14:00:04 -0400 Subject: [PATCH 287/384] Added Home/End menu binds --- src/common/menu/menu.cpp | 3 +++ src/common/menu/menu.h | 2 ++ wadsrc/static/zscript/engine/ui/menu/menu.zs | 2 ++ wadsrc/static/zscript/engine/ui/menu/optionmenu.zs | 12 ++++++++++++ 4 files changed, 19 insertions(+) diff --git a/src/common/menu/menu.cpp b/src/common/menu/menu.cpp index b97118b98..7dfd33bd4 100644 --- a/src/common/menu/menu.cpp +++ b/src/common/menu/menu.cpp @@ -668,6 +668,9 @@ bool M_Responder (event_t *ev) case GK_BACKSPACE: mkey = MKEY_Clear; break; case GK_PGUP: mkey = MKEY_PageUp; break; case GK_PGDN: mkey = MKEY_PageDown; break; + case GK_HOME: mkey = MKEY_Home; break; + case GK_END: mkey = MKEY_End; break; + default: if (!keyup) { diff --git a/src/common/menu/menu.h b/src/common/menu/menu.h index a96e25929..c9596e8f3 100644 --- a/src/common/menu/menu.h +++ b/src/common/menu/menu.h @@ -28,6 +28,8 @@ enum EMenuKey MKEY_Right, MKEY_PageUp, MKEY_PageDown, + MKEY_Home, + MKEY_End, //----------------- Keys past here do not repeat. MKEY_Enter, MKEY_Back, // Back to previous menu diff --git a/wadsrc/static/zscript/engine/ui/menu/menu.zs b/wadsrc/static/zscript/engine/ui/menu/menu.zs index ed98a97e9..cb8756f3b 100644 --- a/wadsrc/static/zscript/engine/ui/menu/menu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/menu.zs @@ -122,6 +122,8 @@ class Menu : Object native ui version("2.4") MKEY_Right, MKEY_PageUp, MKEY_PageDown, + MKEY_Home, + MKEY_End, MKEY_Enter, MKEY_Back, MKEY_Clear, diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index 5dc8643a3..b45b160ad 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -560,6 +560,18 @@ class OptionMenu : Menu MenuScrollViewport(MaxItems, true); break; + case MKEY_Home: + MenuScrollViewport(-mDesc.mItems.Size(), true); + mDesc.mSelectedItem = -1; + MenuMoveCursor(1); + break; + + case MKEY_End: + MenuScrollViewport(mDesc.mItems.Size(), true); + mDesc.mSelectedItem = -1; + MenuMoveCursor(-1); + break; + case MKEY_Enter: if (mDesc.mSelectedItem >= 0 && mDesc.mItems[mDesc.mSelectedItem].Activate()) { From 6b22be3e28435b0e157c526c83aa0a1e9511354b Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Fri, 11 Jul 2025 14:29:52 -0400 Subject: [PATCH 288/384] Expose visible flag to menudef --- .../zscript/engine/ui/menu/optionmenuitems.zs | 154 +++++++++++++++--- 1 file changed, 131 insertions(+), 23 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs index 00a9b23f6..8e4009acd 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs @@ -36,16 +36,26 @@ class OptionMenuItem : MenuItemBase { String mLabel; bool mCentered; - CVar mGrayCheck; - int mGrayCheckVal; + CVar mGrayCheck; int mGrayCheckVal; + CVar mHiddenCheck; int mHiddenCheckVal; - protected void Init(String label, String command, bool center = false, CVar graycheck = null, int graycheckVal = 0) + protected void Init( + String label, + String command, + bool center = false, + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { Super.Init(0, 0, command); mLabel = label; mCentered = center; mGrayCheck = graycheck; mGrayCheckVal = graycheckVal; + mHiddenCheck = hiddencheck; + mHiddenCheckVal = hiddencheckVal; } protected void drawText(int x, int y, int color, String text, bool grayed = false) @@ -80,6 +90,11 @@ class OptionMenuItem : MenuItemBase return mGrayCheck != null && mGrayCheck.GetInt() == mGrayCheckVal; } + override bool Visible() + { + return mHiddenCheck == null || mHiddenCheck.GetInt() != mHiddenCheckVal; + } + override bool Selectable() { return true; @@ -265,9 +280,18 @@ class OptionMenuItemOptionBase : OptionMenuItem const OP_VALUES = 0x11001; - protected void Init(String label, Name command, Name values, CVar graycheck, int center, int graycheckVal = 0) + protected void Init( + String label, + Name command, + Name values, + CVar graycheck, + int center, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, false, graycheck, graycheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mValues = values; mCenter = center; } @@ -363,9 +387,18 @@ class OptionMenuItemOption : OptionMenuItemOptionBase private static native void SetCVarDescription(CVar cv, String label); - OptionMenuItemOption Init(String label, Name command, Name values, CVar graycheck = null, int center = 0, int graycheckVal = 0) + OptionMenuItemOption Init( + String label, + Name command, + Name values, + CVar graycheck = null, + int center = 0, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, values, graycheck, center, graycheckVal); + Super.Init(label, command, values, graycheck, center, graycheckVal, hiddencheck, hiddencheckVal); mCVar = CVar.FindCVar(mAction); if (mCVar) SetCVarDescription(mCVar, label); return self; @@ -747,9 +780,20 @@ class OptionMenuSliderBase : OptionMenuItem int mDrawX; int mSliderShort; - protected void Init(String label, double min, double max, double step, int showval, Name command = 'none', CVar graycheck = NULL, int graycheckVal = 0) + protected void Init( + String label, + double min, + double max, + double step, + int showval, + Name command = 'none', + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, false, graycheck, graycheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mMin = min; mMax = max; mStep = step; @@ -903,9 +947,20 @@ class OptionMenuItemSlider : OptionMenuSliderBase { CVar mCVar; - OptionMenuItemSlider Init(String label, Name command, double min, double max, double step, int showval = 1, CVar graycheck = NULL, int graycheckVal = 0) + OptionMenuItemSlider Init( + String label, + Name command, + double min, + double max, + double step, + int showval = 1, + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, min, max, step, showval, command, graycheck, graycheckVal); + Super.Init(label, min, max, step, showval, command, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mCVar =CVar.FindCVar(command); return self; } @@ -943,9 +998,16 @@ class OptionMenuItemColorPicker : OptionMenuItem const CPF_RESET = 0x20001; - OptionMenuItemColorPicker Init(String label, Name command, CVar graycheck = null, int graycheckVal = 0) + OptionMenuItemColorPicker Init( + String label, + Name command, + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, false, graycheck, graycheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); CVar cv = CVar.FindCVar(command); if (cv != null && cv.GetRealType() != CVar.CVAR_Color) cv = null; mCVar = cv; @@ -1022,9 +1084,16 @@ class OptionMenuFieldBase : OptionMenuItem { CVar mCVar; - void Init (String label, Name command, CVar graycheck = null, int graycheckVal = 0) + void Init ( + String label, + Name command, + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, false, graycheck, graycheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mCVar = CVar.FindCVar(mAction); } @@ -1085,9 +1154,16 @@ class OptionMenuItemTextField : OptionMenuFieldBase { TextEnterMenu mEnter; - OptionMenuItemTextField Init (String label, Name command, CVar graycheck = null, int graycheckVal = 0) + OptionMenuItemTextField Init ( + String label, + Name command, + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, graycheck, graycheckVal); + Super.Init(label, command, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mEnter = null; return self; } @@ -1157,9 +1233,19 @@ class OptionMenuItemTextField : OptionMenuFieldBase class OptionMenuItemNumberField : OptionMenuFieldBase { - OptionMenuItemNumberField Init (String label, Name command, float minimum = 0, float maximum = 100, float step = 1, CVar graycheck = null, int graycheckVal = 0) + OptionMenuItemNumberField Init ( + String label, + Name command, + float minimum = 0, + float maximum = 100, + float step = 1, + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, graycheck, graycheckVal); + Super.Init(label, command, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mMinimum = min(minimum, maximum); mMaximum = max(minimum, maximum); mStep = max(1, step); @@ -1217,9 +1303,21 @@ class OptionMenuItemScaleSlider : OptionMenuItemSlider String TextNegOne; int mClickVal; - OptionMenuItemScaleSlider Init(String label, Name command, double min, double max, double step, String zero, String negone = "", CVar graycheck = null, int graycheckVal = 0) + OptionMenuItemScaleSlider Init( + String label, + Name command, + double min, + double max, + double step, + String zero, + String negone = "", + CVar graycheck = null, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, min, max, step, 0, graycheck, graycheckVal); + Super.Init(label, command, min, max, step, 0, graycheck, graycheckVal, hiddencheck, hiddencheckVal); mCVar =CVar.FindCVar(command); TextZero = zero; TextNEgOne = negone; @@ -1285,9 +1383,19 @@ class OptionMenuItemFlagOption : OptionMenuItemOption { int mBitShift; - OptionMenuItemFlagOption Init(String label, Name command, Name values, int bitShift, CVar greycheck = null, int center = 0, int graycheckVal = 0) + OptionMenuItemFlagOption Init( + String label, + Name command, + Name values, + int bitShift, + CVar greycheck = null, + int center = 0, + int graycheckVal = 0, + CVar hiddencheck = null, + int hiddencheckVal = 0 + ) { - Super.Init(label, command, values, greycheck, center, graycheckVal); + Super.Init(label, command, values, greycheck, center, graycheckVal, hiddencheck, hiddencheckVal); mBitShift = bitShift; return self; From b40af0c923874efa9cb00dfc6e2f80134eaa8e89 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Sat, 12 Jul 2025 11:18:22 -0400 Subject: [PATCH 289/384] Add hideInsteadOfGraying bool --- .../zscript/engine/ui/menu/optionmenuitems.zs | 65 ++++++++----------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs index 8e4009acd..f08f1b9e0 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs @@ -36,8 +36,9 @@ class OptionMenuItem : MenuItemBase { String mLabel; bool mCentered; - CVar mGrayCheck; int mGrayCheckVal; - CVar mHiddenCheck; int mHiddenCheckVal; + CVar mGrayCheck; + int mGrayCheckVal; + bool mHideInsteadOfGraying; protected void Init( String label, @@ -45,8 +46,7 @@ class OptionMenuItem : MenuItemBase bool center = false, CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { Super.Init(0, 0, command); @@ -54,8 +54,7 @@ class OptionMenuItem : MenuItemBase mCentered = center; mGrayCheck = graycheck; mGrayCheckVal = graycheckVal; - mHiddenCheck = hiddencheck; - mHiddenCheckVal = hiddencheckVal; + mHideInsteadOfGraying = hideInsteadOfGraying; } protected void drawText(int x, int y, int color, String text, bool grayed = false) @@ -87,12 +86,12 @@ class OptionMenuItem : MenuItemBase virtual bool IsGrayed() { - return mGrayCheck != null && mGrayCheck.GetInt() == mGrayCheckVal; + return !mHideInsteadOfGraying && mGrayCheck != null && mGrayCheck.GetInt() == mGrayCheckVal; } override bool Visible() { - return mHiddenCheck == null || mHiddenCheck.GetInt() != mHiddenCheckVal; + return !(mHideInsteadOfGraying && mGrayCheck != null && mGrayCheck.GetInt() == mGrayCheckVal); } override bool Selectable() @@ -287,11 +286,10 @@ class OptionMenuItemOptionBase : OptionMenuItem CVar graycheck, int center, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hideInsteadOfGraying); mValues = values; mCenter = center; } @@ -394,11 +392,10 @@ class OptionMenuItemOption : OptionMenuItemOptionBase CVar graycheck = null, int center = 0, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, values, graycheck, center, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, values, graycheck, center, graycheckVal, hideInsteadOfGraying); mCVar = CVar.FindCVar(mAction); if (mCVar) SetCVarDescription(mCVar, label); return self; @@ -789,11 +786,10 @@ class OptionMenuSliderBase : OptionMenuItem Name command = 'none', CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hideInsteadOfGraying); mMin = min; mMax = max; mStep = step; @@ -956,11 +952,10 @@ class OptionMenuItemSlider : OptionMenuSliderBase int showval = 1, CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, min, max, step, showval, command, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, min, max, step, showval, command, graycheck, graycheckVal, hideInsteadOfGraying); mCVar =CVar.FindCVar(command); return self; } @@ -1003,11 +998,10 @@ class OptionMenuItemColorPicker : OptionMenuItem Name command, CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hideInsteadOfGraying); CVar cv = CVar.FindCVar(command); if (cv != null && cv.GetRealType() != CVar.CVAR_Color) cv = null; mCVar = cv; @@ -1089,11 +1083,10 @@ class OptionMenuFieldBase : OptionMenuItem Name command, CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, false, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, false, graycheck, graycheckVal, hideInsteadOfGraying); mCVar = CVar.FindCVar(mAction); } @@ -1159,11 +1152,10 @@ class OptionMenuItemTextField : OptionMenuFieldBase Name command, CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, graycheck, graycheckVal, hideInsteadOfGraying); mEnter = null; return self; } @@ -1241,11 +1233,10 @@ class OptionMenuItemNumberField : OptionMenuFieldBase float step = 1, CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, graycheck, graycheckVal, hideInsteadOfGraying); mMinimum = min(minimum, maximum); mMaximum = max(minimum, maximum); mStep = max(1, step); @@ -1313,11 +1304,10 @@ class OptionMenuItemScaleSlider : OptionMenuItemSlider String negone = "", CVar graycheck = null, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, min, max, step, 0, graycheck, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, min, max, step, 0, graycheck, graycheckVal, hideInsteadOfGraying); mCVar =CVar.FindCVar(command); TextZero = zero; TextNEgOne = negone; @@ -1391,11 +1381,10 @@ class OptionMenuItemFlagOption : OptionMenuItemOption CVar greycheck = null, int center = 0, int graycheckVal = 0, - CVar hiddencheck = null, - int hiddencheckVal = 0 + bool hideInsteadOfGraying = false ) { - Super.Init(label, command, values, greycheck, center, graycheckVal, hiddencheck, hiddencheckVal); + Super.Init(label, command, values, greycheck, center, graycheckVal, hideInsteadOfGraying); mBitShift = bitShift; return self; From f513e3de99040148f7d27558e91a996eaa937f94 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 10 Jul 2025 17:24:26 -0400 Subject: [PATCH 290/384] Moved stream reading/writing protocol to common Allow this to be used anywhere within the engine, especially for internal network handling. --- src/CMakeLists.txt | 1 + src/common/engine/i_protocol.cpp | 399 +++++++++++++++++++++++++++++++ src/common/engine/i_protocol.h | 77 ++++++ src/d_protocol.cpp | 362 ---------------------------- src/d_protocol.h | 38 +-- 5 files changed, 478 insertions(+), 399 deletions(-) create mode 100644 src/common/engine/i_protocol.cpp create mode 100644 src/common/engine/i_protocol.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 012c1f10e..e305535c4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1102,6 +1102,7 @@ set (PCH_SOURCES common/engine/palettecontainer.cpp common/engine/stringtable.cpp common/engine/i_net.cpp + common/engine/i_protocol.cpp common/engine/i_interface.cpp common/engine/renderstyle.cpp common/engine/v_colortables.cpp diff --git a/src/common/engine/i_protocol.cpp b/src/common/engine/i_protocol.cpp new file mode 100644 index 000000000..50497527d --- /dev/null +++ b/src/common/engine/i_protocol.cpp @@ -0,0 +1,399 @@ +/* +** i_protocol.cpp +** Basic network packet creation routines and simple IFF parsing +** +**--------------------------------------------------------------------------- +** Copyright 1998-2006 Randy Heit +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#include "i_protocol.h" +#include "engineerrors.h" +#include "cmdlib.h" + +// Unchecked stream functions. +// Use the checked versions instead unless you're checking the stream size yourself! + +char* UncheckedReadString(uint8_t** stream) +{ + char* string = *((char**)stream); + + *stream += strlen(string) + 1; + return copystring(string); +} + +const char* UncheckedReadStringConst(uint8_t** stream) +{ + const char* string = *((const char**)stream); + *stream += strlen(string) + 1; + return string; +} + +uint8_t UncheckedReadInt8(uint8_t** stream) +{ + uint8_t v = **stream; + *stream += 1; + return v; +} + +int16_t UncheckedReadInt16(uint8_t** stream) +{ + int16_t v = (((*stream)[0]) << 8) | (((*stream)[1])); + *stream += 2; + return v; +} + +int32_t UncheckedReadInt32(uint8_t** stream) +{ + int32_t v = (((*stream)[0]) << 24) | (((*stream)[1]) << 16) | (((*stream)[2]) << 8) | (((*stream)[3])); + *stream += 4; + return v; +} + +int64_t UncheckedReadInt64(uint8_t** stream) +{ + int64_t v = (int64_t((*stream)[0]) << 56) | (int64_t((*stream)[1]) << 48) | (int64_t((*stream)[2]) << 40) | (int64_t((*stream)[3]) << 32) + | (int64_t((*stream)[4]) << 24) | (int64_t((*stream)[5]) << 16) | (int64_t((*stream)[6]) << 8) | (int64_t((*stream)[7])); + *stream += 8; + return v; +} + +float UncheckedReadFloat(uint8_t** stream) +{ + union + { + int32_t i; + float f; + } fakeint; + fakeint.i = UncheckedReadInt32(stream); + return fakeint.f; +} + +double UncheckedReadDouble(uint8_t** stream) +{ + union + { + int64_t i; + double f; + } fakeint; + fakeint.i = UncheckedReadInt64(stream); + return fakeint.f; +} + +void UncheckedWriteString(const char* string, uint8_t** stream) +{ + char* p = *((char**)stream); + + while (*string) { + *p++ = *string++; + } + + *p++ = 0; + *stream = (uint8_t*)p; +} + +void UncheckedWriteInt8(uint8_t v, uint8_t** stream) +{ + **stream = v; + *stream += 1; +} + +void UncheckedWriteInt16(int16_t v, uint8_t** stream) +{ + (*stream)[0] = v >> 8; + (*stream)[1] = v & 255; + *stream += 2; +} + +void UncheckedWriteInt32(int32_t v, uint8_t** stream) +{ + (*stream)[0] = v >> 24; + (*stream)[1] = (v >> 16) & 255; + (*stream)[2] = (v >> 8) & 255; + (*stream)[3] = v & 255; + *stream += 4; +} + +void UncheckedWriteInt64(int64_t v, uint8_t** stream) +{ + (*stream)[0] = v >> 56; + (*stream)[1] = (v >> 48) & 255; + (*stream)[2] = (v >> 40) & 255; + (*stream)[3] = (v >> 32) & 255; + (*stream)[4] = (v >> 24) & 255; + (*stream)[5] = (v >> 16) & 255; + (*stream)[6] = (v >> 8) & 255; + (*stream)[7] = v & 255; + *stream += 8; +} + +void UncheckedWriteFloat(float v, uint8_t** stream) +{ + union + { + int32_t i; + float f; + } fakeint; + fakeint.f = v; + UncheckedWriteInt32(fakeint.i, stream); +} + +void UncheckedWriteDouble(double v, uint8_t** stream) +{ + union + { + int64_t i; + double f; + } fakeint; + fakeint.f = v; + UncheckedWriteInt64(fakeint.i, stream); +} + +void AdvanceStream(TArrayView& stream, size_t bytes) +{ + assert(bytes <= stream.Size()); + stream = TArrayView(stream.Data() + bytes, stream.Size() - bytes); +} + +// Checked stream functions + +char* ReadString(TArrayView& stream) +{ + char* string = (char*)stream.Data(); + size_t len = strnlen(string, stream.Size()); + if (len == stream.Size()) + { + I_Error("Attempted to read past end of stream"); + } + AdvanceStream(stream, len + 1); + return copystring(string); +} + +const char* ReadStringConst(TArrayView& stream) +{ + const char* string = (const char*)stream.Data(); + size_t len = strnlen(string, stream.Size()); + if (len == stream.Size()) + { + I_Error("Attempted to read past end of stream"); + } + AdvanceStream(stream, len + 1); + return string; +} + +void ReadBytes(TArrayView& dst, TArrayView& stream) +{ + if (dst.Size() > stream.Size()) + { + I_Error("Attempted to read past end of stream"); + } + if (dst.Size()) + { + memcpy(dst.Data(), stream.Data(), dst.Size()); + AdvanceStream(stream, dst.Size()); + } +} + +uint8_t ReadInt8(TArrayView& stream) +{ + if (stream.Size() < 1) + { + I_Error("Attempted to read past end of stream"); + } + uint8_t v = stream[0]; + AdvanceStream(stream, 1); + return v; +} + +int16_t ReadInt16(TArrayView& stream) +{ + if (stream.Size() < 2) + { + I_Error("Attempted to read past end of stream"); + } + int16_t v = ((stream[0]) << 8) | stream[1]; + AdvanceStream(stream, 2); + return v; +} + +int32_t ReadInt32(TArrayView& stream) +{ + if (stream.Size() < 4) + { + I_Error("Attempted to read past end of stream"); + } + int32_t v = (stream[0] << 24) | (stream[1] << 16) | (stream[2] << 8) | stream[3]; + AdvanceStream(stream, 4); + return v; +} + +int64_t ReadInt64(TArrayView& stream) +{ + if (stream.Size() < 8) + { + I_Error("Attempted to read past end of stream"); + } + int64_t v = (int64_t(stream[0]) << 56) | (int64_t(stream[1]) << 48) | (int64_t(stream[2]) << 40) | (int64_t(stream[3]) << 32) + | (int64_t(stream[4]) << 24) | (int64_t(stream[5]) << 16) | (int64_t(stream[6]) << 8) | int64_t(stream[7]); + AdvanceStream(stream, 8); + return v; +} + +float ReadFloat(TArrayView& stream) +{ + union + { + int32_t i; + float f; + } fakeint; + fakeint.i = ReadInt32(stream); + return fakeint.f; +} + +double ReadDouble(TArrayView& stream) +{ + union + { + int64_t i; + double f; + } fakeint; + fakeint.i = ReadInt64(stream); + return fakeint.f; +} + +void WriteString(const char* string, TArrayView& stream) +{ + char* p = (char*)stream.Data(); + unsigned int remaining = stream.Size(); + + while (*string) { + if (remaining-- == 0) + { + I_Error("Attempted to write past end of stream"); + } + *p++ = *string++; + } + + if (remaining == 0) + { + I_Error("Attempted to write past end of stream"); + } + *p++ = 0; + AdvanceStream(stream, p - (char*)stream.Data()); +} + +void WriteBytes(const TArrayView& source, TArrayView& stream) +{ + if (source.Size() > stream.Size()) + { + I_Error("Attempted to write past end of stream"); + } + if (source.Size()) + { + memcpy(stream.Data(), source.Data(), source.Size()); + AdvanceStream(stream, source.Size()); + } +} + +void WriteFString(FString& string, TArrayView& stream) +{ + WriteBytes(string.GetTArrayView(), stream); +} + +void WriteInt8(uint8_t v, TArrayView& stream) +{ + if (stream.Size() < 1) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v; + AdvanceStream(stream, 1); +} + +void WriteInt16(int16_t v, TArrayView& stream) +{ + if (stream.Size() < 2) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v >> 8; + stream[1] = v & 255; + AdvanceStream(stream, 2); +} + +void WriteInt32(int32_t v, TArrayView& stream) +{ + if (stream.Size() < 4) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v >> 24; + stream[1] = (v >> 16) & 255; + stream[2] = (v >> 8) & 255; + stream[3] = v & 255; + AdvanceStream(stream, 4); +} + +void WriteInt64(int64_t v, TArrayView& stream) +{ + if (stream.Size() < 8) + { + I_Error("Attempted to write past end of stream"); + } + stream[0] = v >> 56; + stream[1] = (v >> 48) & 255; + stream[2] = (v >> 40) & 255; + stream[3] = (v >> 32) & 255; + stream[4] = (v >> 24) & 255; + stream[5] = (v >> 16) & 255; + stream[6] = (v >> 8) & 255; + stream[7] = v & 255; + AdvanceStream(stream, 8); +} + +void WriteFloat(float v, TArrayView& stream) +{ + union + { + int32_t i; + float f; + } fakeint; + fakeint.f = v; + WriteInt32(fakeint.i, stream); +} + +void WriteDouble(double v, TArrayView& stream) +{ + union + { + int64_t i; + double f; + } fakeint; + fakeint.f = v; + WriteInt64(fakeint.i, stream); +} diff --git a/src/common/engine/i_protocol.h b/src/common/engine/i_protocol.h new file mode 100644 index 000000000..15fb485da --- /dev/null +++ b/src/common/engine/i_protocol.h @@ -0,0 +1,77 @@ +/* +** i_protocol.h +** +**--------------------------------------------------------------------------- +** Copyright 1998-2006 Randy Heit +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#ifndef __I_PROTOCOL_H__ +#define __I_PROTOCOL_H__ + +#include "tarray.h" +#include "zstring.h" + +uint8_t UncheckedReadInt8(uint8_t** stream); +int16_t UncheckedReadInt16(uint8_t** stream); +int32_t UncheckedReadInt32(uint8_t** stream); +int64_t UncheckedReadInt64(uint8_t** stream); +float UncheckedReadFloat(uint8_t** stream); +double UncheckedReadDouble(uint8_t** stream); +char* UncheckedReadString(uint8_t** stream); +const char* UncheckedReadStringConst(uint8_t** stream); +void UncheckedWriteInt8(uint8_t val, uint8_t** stream); +void UncheckedWriteInt16(int16_t val, uint8_t** stream); +void UncheckedWriteInt32(int32_t val, uint8_t** stream); +void UncheckedWriteInt64(int64_t val, uint8_t** stream); +void UncheckedWriteFloat(float val, uint8_t** stream); +void UncheckedWriteDouble(double val, uint8_t** stream); +void UncheckedWriteString(const char* string, uint8_t** stream); + +void AdvanceStream(TArrayView& stream, size_t bytes); + +uint8_t ReadInt8(TArrayView& stream); +int16_t ReadInt16(TArrayView& stream); +int32_t ReadInt32(TArrayView& stream); +int64_t ReadInt64(TArrayView& stream); +float ReadFloat(TArrayView& stream); +double ReadDouble(TArrayView& stream); +char* ReadString(TArrayView& stream); +const char* ReadStringConst(TArrayView& stream); +void ReadBytes(TArrayView& dst, TArrayView& stream); +void WriteInt8(uint8_t val, TArrayView& stream); +void WriteInt16(int16_t val, TArrayView& stream); +void WriteInt32(int32_t val, TArrayView& stream); +void WriteInt64(int64_t val, TArrayView& stream); +void WriteFloat(float val, TArrayView& stream); +void WriteDouble(double val, TArrayView& stream); +void WriteString(const char* string, TArrayView& stream); +void WriteBytes(const TArrayView& source, TArrayView& stream); +void WriteFString(FString& string, TArrayView& stream); + +#endif //__I_PROTOCOL_H__ diff --git a/src/d_protocol.cpp b/src/d_protocol.cpp index de56e2ba3..fe7b397a6 100644 --- a/src/d_protocol.cpp +++ b/src/d_protocol.cpp @@ -38,368 +38,6 @@ #include "cmdlib.h" #include "serializer.h" -// Unchecked stream functions. -// Use the checked versions instead unless you're checking the stream size yourself! - -char* UncheckedReadString(uint8_t **stream) -{ - char *string = *((char **)stream); - - *stream += strlen(string) + 1; - return copystring(string); -} - -const char* UncheckedReadStringConst(uint8_t **stream) -{ - const char *string = *((const char **)stream); - *stream += strlen(string) + 1; - return string; -} - -uint8_t UncheckedReadInt8(uint8_t **stream) -{ - uint8_t v = **stream; - *stream += 1; - return v; -} - -int16_t UncheckedReadInt16(uint8_t **stream) -{ - int16_t v = (((*stream)[0]) << 8) | (((*stream)[1])); - *stream += 2; - return v; -} - -int32_t UncheckedReadInt32(uint8_t **stream) -{ - int32_t v = (((*stream)[0]) << 24) | (((*stream)[1]) << 16) | (((*stream)[2]) << 8) | (((*stream)[3])); - *stream += 4; - return v; -} - -int64_t UncheckedReadInt64(uint8_t **stream) -{ - int64_t v = (int64_t((*stream)[0]) << 56) | (int64_t((*stream)[1]) << 48) | (int64_t((*stream)[2]) << 40) | (int64_t((*stream)[3]) << 32) - | (int64_t((*stream)[4]) << 24) | (int64_t((*stream)[5]) << 16) | (int64_t((*stream)[6]) << 8) | (int64_t((*stream)[7])); - *stream += 8; - return v; -} - -float UncheckedReadFloat(uint8_t **stream) -{ - union - { - int32_t i; - float f; - } fakeint; - fakeint.i = UncheckedReadInt32(stream); - return fakeint.f; -} - -double UncheckedReadDouble(uint8_t **stream) -{ - union - { - int64_t i; - double f; - } fakeint; - fakeint.i = UncheckedReadInt64(stream); - return fakeint.f; -} - -void UncheckedWriteString(const char *string, uint8_t **stream) -{ - char *p = *((char **)stream); - - while (*string) { - *p++ = *string++; - } - - *p++ = 0; - *stream = (uint8_t *)p; -} - -void UncheckedWriteInt8(uint8_t v, uint8_t **stream) -{ - **stream = v; - *stream += 1; -} - -void UncheckedWriteInt16(int16_t v, uint8_t **stream) -{ - (*stream)[0] = v >> 8; - (*stream)[1] = v & 255; - *stream += 2; -} - -void UncheckedWriteInt32(int32_t v, uint8_t **stream) -{ - (*stream)[0] = v >> 24; - (*stream)[1] = (v >> 16) & 255; - (*stream)[2] = (v >> 8) & 255; - (*stream)[3] = v & 255; - *stream += 4; -} - -void UncheckedWriteInt64(int64_t v, uint8_t **stream) -{ - (*stream)[0] = v >> 56; - (*stream)[1] = (v >> 48) & 255; - (*stream)[2] = (v >> 40) & 255; - (*stream)[3] = (v >> 32) & 255; - (*stream)[4] = (v >> 24) & 255; - (*stream)[5] = (v >> 16) & 255; - (*stream)[6] = (v >> 8) & 255; - (*stream)[7] = v & 255; - *stream += 8; -} - -void UncheckedWriteFloat(float v, uint8_t **stream) -{ - union - { - int32_t i; - float f; - } fakeint; - fakeint.f = v; - UncheckedWriteInt32(fakeint.i, stream); -} - -void UncheckedWriteDouble(double v, uint8_t **stream) -{ - union - { - int64_t i; - double f; - } fakeint; - fakeint.f = v; - UncheckedWriteInt64(fakeint.i, stream); -} - -void AdvanceStream(TArrayView& stream, size_t bytes) -{ - assert(bytes <= stream.Size()); - stream = TArrayView(stream.Data() + bytes, stream.Size() - bytes); -} - -// Checked stream functions - -char* ReadString(TArrayView& stream) -{ - char *string = (char*)stream.Data(); - size_t len = strnlen(string, stream.Size()); - if (len == stream.Size()) - { - I_Error("Attempted to read past end of stream"); - } - AdvanceStream(stream, len + 1); - return copystring(string); -} - -const char* ReadStringConst(TArrayView& stream) -{ - const char* string = (const char*)stream.Data(); - size_t len = strnlen(string, stream.Size()); - if (len == stream.Size()) - { - I_Error("Attempted to read past end of stream"); - } - AdvanceStream(stream, len + 1); - return string; -} - -void ReadBytes(TArrayView& dst, TArrayView& stream) -{ - if (dst.Size() > stream.Size()) - { - I_Error("Attempted to read past end of stream"); - } - if (dst.Size()) - { - memcpy(dst.Data(), stream.Data(), dst.Size()); - AdvanceStream(stream, dst.Size()); - } -} - -uint8_t ReadInt8(TArrayView& stream) -{ - if (stream.Size() < 1) - { - I_Error("Attempted to read past end of stream"); - } - uint8_t v = stream[0]; - AdvanceStream(stream, 1); - return v; -} - -int16_t ReadInt16(TArrayView& stream) -{ - if (stream.Size() < 2) - { - I_Error("Attempted to read past end of stream"); - } - int16_t v = ((stream[0]) << 8) | stream[1]; - AdvanceStream(stream, 2); - return v; -} - -int32_t ReadInt32(TArrayView& stream) -{ - if (stream.Size() < 4) - { - I_Error("Attempted to read past end of stream"); - } - int32_t v = (stream[0] << 24) | (stream[1] << 16) | (stream[2] << 8) | stream[3]; - AdvanceStream(stream, 4); - return v; -} - -int64_t ReadInt64(TArrayView& stream) -{ - if (stream.Size() < 8) - { - I_Error("Attempted to read past end of stream"); - } - int64_t v = (int64_t(stream[0]) << 56) | (int64_t(stream[1]) << 48) | (int64_t(stream[2]) << 40) | (int64_t(stream[3]) << 32) - | (int64_t(stream[4]) << 24) | (int64_t(stream[5]) << 16) | (int64_t(stream[6]) << 8) | int64_t(stream[7]); - AdvanceStream(stream, 8); - return v; -} - -float ReadFloat(TArrayView& stream) -{ - union - { - int32_t i; - float f; - } fakeint; - fakeint.i = ReadInt32 (stream); - return fakeint.f; -} - -double ReadDouble(TArrayView& stream) -{ - union - { - int64_t i; - double f; - } fakeint; - fakeint.i = ReadInt64(stream); - return fakeint.f; -} - -void WriteString(const char *string, TArrayView& stream) -{ - char *p = (char *)stream.Data(); - unsigned int remaining = stream.Size(); - - while (*string) { - if (remaining-- == 0) - { - I_Error("Attempted to write past end of stream"); - } - *p++ = *string++; - } - - if (remaining == 0) - { - I_Error("Attempted to write past end of stream"); - } - *p++ = 0; - AdvanceStream(stream, p - (char*)stream.Data()); -} - -void WriteBytes(const TArrayView& source, TArrayView& stream) -{ - if (source.Size() > stream.Size()) - { - I_Error("Attempted to write past end of stream"); - } - if (source.Size()) - { - memcpy(stream.Data(), source.Data(), source.Size()); - AdvanceStream(stream, source.Size()); - } -} - -void WriteFString(FString& string, TArrayView& stream) -{ - WriteBytes(string.GetTArrayView(), stream); -} - -void WriteInt8(uint8_t v, TArrayView& stream) -{ - if (stream.Size() < 1) - { - I_Error("Attempted to write past end of stream"); - } - stream[0] = v; - AdvanceStream(stream, 1); -} - -void WriteInt16(int16_t v, TArrayView& stream) -{ - if (stream.Size() < 2) - { - I_Error("Attempted to write past end of stream"); - } - stream[0] = v >> 8; - stream[1] = v & 255; - AdvanceStream(stream, 2); -} - -void WriteInt32(int32_t v, TArrayView& stream) -{ - if (stream.Size() < 4) - { - I_Error("Attempted to write past end of stream"); - } - stream[0] = v >> 24; - stream[1] = (v >> 16) & 255; - stream[2] = (v >> 8) & 255; - stream[3] = v & 255; - AdvanceStream(stream, 4); -} - -void WriteInt64(int64_t v, TArrayView& stream) -{ - if (stream.Size() < 8) - { - I_Error("Attempted to write past end of stream"); - } - stream[0] = v >> 56; - stream[1] = (v >> 48) & 255; - stream[2] = (v >> 40) & 255; - stream[3] = (v >> 32) & 255; - stream[4] = (v >> 24) & 255; - stream[5] = (v >> 16) & 255; - stream[6] = (v >> 8) & 255; - stream[7] = v & 255; - AdvanceStream(stream, 8); -} - -void WriteFloat(float v, TArrayView& stream) -{ - union - { - int32_t i; - float f; - } fakeint; - fakeint.f = v; - WriteInt32 (fakeint.i, stream); -} - -void WriteDouble(double v, TArrayView& stream) -{ - union - { - int64_t i; - double f; - } fakeint; - fakeint.f = v; - WriteInt64(fakeint.i, stream); -} - FSerializer& Serialize(FSerializer& arc, const char* key, usercmd_t& cmd, usercmd_t* def) { // This used packed data with the old serializer but that's totally counterproductive when diff --git a/src/d_protocol.h b/src/d_protocol.h index 6a9717043..c738a14df 100644 --- a/src/d_protocol.h +++ b/src/d_protocol.h @@ -35,6 +35,7 @@ #define __D_PROTOCOL_H__ #include "doomtype.h" +#include "i_protocol.h" // The IFF routines here all work with big-endian IDs, even if the host // system is little-endian. @@ -241,41 +242,4 @@ void SkipUserCmdMessage(TArrayView& stream); void ReadUserCmdMessage(TArrayView& stream, int player, int tic); void RunPlayerCommands(int player, int tic); -uint8_t UncheckedReadInt8(uint8_t** stream); -int16_t UncheckedReadInt16(uint8_t** stream); -int32_t UncheckedReadInt32(uint8_t** stream); -int64_t UncheckedReadInt64(uint8_t** stream); -float UncheckedReadFloat(uint8_t** stream); -double UncheckedReadDouble(uint8_t** stream); -char* UncheckedReadString(uint8_t** stream); -const char* UncheckedReadStringConst(uint8_t** stream); -void UncheckedWriteInt8(uint8_t val, uint8_t** stream); -void UncheckedWriteInt16(int16_t val, uint8_t** stream); -void UncheckedWriteInt32(int32_t val, uint8_t** stream); -void UncheckedWriteInt64(int64_t val, uint8_t** stream); -void UncheckedWriteFloat(float val, uint8_t** stream); -void UncheckedWriteDouble(double val, uint8_t** stream); -void UncheckedWriteString(const char* string, uint8_t** stream); - -void AdvanceStream(TArrayView& stream, size_t bytes); - -uint8_t ReadInt8(TArrayView& stream); -int16_t ReadInt16(TArrayView& stream); -int32_t ReadInt32(TArrayView& stream); -int64_t ReadInt64(TArrayView& stream); -float ReadFloat(TArrayView& stream); -double ReadDouble(TArrayView& stream); -char* ReadString(TArrayView& stream); -const char* ReadStringConst(TArrayView& stream); -void ReadBytes(TArrayView& dst, TArrayView& stream); -void WriteInt8(uint8_t val, TArrayView& stream); -void WriteInt16(int16_t val, TArrayView& stream); -void WriteInt32(int32_t val, TArrayView& stream); -void WriteInt64(int64_t val, TArrayView& stream); -void WriteFloat(float val, TArrayView& stream); -void WriteDouble(double val, TArrayView& stream); -void WriteString(const char* string, TArrayView& stream); -void WriteBytes(const TArrayView& source, TArrayView& stream); -void WriteFString(FString& string, TArrayView& stream); - #endif //__D_PROTOCOL_H__ From b23f050b887624b9da17d4c0ff52a0afb3ed26ed Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 10 Jul 2025 17:32:46 -0400 Subject: [PATCH 291/384] c_cvars.cpp: d_protocol.h -> i_protocol.h --- src/common/console/c_cvars.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/console/c_cvars.cpp b/src/common/console/c_cvars.cpp index eb76f136a..888e65fd1 100644 --- a/src/common/console/c_cvars.cpp +++ b/src/common/console/c_cvars.cpp @@ -40,7 +40,7 @@ #include "c_console.h" #include "c_dispatch.h" #include "c_cvars.h" -#include "d_protocol.h" +#include "i_protocol.h" #include "engineerrors.h" #include "printf.h" #include "palutil.h" From 61cfbee7394bd6952c2dec534c442b4932d369f9 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Tue, 15 Jul 2025 10:20:46 -0400 Subject: [PATCH 292/384] Updated .gitignore Organized and add some more rules --- .gitignore | 51 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 6e9e5b9de..3a3b7cc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,43 @@ +# build artifacts +/build* +/wadsrc/*.pk3 +/appimage-build +.flatpak-builder/ +*.AppImage +*.flatpak +flatpak/ +gzdoom-crash.log +fmodapi*linux/ + +# generated code +/src/gitinfo.h + +# editor/tooling/os files +*~ +.gdb_history +.DS_Store +.vs +.idea +.vscode +.cache +*.kate-swp + +# private dev files +*.user +*.local +*.log + +# other / unknown *.ncb *.suo *.pdb *.ilk *.aps /wadsrc_wad -*.user -/build /release_gcc /DOOMSTATS.TXT -/src/gitinfo.h -/wadsrc/*.pk3 /disasm.txt -/build_vc2015 -/build_vc2015-32 -/build_vc2015-64 -/build_vc2017-64 -/build /llvm -.vs -.idea -.vscode /src/gl/unused /mapfiles_release/*.map /AppDir -/appimage-build -*.AppImage -.DS_Store -/build_vc2017-32 -/build2 -/build_vc2019-64 -/build_vc2019-32 -/build__ -gzdoom-crash.log From 99af0fb970cf531b06e5afb1f86ad3dcd6278499 Mon Sep 17 00:00:00 2001 From: DyNaM1Kk Date: Tue, 15 Jul 2025 23:25:57 +0400 Subject: [PATCH 293/384] Added the Discord RPC option to the menu --- wadsrc/static/menudef.txt | 3 +++ wadsrc/static/menudef.zsimple | 2 ++ 2 files changed, 5 insertions(+) diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 2577b9aa7..9eecfde3e 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -1320,6 +1320,9 @@ OptionMenu "MiscOptions" protected StaticText " " Option "$MISCMNU_ALWAYSTALLY", "sv_alwaystally", "AlwaysTally" + + StaticText " " + Option "$MISCMNU_DISCORDRPC", "i_discordrpc", "OnOff" } diff --git a/wadsrc/static/menudef.zsimple b/wadsrc/static/menudef.zsimple index 2e0aabf87..08d2ce028 100644 --- a/wadsrc/static/menudef.zsimple +++ b/wadsrc/static/menudef.zsimple @@ -105,6 +105,8 @@ OptionMenu MiscOptionsSimple protected Option "$MISCMNU_ENABLESCRIPTSCREENSHOTS", "enablescriptscreenshot", "OnOff" Option "$OPTMNU_LANGUAGE", "language", "LanguageOptions" Option "$MSGMNU_LONGSAVEMESSAGES", "longsavemessages", "OnOff" + StaticText " " + Option "$MISCMNU_DISCORDRPC", "i_discordrpc", "OnOff" } OptionMenu "MouseOptionsSimple" protected From 8954463f0e2adfb6fb24bd652bec8b31f1d8dcf1 Mon Sep 17 00:00:00 2001 From: Wohlstand Date: Sat, 5 Jul 2025 14:21:25 +0300 Subject: [PATCH 294/384] Added new libADLMIDI and libOPNMIDI options Not yet translated --- src/common/audio/music/music_config.cpp | 24 ++++++++++++++++ src/gameconfigfile.cpp | 11 ++++++++ wadsrc/static/menudef.txt | 35 ++++++++++++++++++++++-- wadsrc/static/xg.wopn | Bin 62080 -> 186204 bytes 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/common/audio/music/music_config.cpp b/src/common/audio/music/music_config.cpp index 5ea5e97d9..7a4b8a200 100644 --- a/src/common/audio/music/music_config.cpp +++ b/src/common/audio/music/music_config.cpp @@ -104,6 +104,16 @@ CUSTOM_CVAR(Int, adl_volume_model, 0 /*ADLMIDI_VolumeModel_AUTO*/, CVAR_ARCHIVE { FORWARD_CVAR(adl_volume_model); } + +CUSTOM_CVAR(Int, adl_chan_alloc, 0 /*ADLMIDI_ChanAlloc_AUTO*/, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +{ + FORWARD_CVAR(adl_chan_alloc); +} + +CUSTOM_CVAR(Bool, adl_auto_arpeggio, false, CVAR_ARCHIVE | CVAR_VIRTUAL) +{ + FORWARD_BOOL_CVAR(adl_auto_arpeggio); +} #endif //========================================================================== // @@ -271,6 +281,20 @@ CUSTOM_CVAR(String, opn_custom_bank, "", CVAR_ARCHIVE | CVAR_VIRTUAL) FORWARD_STRING_CVAR(opn_custom_bank); } +CUSTOM_CVAR(Int, opn_volume_model, 0 /*OPNMIDI_VolumeModel_AUTO*/, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +{ + FORWARD_CVAR(adl_volume_model); +} + +CUSTOM_CVAR(Int, opn_chan_alloc, -1 /*OPNMIDI_ChanAlloc_AUTO*/, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +{ + FORWARD_CVAR(adl_chan_alloc); +} + +CUSTOM_CVAR(Bool, opn_auto_arpeggio, false, CVAR_ARCHIVE | CVAR_VIRTUAL) +{ + FORWARD_BOOL_CVAR(adl_auto_arpeggio); +} //========================================================================== // // GUS MIDI device diff --git a/src/gameconfigfile.cpp b/src/gameconfigfile.cpp index efdad2fd5..8f89f29ca 100644 --- a/src/gameconfigfile.cpp +++ b/src/gameconfigfile.cpp @@ -71,6 +71,11 @@ EXTERN_CVAR (Bool, vid_scale_linear) EXTERN_CVAR(Float, m_sensitivity_x) EXTERN_CVAR(Float, m_sensitivity_y) EXTERN_CVAR(Int, adl_volume_model) +EXTERN_CVAR(Int, adl_chan_alloc) +EXTERN_CVAR(Bool, adl_auto_arpeggio) +EXTERN_CVAR(Int, opn_volume_model) +EXTERN_CVAR(Int, opn_chan_alloc) +EXTERN_CVAR(Bool, opn_auto_arpeggio) EXTERN_CVAR (Int, gl_texture_hqresize_targets) EXTERN_CVAR(Int, wipetype) EXTERN_CVAR(Bool, i_pauseinbackground) @@ -587,6 +592,12 @@ void FGameConfigFile::DoGlobalSetup () m_sensitivity_y = (float)yfact; adl_volume_model = 0; + adl_chan_alloc = -1; + adl_auto_arpeggio = false; + + opn_volume_model = 0; + opn_chan_alloc = -1; + opn_auto_arpeggio = false; // if user originally wanted the in-game textures resized, set model skins to resize too int old_targets = gl_texture_hqresize_targets; diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 9eecfde3e..3b568e392 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2244,6 +2244,24 @@ OptionMenu ModReplayerOptions protected 11, "$ADLVLMODEL_HMIOLD" } + OptionValue OpnVolumeModels + { + 0, "$OPNVLMODEL_AUTO" + 1, "$OPNVLMODEL_GENERIC" + 2, "$OPNVLMODEL_NATIVE" + 3, "$OPNVLMODEL_DMX" + 4, "$OPNVLMODEL_APOGEE" + 5, "$OPNVLMODEL_WIN9X" + } + + OptionValue AdlChanAllocs + { + -1, "$ADLCHANALLOC_AUTO" + 0, "$ADLCHANALLOC_OFFDELAY" + 1, "$ADLCHANALLOC_SAMEINST" + 2, "$ADLCHANALLOC_ANYREL" + } + OptionValue ADLOplCores { 0, "$OPTVAL_NUKEDOPL3" @@ -2251,16 +2269,24 @@ OptionMenu ModReplayerOptions protected 2, "$OPTVAL_DOSBOXOPL3" 3, "$OPTVAL_OPALOPL3" 4, "$OPTVAL_JAVAOPL3" + 5, "$OPTVAL_ESFMu" + 6, "$OPTVAL_MAMEOPL2" + 7, "$OPTVAL_YMFMOPL2" + 8, "$OPTVAL_YMFMOPL3" + 9, "$OPTVAL_LLENUKEDOPL2" + 10, "$OPTVAL_LLENUKEDOPL3" } OptionValue OpnCores { 0, "$OPTVAL_MAMEOPN2" - 1, "$OPTVAL_NUKEDOPN2" + 1, "$OPTVAL_NUKEDOPN2YM3438" 2, "$OPTVAL_GENSOPN2" + 3, "$OPTVAL_YMFMOPN2" 4, "$OPTVAL_NP2OPNA" 5, "$OPTVAL_MAMEOPNA" - 6, "$OPTVAL_PMDWINOPNA" + 6, "$OPTVAL_YMFMOPNA" + 7, "$OPTVAL_NUKEDOPN2YM2612" } OptionMenu ADLOptions protected @@ -2271,6 +2297,8 @@ OptionMenu ModReplayerOptions protected Slider "$ADVSNDMNU_ADLNUMCHIPS", "adl_chips_count", 1, 32, 1, 0 Option "$ADVSNDMNU_OPLFULLPAN", "adl_fullpan", "OnOff" Option "$ADVSNDMNU_VLMODEL", "adl_volume_model", "AdlVolumeModels" + Option "$ADVSNDMNU_CHANALLOC", "adl_chan_alloc", "AdlChanAllocs" + Option "$ADVSNDMNU_AUTOARPEGGIO", "adl_auto_arpeggio", "OnOff" StaticText "" LabeledSubmenu "$ADVSNDMNU_OPLBANK", "adl_bank", "ADLBankMenu" StaticText "" @@ -2285,6 +2313,9 @@ OptionMenu ModReplayerOptions protected Option "$ADVSNDMNU_RUNPCMRATE", "opn_run_at_pcm_rate", "OnOff" Slider "$ADVSNDMNU_OPNNUMCHIPS", "opn_chips_count", 1, 32, 1, 0 Option "$ADVSNDMNU_OPLFULLPAN", "opn_fullpan", "OnOff" + Option "$ADVSNDMNU_VLMODEL", "opn_volume_model", "OpnVolumeModels" + Option "$ADVSNDMNU_CHANALLOC", "opn_chan_alloc", "AdlChanAllocs" + Option "$ADVSNDMNU_AUTOARPEGGIO", "opn_auto_arpeggio", "OnOff" StaticText "" Option "$ADVSNDMNU_OPNCUSTOMBANK", "opn_use_custom_bank", "OnOff" LabeledSubmenu "$ADVSNDMNU_OPNBANKFILE", "opn_custom_bank", "OPNMIDICustomBanksMenu" diff --git a/wadsrc/static/xg.wopn b/wadsrc/static/xg.wopn index b5290c542502ca5f4b3e7d920e2a9ffff30bd6db..38208b255b4759eb1869c24d807ecd02a19fa9ae 100644 GIT binary patch delta 9158 zcmZp8%6w-Xw_vz`fS-}BlaZe{0}}%S7X$Z1LD$I#4HOs|Cm%FWWMl@DY+#ZDO!9z9 zJ}@Z&BAEmkCWft=e8@l%tVe+fLNP*=gOxBUf)p}pZ2Z4iNm1W|fq_Auf#E*`gTA~F zh+>!z;xTdvi!l6RU|2A@ajJ^4uK)uBKSY%@M3p3ps!0r4lMfmwb67Ai$jdV@@K3(T zEyWdFQk0pOt`MA)IC-IznCkx*3?>ZxAiG!?SQ+Fc8NM+v@P$YTN-!{UOc7%E!N4fN zD7g8cK|Hgx1OpQr*cwJwd0~eC3`~mi#TfoGFfz;+oqTVy=H?3qFPT|_GxEzPE5yi5 zKBdDu*;hxL>qEF_fI>({X(6ezq@s?0oTj5FUt{@D|;ITGXZ%qh7WwELh=#}3=B0Kd_qD$8A^Hj zKonyQ!vc@|qT*Bq_r#o>{K*TY#Fb5L85kJE1Q|YXFbT@@GJHp)<~T5zg2Gf$o`GHR zm;fI;gQy@sFCP~JhrkIwfiMO(hHgHAZ49h;m?tlklAYYZ#;#<+P{_c*z`)MHB+kdr z@SUNMft8Jcje&`Qn}wI1f#oOT&B+H1)+(_uFliiSW=v#YP-f?3_{_k_*2>0I%)lhe z#m4Z3p?rf>2q?*c;>(7igjW<|ATP+k0tsPmJ~j>}22N&Pb_Vt~CZ){>4K$dz0y6UR zQWcyNb8;qMHj6A1E00#?aAea1N4i>&Z9=W9)a~zmtpvo9DQ0~VX7U|_IeVPN1A zkrJH2!^|KcDL8|Nkwco7!Ggh%Lxx*OAEfgjNKbGHD0U^a{=c)cQK%E;ZeU40&Kz(Y%F37rZQ3-%w`Nm%2MpiW(-Bl zoDDqe4D7QQZ-Uf27bO;FfPz`W+eQJLoj!0BGJumF1A}xo!+!Z)RKb49B|M~VrJD6v14FlV+DD!fQOZV zft8_9jFo}m8v`>pD+2=?!yE^8v*41%qT-Cq$qS{Vl|yWVt)=A|7?>GqjKGu;2WtZZ z8$&UKn&ZH%1}dO*c^GVjt(7D=_=7oWbc9&>l{t(!_#g`TLDU=vre08pz@tfxfq_99 z6ioss(Ig9rCUIyqU4^9XFKPr!gOi&N8bmTqPOx;vS(!AshP~o7hX~V$649egP1`HwM3_lqdIpXMizN)ZZigJ1{q#%bp{SA=Cf{@MXBZaMcKs) z(RrzY`viYEa4f7;MM;5<#%w>~Z868<{b}=8D+{Ng`Jc*rg@_9yQ zF2+19g`kZ5l+@zMzKqh7&6(s_f@U!aPL5=9WU+d|{BH6*CMRwcCK)XS=bXf{%)H44 zN>Y%4g3e3d&ZXO1+C)OTjrKKeI@|F)?GZqm0;ORemKVwh(a1Jh@O%OWKV=fpaPY zlPH5B!)#V2GX@d1DGf{<409YoXMIVhG!O&_INd$2T=6Cto4BGOr{x6aDGny3NWxRFfq4t zF@U0ut#tB1gY*1gZ6!six%oNy3dJRp6=Ot{Y*Y z58|~jGRJH_Xu!?F<(iz4uMm)!qF@9nrIo~3j5Na=7(yACIA?IMv+^?W#5eG;@D>V8 zX<%SsnB%~u0ZurZFBp7fRx)R(l9T7+`@s;%70JoFLw5&=96 zUl|ycg;*IrF)+%Cf!k~hL0k-98F&KOFKj+&aF9_+i-}QKo{ixfL!r1l0|O6(A*dbB z%fQGi&%gicoI81;l%$d&1EY*I11~oNqoy3F4TL#a+!+*@C(joYWny$gQEko09*|#
    ZL zi^wRkFu1zQPiC)UP|}z`iKU6bkU>O9?iT|igQT#;Zw5vdhKk7t4L*V6FCG$q^EudA z89?#J2a3P>(D-Xbu>|a|b}qllocuiZ(##U0$rB|dbrl#`4dsOx)ENvloGonS-g*l2VVAq z%EyU9;z}$m2C9J#41o*myM9$$)a9ygW05KZBuyJR3tiLlKKSJHvVgBNnjW90!H~kY()P zoEwlhd83Y$3J(J-3xk?R149r4gJ3fQLofpak2}ce418^nfak-T7ED*lpI+z&G zY~~F-ClBsO)cPqgg2F;+@*zJZ#xSrLyFZL|rcjZIF>&(y!t;~&ZC6q@uwf8j5Mkf| zwPEGOB{>-c7-R+bS1^Ej(n6dJj1w4HCNu9yS2iM~j^W$ng(XUx*B85_D2ti08nfJI z5ctf%$Z%hX?`TotYfY|)=X1fF;pf+DH$YG@FXn-rm0KjV_f&&1cNQ`5i%hZi!(4VG1#$)fw~$se3Bqu85;)&gDJxt2ewZ1Q2=EoX35E`bex&k8z#Tk zab{zaKphp(Bh(LIhy?|!vOb|v0mck)gP9(q0!qpbpiVaKVF4x=MoX}({Rs>TBrwjM ze9&Nxk}dvG0hM2zlLJjEnLIoDKG=jja0L~>e88uMSu`yvNWf0_I7H2Tz=oezkU|`k=5a3K_ zU}W%~D8K<4Nc0R&E*eGy3mOaz4FA8l4U?e-g=oLj)D&!E3&agAVCyjYF;33(Rbt!) zN_Lai`6^B>T%)CM^a3g48^ZOt483lRJVGm}5A%O zGns+;tkz`S5Xs5@!5YkF%q^3vg7Fy0vVlclvi)u+W=U?h$t)p`EGqkWA5QiRablKa zJvg~9#EH9!F+@utxF82KMhi0bONb&z5*wG6f^&ZPU#fI$5gRkwr_vBQrf?^7(L?$;-nPnV65r9DlVGoD-80izf?4i%&FQnY=qffjN&McJjRl7miphh2V_*qLRsZk%(Sm#*MmnpreCkWqZ{`AD|OvudQ6uYhXPD5c4fQQ*P& zD5c5sq7<1Jxh5Zsg1FDOv?O(MV6@C+jc7$CrjwJsqn(+U*Gv|ub7Hn-h@E^j+KJhY z<>_Rp7$@cv3>A~(W1M)(m|L_IoKkagLW)ws13HHnsxa}GO@0{zN-pc_op=S9%OHs& zxU?u`vSFCSlwKDzcR4ZGw}0#V_;;6XMlAy^8J-Lgcv|WB*N^I9VZL3 z^RP3pDl$Hp{1a=QW0ar#aJw)+w-5&dBZEAjPy_?xB}UcB3wJ6_UcW)K^I{ncWJ^5sVK;)BA8p z?~sZHn%W_GDB913F<|lnKTrh#O4*3su8+SGNTIL`b zh4O@GP~Aa#k*pW%#LLQf2@;2hA~{xp`560MqRM0%rqPSn)#WaOW|JA~;X^AjKNuM6 z!9yzyR~b$~(=SNj1y=sg42<>nMId7^Lh|3i3VF&H7?v?cjizgww>E}oy7p%R0J^z| AVgLXD delta 499 zcmcb!j=SM0vtYP?fS-}BlaZe{0}}%S69el+LD$J?jH@=^VOpfLSzvJ;2TQnTfWqWI zy7H6d^%NN^COhh#-(09>!weRmCeI`{ou!abP^gf>k4=z`frX(DL@`D&T5e`s9KbU9 zp;^~v#>LxMRCpNJ)fphbh*6}0fepfDU?}1gVPIfmnB%~BW^Dzuh&Nv6D%Vhn<1dl;N+ILU2iDa&~ZL%H)YQ64Sq@GAd7>HHXQWiP3U;d>W%G zh@&yxXD*X86Ql3;muZaWVO%eurQn-dQk0*UpPQ=Sl$w(>U2GMj z>Gad>jORi6m6TOXS&dolv+{ptU}U&2!uykfkzqc+{C5x~z+KM3^n>9M+%_d;4Fam4 zFg%)G+r_A~y}OgqB}G{steimrY`72~*l+>)9~_Jf^96Xz80I)I*i0@|Q<^@djZv9s Yw;!xva+= Date: Sat, 5 Jul 2025 15:19:33 +0300 Subject: [PATCH 295/384] Added gain for libADLMIDI and for libOPNMIDI --- src/common/audio/music/music.cpp | 2 ++ src/common/audio/music/music_config.cpp | 28 +++++++++++++++++++++++++ wadsrc/static/menudef.txt | 5 ++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/common/audio/music/music.cpp b/src/common/audio/music/music.cpp index 9664c5458..6d837d0f7 100644 --- a/src/common/audio/music/music.cpp +++ b/src/common/audio/music/music.cpp @@ -90,6 +90,8 @@ EXTERN_CVAR(Float, snd_musicvolume) EXTERN_CVAR(Int, snd_mididevice) EXTERN_CVAR(Float, mod_dumb_mastervolume) EXTERN_CVAR(Float, fluid_gain) +EXTERN_CVAR(Float, adl_gain) +EXTERN_CVAR(Float, opn_gain) CVAR(Bool, mus_calcgain, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // changing this will only take effect for the next song. diff --git a/src/common/audio/music/music_config.cpp b/src/common/audio/music/music_config.cpp index 7a4b8a200..b0e2211d9 100644 --- a/src/common/audio/music/music_config.cpp +++ b/src/common/audio/music/music_config.cpp @@ -114,6 +114,20 @@ CUSTOM_CVAR(Bool, adl_auto_arpeggio, false, CVAR_ARCHIVE | CVAR_VIRTUAL) { FORWARD_BOOL_CVAR(adl_auto_arpeggio); } + +CUSTOM_CVAR(Float, adl_gain, 0.5, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +{ + if (!mus_usereplaygain) + { + FORWARD_CVAR(adl_gain); + } + else + { + // Replay gain will disable the user setting for consistency. + float newval; + ChangeMusicSetting(zmusic_adl_gain, mus_playing.handle, 1.0f, & newval); + } +} #endif //========================================================================== // @@ -295,6 +309,20 @@ CUSTOM_CVAR(Bool, opn_auto_arpeggio, false, CVAR_ARCHIVE | CVAR_VIRTUAL) { FORWARD_BOOL_CVAR(adl_auto_arpeggio); } + +CUSTOM_CVAR(Float, opn_gain, 0.5, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +{ + if (!mus_usereplaygain) + { + FORWARD_CVAR(opn_gain); + } + else + { + // Replay gain will disable the user setting for consistency. + float newval; + ChangeMusicSetting(zmusic_opn_gain, mus_playing.handle, 1.0f, & newval); + } +} //========================================================================== // // GUS MIDI device diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 3b568e392..0eb208f0c 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2286,12 +2286,14 @@ OptionMenu ModReplayerOptions protected 4, "$OPTVAL_NP2OPNA" 5, "$OPTVAL_MAMEOPNA" 6, "$OPTVAL_YMFMOPNA" - 7, "$OPTVAL_NUKEDOPN2YM2612" + // 7'th is a VGM Dumper, it should NEVER been used during normal runtime + 8, "$OPTVAL_NUKEDOPN2YM2612" } OptionMenu ADLOptions protected { Title "$ADVSNDMNU_ADLMIDI" + Slider "$ADVSNDMNU_FLUIDGAIN", "adl_gain", 0, 10, 0.1, 1 Option "$ADVSNDMNU_OPLCORES", "adl_emulator_id", "ADLOplCores" Option "$ADVSNDMNU_RUNPCMRATE", "adl_run_at_pcm_rate", "OnOff" Slider "$ADVSNDMNU_ADLNUMCHIPS", "adl_chips_count", 1, 32, 1, 0 @@ -2309,6 +2311,7 @@ OptionMenu ModReplayerOptions protected OptionMenu OPNOptions protected { Title "$ADVSNDMNU_OPNMIDI" + Slider "$ADVSNDMNU_FLUIDGAIN", "opn_gain", 0, 10, 0.1, 1 Option "$ADVSNDMNU_OPNCORES", "opn_emulator_id", "OpnCores" Option "$ADVSNDMNU_RUNPCMRATE", "opn_run_at_pcm_rate", "OnOff" Slider "$ADVSNDMNU_OPNNUMCHIPS", "opn_chips_count", 1, 32, 1, 0 From c461ecf19b944341afb0a58b3060481753905753 Mon Sep 17 00:00:00 2001 From: Wohlstand Date: Sat, 5 Jul 2025 18:40:51 +0300 Subject: [PATCH 296/384] ADLMIDI: Added support for GENMIDI as a custom bank Now, libADLMIDI can use GENMIDI bank too as the "OPL Synth Emulation" --- src/common/audio/music/music.cpp | 1 + src/common/audio/music/music_config.cpp | 5 +++++ wadsrc/static/menudef.txt | 1 + 3 files changed, 7 insertions(+) diff --git a/src/common/audio/music/music.cpp b/src/common/audio/music/music.cpp index 6d837d0f7..9f51591fe 100644 --- a/src/common/audio/music/music.cpp +++ b/src/common/audio/music/music.cpp @@ -446,6 +446,7 @@ EXTERN_CVAR(String, wildmidi_config) EXTERN_CVAR(String, adl_custom_bank) EXTERN_CVAR(Int, adl_bank) EXTERN_CVAR(Bool, adl_use_custom_bank) +EXTERN_CVAR(Bool, adl_use_genmidi) EXTERN_CVAR(String, opn_custom_bank) EXTERN_CVAR(Bool, opn_use_custom_bank) EXTERN_CVAR(Int, opl_core) diff --git a/src/common/audio/music/music_config.cpp b/src/common/audio/music/music_config.cpp index b0e2211d9..3452f455b 100644 --- a/src/common/audio/music/music_config.cpp +++ b/src/common/audio/music/music_config.cpp @@ -95,6 +95,11 @@ CUSTOM_CVAR(Bool, adl_use_custom_bank, false, CVAR_ARCHIVE | CVAR_VIRTUAL) FORWARD_BOOL_CVAR(adl_use_custom_bank); } +CUSTOM_CVAR(Bool, adl_use_genmidi, false, CVAR_ARCHIVE | CVAR_VIRTUAL) +{ + FORWARD_BOOL_CVAR(adl_use_genmidi); +} + CUSTOM_CVAR(String, adl_custom_bank, "", CVAR_ARCHIVE | CVAR_VIRTUAL | CVAR_SYSTEM_ONLY) { FORWARD_STRING_CVAR(adl_custom_bank); diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 0eb208f0c..0f31f3972 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2305,6 +2305,7 @@ OptionMenu ModReplayerOptions protected LabeledSubmenu "$ADVSNDMNU_OPLBANK", "adl_bank", "ADLBankMenu" StaticText "" Option "$ADVSNDMNU_ADLCUSTOMBANK", "adl_use_custom_bank", "OnOff" + Option "$ADVSNDMNU_ADLUSEWADBANK", "adl_use_genmidi", "OnOff" LabeledSubmenu "$ADVSNDMNU_OPLBANKFILE", "adl_custom_bank", "ADLMIDICustomBanksMenu" } From ee7e913e1a72c5a1e7b5152561633c1d066346bf Mon Sep 17 00:00:00 2001 From: Wohlstand Date: Sat, 5 Jul 2025 19:00:19 +0300 Subject: [PATCH 297/384] Added gaining factor for OPL Synth Emulation too Also, fixed default gaining values at libADL and libOPN (they should be 1.0, not 0.5) --- src/common/audio/music/music.cpp | 1 + src/common/audio/music/music_config.cpp | 18 ++++++++++++++++-- wadsrc/static/menudef.txt | 5 +++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/common/audio/music/music.cpp b/src/common/audio/music/music.cpp index 9f51591fe..17f6d242e 100644 --- a/src/common/audio/music/music.cpp +++ b/src/common/audio/music/music.cpp @@ -90,6 +90,7 @@ EXTERN_CVAR(Float, snd_musicvolume) EXTERN_CVAR(Int, snd_mididevice) EXTERN_CVAR(Float, mod_dumb_mastervolume) EXTERN_CVAR(Float, fluid_gain) +EXTERN_CVAR(Float, opl_gain) EXTERN_CVAR(Float, adl_gain) EXTERN_CVAR(Float, opn_gain) diff --git a/src/common/audio/music/music_config.cpp b/src/common/audio/music/music_config.cpp index 3452f455b..ab12f2fc5 100644 --- a/src/common/audio/music/music_config.cpp +++ b/src/common/audio/music/music_config.cpp @@ -120,7 +120,7 @@ CUSTOM_CVAR(Bool, adl_auto_arpeggio, false, CVAR_ARCHIVE | CVAR_VIRTUAL) FORWARD_BOOL_CVAR(adl_auto_arpeggio); } -CUSTOM_CVAR(Float, adl_gain, 0.5, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +CUSTOM_CVAR(Float, adl_gain, 1.0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) { if (!mus_usereplaygain) { @@ -262,6 +262,20 @@ CUSTOM_CVAR(Bool, opl_fullpan, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIR FORWARD_BOOL_CVAR(opl_fullpan); } +CUSTOM_CVAR(Float, opl_gain, 1.0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +{ + if (!mus_usereplaygain) + { + FORWARD_CVAR(opl_gain); + } + else + { + // Replay gain will disable the user setting for consistency. + float newval; + ChangeMusicSetting(zmusic_opl_gain, mus_playing.handle, 1.0f, & newval); + } +} + #ifndef ZMUSIC_LITE //========================================================================== // @@ -315,7 +329,7 @@ CUSTOM_CVAR(Bool, opn_auto_arpeggio, false, CVAR_ARCHIVE | CVAR_VIRTUAL) FORWARD_BOOL_CVAR(adl_auto_arpeggio); } -CUSTOM_CVAR(Float, opn_gain, 0.5, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) +CUSTOM_CVAR(Float, opn_gain, 1.0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) { if (!mus_usereplaygain) { diff --git a/wadsrc/static/menudef.txt b/wadsrc/static/menudef.txt index 0f31f3972..0e55fd790 100644 --- a/wadsrc/static/menudef.txt +++ b/wadsrc/static/menudef.txt @@ -2225,6 +2225,7 @@ OptionMenu ModReplayerOptions protected Title "$ADVSNDMNU_OPLSYNTHESIS" Option "$ADVSNDMNU_OPLCORES", "opl_core", "OplCores" Slider "$ADVSNDMNU_OPLNUMCHIPS", "opl_numchips", 1, 8, 1, 0 + Slider "$ADVSNDMNU_OPLGAIN", "opl_gain", 0, 10, 0.1, 1 Option "$ADVSNDMNU_OPLFULLPAN", "opl_fullpan", "OnOff" } @@ -2293,7 +2294,7 @@ OptionMenu ModReplayerOptions protected OptionMenu ADLOptions protected { Title "$ADVSNDMNU_ADLMIDI" - Slider "$ADVSNDMNU_FLUIDGAIN", "adl_gain", 0, 10, 0.1, 1 + Slider "$ADVSNDMNU_ADLGAIN", "adl_gain", 0, 10, 0.1, 1 Option "$ADVSNDMNU_OPLCORES", "adl_emulator_id", "ADLOplCores" Option "$ADVSNDMNU_RUNPCMRATE", "adl_run_at_pcm_rate", "OnOff" Slider "$ADVSNDMNU_ADLNUMCHIPS", "adl_chips_count", 1, 32, 1, 0 @@ -2312,7 +2313,7 @@ OptionMenu ModReplayerOptions protected OptionMenu OPNOptions protected { Title "$ADVSNDMNU_OPNMIDI" - Slider "$ADVSNDMNU_FLUIDGAIN", "opn_gain", 0, 10, 0.1, 1 + Slider "$ADVSNDMNU_OPNGAIN", "opn_gain", 0, 10, 0.1, 1 Option "$ADVSNDMNU_OPNCORES", "opn_emulator_id", "OpnCores" Option "$ADVSNDMNU_RUNPCMRATE", "opn_run_at_pcm_rate", "OnOff" Slider "$ADVSNDMNU_OPNNUMCHIPS", "opn_chips_count", 1, 32, 1, 0 From 240bac81c2d6c8c76117eb6135d20fd974d2923a Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Tue, 15 Jul 2025 17:14:37 -0400 Subject: [PATCH 298/384] - update zmusic --- bin/windows/zmusic/32bit/zmusic.lib | Bin 10896 -> 0 bytes bin/windows/zmusic/64bit/zmusic.lib | Bin 10688 -> 11146 bytes bin/windows/zmusic/arm64/zmusic.lib | Bin 10688 -> 11146 bytes bin/windows/zmusic/include/zmusic.h | 12 ++++++++++++ 4 files changed, 12 insertions(+) delete mode 100644 bin/windows/zmusic/32bit/zmusic.lib diff --git a/bin/windows/zmusic/32bit/zmusic.lib b/bin/windows/zmusic/32bit/zmusic.lib deleted file mode 100644 index e426e98926c468dc9a8c08c6a318d41029b7f654..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10896 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 diff --git a/bin/windows/zmusic/64bit/zmusic.lib b/bin/windows/zmusic/64bit/zmusic.lib index bbacecf877c814abc82a97fb8709307c0ff07f3b..7952de5b4ed224e54aa58cae23bc548f7235387e 100644 GIT binary patch delta 1512 zcmX>Q+!a2-vfkL#KmiOAxEL515*ZkTo-r_p@-Q%nIWaKE%R#Wx0tN;Uma}1C0AU#g z1_lsT31eUYVd-rQ3?MA?je!A#Wz!fKKv<@OfdPco0~i=USVM<_0faR<7#KiU{Xjhf zgZcpm2KgQcmb$>e0LC#43{o)+4AM>ztnh$=0gMY67!(Q^7*tK5cmV^0>H-D^H30}# zImW;M!g3oJ7(iIFgnQ1j}AwU;tr_ zSquyytSrL70Ky6k3=ANw+`zy9!dmMX7(iHCj)4J$756bPfUuGt0|N*v1~D*zu;e5L z1`w7w!N35*k~$0wAT0imfdPcYB)>5*fUx*11_lt8NMc}+NSeHX*JSet#+j@d?x`g% zsb!hTsUejGsSNS)nYjh=QNE?cnaS}eQj;6mw{jJwCgytPrRBR;Ouoo3jKtln$-%~0 zZ)^sSYa<2*hG#+y3_PL?3{GMU407@e3=5PP7;NMi7!+g}7{XK-7`90>Fnp6?U`Uf? zVCaxxU{Dc5&{JYy2vTHVm?X)-a6*ECK}V8-;h#7I!#7C=hFRha3`r8; zRLfwPZBd1LhrfS-v|ye0QL7b61$$K+fQ>B;*VAO|vU>hFo;f0 zl~3pPO)L4K$iToL#=tUJkX3y0JNW?4wy=p`q!}0(#3#EeBy;ZL2|55WS7P#Jg%DE{ zaM7Zmpa3de{{LrSNNE#cy?auLfq_91#bV?joZO&fJDFWkm{X<1><}nfNKIB&%;vOs z`0|_#0|SHfMD$R3>MtCi3{W zI9~^8R|RR`z$`NPm1+QI-)GT>Aa|)vc2-N~d>niC7RU|ilh>=IavN;QKL_GzFtAK^ zz~z~0$Hau`MY{N=c5^KE{ZcSFlbMX*NEp-eD@pd kf1Sz4H4-@+>puPfg}Ux!L(Ozf4V$7@AXD@vFVvI=0B65rV*mgE delta 1276 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 Y2Ko3uyXa(LZGW%^kneORXKTv?09RDJr2qf` diff --git a/bin/windows/zmusic/include/zmusic.h b/bin/windows/zmusic/include/zmusic.h index 90cb80e25..1de6daac7 100644 --- a/bin/windows/zmusic/include/zmusic.h +++ b/bin/windows/zmusic/include/zmusic.h @@ -94,7 +94,10 @@ typedef enum EIntConfigKey_ zmusic_adl_fullpan, zmusic_adl_bank, zmusic_adl_use_custom_bank, + zmusic_adl_use_genmidi, zmusic_adl_volume_model, + zmusic_adl_chan_alloc, + zmusic_adl_auto_arpeggio, zmusic_fluid_reverb, zmusic_fluid_chorus, @@ -114,6 +117,9 @@ typedef enum EIntConfigKey_ zmusic_opn_run_at_pcm_rate, zmusic_opn_fullpan, zmusic_opn_use_custom_bank, + zmusic_opn_volume_model, + zmusic_opn_chan_alloc, + zmusic_opn_auto_arpeggio, zmusic_gus_dmxgus, zmusic_gus_midi_voices, @@ -167,6 +173,12 @@ typedef enum EFloatConfigKey_ zmusic_fluid_chorus_speed, zmusic_fluid_chorus_depth, + zmusic_opl_gain, + + zmusic_adl_gain, + + zmusic_opn_gain, + zmusic_timidity_drum_power, zmusic_timidity_tempo_adjust, zmusic_timidity_min_sustain_time, From c4b4705acf4ecf94054e7fc91c9ddb95af2f6f5a Mon Sep 17 00:00:00 2001 From: Rachael Alexanderson Date: Tue, 15 Jul 2025 18:27:21 -0400 Subject: [PATCH 299/384] - update CI deps to temporary archives until next release --- .github/workflows/continuous_integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 1321833f1..792049f5b 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -64,9 +64,9 @@ jobs: fi mkdir build if [[ "${{ runner.os }}" == 'macOS' ]]; then - export ZMUSIC_PACKAGE=zmusic-1.1.14-macos-arm.tar.xz + export ZMUSIC_PACKAGE=zmusic-2025-07-15-mac-universal.tar.xz elif [[ "${{ runner.os }}" == 'Linux' ]]; then - export ZMUSIC_PACKAGE=zmusic-1.1.14-linux.tar.xz + export ZMUSIC_PACKAGE=zmusic-2025-07-15-linux.tar.xz fi if [[ -n "${ZMUSIC_PACKAGE}" ]]; then cd build From 99ffd8727b6983255b818836c03e3e15b87f6555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Sun, 22 Jun 2025 02:27:07 -0300 Subject: [PATCH 300/384] rewrite array codegen for field access fixes major codegen bug --- src/common/scripting/backend/codegen.cpp | 104 +++++++++++++++++++---- 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/src/common/scripting/backend/codegen.cpp b/src/common/scripting/backend/codegen.cpp index 82d82d893..1f7df3dd0 100644 --- a/src/common/scripting/backend/codegen.cpp +++ b/src/common/scripting/backend/codegen.cpp @@ -8072,7 +8072,6 @@ ExpEmit FxArrayElement::Emit(VMFunctionBuilder *build) { arraytype = static_cast(Array->ValueType); } - ExpEmit arrayvar = Array->Emit(build); ExpEmit start; ExpEmit bound; bool nestedarray = false; @@ -8080,31 +8079,99 @@ ExpEmit FxArrayElement::Emit(VMFunctionBuilder *build) if (SizeAddr != ~0u) { bool ismeta = Array->ExprType == EFX_ClassMember && static_cast(Array)->membervar->Flags & VARF_Meta; - - start = ExpEmit(build, REGT_POINTER); - build->Emit(OP_LP, start.RegNum, arrayvar.RegNum, build->GetConstantInt(0)); - + auto f = Create(NAME_None, TypeUInt32, ismeta? VARF_Meta : 0, SizeAddr); auto arraymemberbase = static_cast(Array); - auto origmembervar = arraymemberbase->membervar; - auto origaddrreq = arraymemberbase->AddressRequested; - auto origvaluetype = Array->ValueType; + if (Array->ExprType == EFX_StructMember || Array->ExprType == EFX_ClassMember) + { + struct DummyVar : public FxExpression + { + ExpEmit dummy; + DummyVar(ExpEmit e) : FxExpression(EFX_Expression,{}), dummy(e){} + ExpEmit Emit(VMFunctionBuilder *build) + { + return dummy; + }; + }; - arraymemberbase->membervar = f; - arraymemberbase->AddressRequested = false; - Array->ValueType = TypeUInt32; + //fix expression bug + FxStructMember * orig = static_cast(Array); + FxExpression * prev = orig->classx; + ExpEmit objvar = prev->Emit(build); + bool wasFixed = objvar.Fixed; - bound = Array->Emit(build); + objvar.Fixed = true; - arraymemberbase->membervar = origmembervar; - arraymemberbase->AddressRequested = origaddrreq; - Array->ValueType = origvaluetype; + orig->classx = new DummyVar(objvar); + orig->classx->ValueType = prev->ValueType; + ExpEmit arrayvar = Array->Emit(build); + start = ExpEmit(build, REGT_POINTER); + delete orig->classx; + orig->classx = prev; - arrayvar.Free(build); + build->Emit(OP_LP, start.RegNum, arrayvar.RegNum, build->GetConstantInt(0)); + + if(ismeta) + { + // [Jay0] + // ugh do it the old way for meta since i don't want to duplicate that code here, but this time it only double-emits the member access, not the function call, so no bad side-effects, still not "right" though + // TODO handle meta better + + auto origmembervar = arraymemberbase->membervar; + auto origaddrreq = arraymemberbase->AddressRequested; + auto origvaluetype = Array->ValueType; + + arraymemberbase->membervar = f; + arraymemberbase->AddressRequested = false; + Array->ValueType = TypeUInt32; + + bound = Array->Emit(build); + + arraymemberbase->membervar = origmembervar; + arraymemberbase->AddressRequested = origaddrreq; + Array->ValueType = origvaluetype; + } + else + { + bound = ExpEmit(build, REGT_INT); + build->Emit(OP_LW, bound.RegNum, objvar.RegNum, build->GetConstantInt((int)SizeAddr)); + } + + objvar.Fixed = wasFixed; + objvar.Free(build); + arrayvar.Free(build); + } + else + { + // [Jay0] + // now only runs for global variables and stack variables, so the double-emit is """fine""" + // TODO replace this entirely with something better still + + ExpEmit arrayvar = Array->Emit(build); + + start = ExpEmit(build, REGT_POINTER); + build->Emit(OP_LP, start.RegNum, arrayvar.RegNum, build->GetConstantInt(0)); + + auto origmembervar = arraymemberbase->membervar; + auto origaddrreq = arraymemberbase->AddressRequested; + auto origvaluetype = Array->ValueType; + + arraymemberbase->membervar = f; + arraymemberbase->AddressRequested = false; + Array->ValueType = TypeUInt32; + + bound = Array->Emit(build); + + arraymemberbase->membervar = origmembervar; + arraymemberbase->AddressRequested = origaddrreq; + Array->ValueType = origvaluetype; + arrayvar.Free(build); + } } else if ((Array->ExprType == EFX_ArrayElement || Array->ExprType == EFX_OutVarDereference) && Array->isStaticArray()) { + ExpEmit arrayvar = Array->Emit(build); bound = ExpEmit(build, REGT_INT); build->Emit(OP_LW, bound.RegNum, arrayvar.RegNum, build->GetConstantInt(myoffsetof(FArray, Count))); @@ -8114,7 +8181,10 @@ ExpEmit FxArrayElement::Emit(VMFunctionBuilder *build) nestedarray = true; } - else start = arrayvar; + else + { + start = Array->Emit(build); + } if (index->isConstant()) { From 73445f6f6affa611639f452601e22d1ee86546a4 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Tue, 15 Jul 2025 13:00:12 -0400 Subject: [PATCH 301/384] Fixed arithmetic state jumps going out-of-bounds --- src/common/scripting/backend/codegen.cpp | 8 +- src/common/scripting/backend/codegen.h | 5 +- src/namedef_custom.h | 1 + src/scripting/backend/codegen_doom.cpp | 100 +++++++++++++++++++++-- src/scripting/backend/codegen_doom.h | 17 ++++ wadsrc/static/zscript/doombase.zs | 1 + 6 files changed, 119 insertions(+), 13 deletions(-) diff --git a/src/common/scripting/backend/codegen.cpp b/src/common/scripting/backend/codegen.cpp index 1f7df3dd0..56e915c5d 100644 --- a/src/common/scripting/backend/codegen.cpp +++ b/src/common/scripting/backend/codegen.cpp @@ -3326,11 +3326,11 @@ FxExpression *FxAddSub::Resolve(FCompileContext& ctx) if (compileEnvironment.CheckForCustomAddition) { - auto result = compileEnvironment.CheckForCustomAddition(this, ctx); - if (result) + auto expr = compileEnvironment.CheckForCustomAddition(this, ctx); + if (expr) { - ABORT(right); - goto goon; + delete this; + return expr->Resolve(ctx); } } diff --git a/src/common/scripting/backend/codegen.h b/src/common/scripting/backend/codegen.h index bfe08ec74..842524cb2 100644 --- a/src/common/scripting/backend/codegen.h +++ b/src/common/scripting/backend/codegen.h @@ -316,7 +316,8 @@ enum EFxType EFX_LocalArrayDeclaration, EFX_OutVarDereference, EFX_ToVector, - EFX_COUNT + EFX_COUNT, + EFX_FStateOffset, }; //========================================================================== @@ -2388,7 +2389,7 @@ public: struct CompileEnvironment { FxExpression* (*SpecialTypeCast)(FxTypeCast* func, FCompileContext& ctx); - bool (*CheckForCustomAddition)(FxAddSub* func, FCompileContext& ctx); + FxExpression* (*CheckForCustomAddition)(FxAddSub* func, FCompileContext& ctx); FxExpression* (*CheckSpecialIdentifier)(FxIdentifier* func, FCompileContext& ctx); FxExpression* (*CheckSpecialGlobalIdentifier)(FxIdentifier* func, FCompileContext& ctx); FxExpression* (*ResolveSpecialIdentifier)(FxIdentifier* func, FxExpression*& object, PContainerType* objtype, FCompileContext& ctx); diff --git a/src/namedef_custom.h b/src/namedef_custom.h index 87675346b..fd6518d53 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -925,3 +925,4 @@ xx(frictionfactor) xx(movefactor) xx(Corona) +xx(BuiltinStateOffset) diff --git a/src/scripting/backend/codegen_doom.cpp b/src/scripting/backend/codegen_doom.cpp index 8ca5437e3..1164d6903 100644 --- a/src/scripting/backend/codegen_doom.cpp +++ b/src/scripting/backend/codegen_doom.cpp @@ -82,6 +82,92 @@ bool isActor(PContainerType *type) // //========================================================================== +static FState* NativeStateOffset(FState* state, int offset) +{ + const PClassActor* cls = FState::StaticFindStateOwner(state); + const ptrdiff_t i = state - cls->ActorInfo()->OwnedStates; + if (i + offset < 0 || i + offset >= cls->ActorInfo()->NumOwnedStates) + I_Error("Tried to fetch out-of-bounds state from Actor %s", cls->TypeName.GetChars()); + + return &cls->ActorInfo()->OwnedStates[i + offset]; +} + +DEFINE_ACTION_FUNCTION_NATIVE(DObject, BuiltinStateOffset, NativeStateOffset) +{ + PARAM_PROLOGUE; + PARAM_POINTER(state, FState); + PARAM_INT(offset); + ACTION_RETURN_STATE(NativeStateOffset(state, offset)); +} + +ExpEmit FxFStateOffset::Emit(VMFunctionBuilder* build) +{ + auto sym = FindBuiltinFunction(NAME_BuiltinStateOffset); + + assert(sym); + VMFunction* callfunc = sym->Variants[0].Implementation; + assert(State && Offset); + + FunctionCallEmitter emitters(callfunc); + emitters.AddParameter(build, State); + emitters.AddParameter(build, Offset); + emitters.AddReturn(REGT_POINTER); + return emitters.EmitCall(build); +} + +//========================================================================== +// +// +// +//========================================================================== + +FxExpression* FxFStateOffset::Resolve(FCompileContext& ctx) +{ + CHECKRESOLVED(); + if (State && Offset) + { + RESOLVE(State, ctx); + RESOLVE(Offset, ctx); + ABORT(State && Offset); + assert(State->ValueType == ValueType); + assert(Offset->IsInteger()); + } + return this; +}; + +//========================================================================== +// +// +// +//========================================================================== + +FxFStateOffset::FxFStateOffset(FxExpression* state, FxExpression* offset, const FScriptPosition& pos) + : FxExpression(EFX_FStateOffset, pos) +{ + assert(state && offset); + State = state; + Offset = offset; + ValueType = TypeState; +} + +//========================================================================== +// +// +// +//========================================================================== + +FxFStateOffset::~FxFStateOffset() +{ + SAFE_DELETE(State); + SAFE_DELETE(Offset); +} + +//========================================================================== +// +// +// +//========================================================================== + static FxExpression *CustomTypeCast(FxTypeCast *func, FCompileContext &ctx) { if (func->ValueType == TypeStateLabel) @@ -159,17 +245,17 @@ static FxExpression *CustomTypeCast(FxTypeCast *func, FCompileContext &ctx) // //========================================================================== -static bool CheckForCustomAddition(FxAddSub *func, FCompileContext &ctx) +static FxExpression* CheckForCustomAddition(FxAddSub *func, FCompileContext &ctx) { if (func->left->ValueType == TypeState && func->right->IsInteger() && func->Operator == '+' && !func->left->isConstant()) { - // This is the only special case of pointer addition that will be accepted - because it is used quite often in the existing game code. - func->ValueType = TypeState; - func->right = new FxMulDiv('*', func->right, new FxConstant((int)sizeof(FState), func->ScriptPosition)); // multiply by size here, so that constants can be better optimized. - func->right = func->right->Resolve(ctx); - return true; + // This has to be locked down unlike previously. As such it's now a significiantly slower builtin. :) + auto expr = new FxFStateOffset(func->left, func->right, func->ScriptPosition); + func->left = nullptr; + func->right = nullptr; + return expr; } - return false; + return nullptr; } //========================================================================== diff --git a/src/scripting/backend/codegen_doom.h b/src/scripting/backend/codegen_doom.h index eaa1f9e73..b5ae0383a 100644 --- a/src/scripting/backend/codegen_doom.h +++ b/src/scripting/backend/codegen_doom.h @@ -110,3 +110,20 @@ public: FxMultiNameState(const char *statestring, const FScriptPosition &pos, PClassActor *checkclass = nullptr); FxExpression *Resolve(FCompileContext&); }; + +//========================================================================== +// +// +// +//========================================================================== + +class FxFStateOffset : public FxExpression +{ + FxExpression* State; + FxExpression* Offset; +public: + FxFStateOffset(FxExpression* state, FxExpression* offset, const FScriptPosition& pos); + ~FxFStateOffset(); + FxExpression* Resolve(FCompileContext&); + ExpEmit Emit(VMFunctionBuilder* build); +}; diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index ca4361d7c..d589e1c4c 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -170,6 +170,7 @@ extend class Object private native static Object BuiltinNewDoom(Class cls, int outerclass, int compatibility); private native static TranslationID BuiltinFindTranslation(Name nm); private native static int BuiltinCallLineSpecial(int special, Actor activator, int arg1, int arg2, int arg3, int arg4, int arg5); + private native static State BuiltinStateOffset(State st, int offset); // These really should be global functions... native static String G_SkillName(); native static int G_SkillPropertyInt(int p); From a44632f41b87018ba977868c01c9d819e0329e3f Mon Sep 17 00:00:00 2001 From: Boondorl Date: Thu, 17 Jul 2025 01:49:23 -0400 Subject: [PATCH 302/384] Fixed EFX enum --- src/common/scripting/backend/codegen.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/scripting/backend/codegen.h b/src/common/scripting/backend/codegen.h index 842524cb2..208bfad9e 100644 --- a/src/common/scripting/backend/codegen.h +++ b/src/common/scripting/backend/codegen.h @@ -316,8 +316,9 @@ enum EFxType EFX_LocalArrayDeclaration, EFX_OutVarDereference, EFX_ToVector, - EFX_COUNT, EFX_FStateOffset, + + EFX_COUNT }; //========================================================================== From 385c8db2ccc08e81d4be16ff7806d0d727b36515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Thu, 17 Jul 2025 02:15:13 -0300 Subject: [PATCH 303/384] fix window showing with norun --- src/common/platform/win32/i_main.cpp | 2 +- src/d_main.cpp | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/common/platform/win32/i_main.cpp b/src/common/platform/win32/i_main.cpp index c68b5a7aa..406c165c0 100644 --- a/src/common/platform/win32/i_main.cpp +++ b/src/common/platform/win32/i_main.cpp @@ -339,7 +339,7 @@ int DoMain (HINSTANCE hInstance) } else if (StdOut == nullptr) { - mainwindow.ShowErrorPane(nullptr); + mainwindow.ShowErrorPane(""); } } } diff --git a/src/d_main.cpp b/src/d_main.cpp index 3f0ee70a0..bc65e9e6b 100644 --- a/src/d_main.cpp +++ b/src/d_main.cpp @@ -3227,7 +3227,9 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw int max_progress = TexMan.GuesstimateNumTextures(); int per_shader_progress = 0;//screen->GetShaderCount()? (max_progress / 10 / screen->GetShaderCount()) : 0; - bool nostartscreen = batchrun || restart || Args->CheckParm("-join") || Args->CheckParm("-host") || Args->CheckParm("-norun"); + + bool norun = Args->CheckParm("-norun"); + bool nostartscreen = batchrun || restart || Args->CheckParm("-join") || Args->CheckParm("-host") || norun; if (GameStartupInfo.Type == FStartupInfo::DefaultStartup) { @@ -3252,7 +3254,7 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw exec = NULL; } - if (!restart) + if (!(restart || norun)) V_Init2(); // [RH] Initialize localizable strings. @@ -3276,12 +3278,12 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw TexMan.Init(); - if (!batchrun) Printf ("V_Init: allocate screen.\n"); - if (!restart) + if (!(batchrun || norun)) Printf ("V_Init: allocate screen.\n"); + if (!(restart || norun)) { screen->CompileNextShader(); } - else + else if(!norun) { // Update screen palette when restarting screen->UpdatePalette(); @@ -3491,6 +3493,11 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw // about to begin the game. FBaseCVar::EnableNoSet (); + if (norun || batchrun) + { + return 1337; // special exit + } + // [RH] Run any saved commands from the command line or autoexec.cfg now. gamestate = GS_FULLCONSOLE; Net_Initialize(); @@ -3514,11 +3521,6 @@ static int D_InitGame(const FIWADInfo* iwad_info, std::vector& allw S_Sound (CHAN_BODY, 0, "misc/startupdone", 1, ATTN_NONE); - if (Args->CheckParm("-norun") || batchrun) - { - return 1337; // special exit - } - if (StartScreen) { StartScreen->Progress(max_progress); // advance progress bar to the end. @@ -3849,7 +3851,10 @@ int GameMain() M_SaveDefaultsFinal(); DeleteStartupScreen(); C_UninitCVars(); // must come last so that nothing will access the CVARs anymore after deletion. - CloseWidgetResources(); + if(ret != 1337) + { + CloseWidgetResources(); + } delete Args; Args = nullptr; return ret; From 79855337c8097a5a32717724ce6963678ca97fb6 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Thu, 17 Jul 2025 16:15:21 -0400 Subject: [PATCH 304/384] Fixed oob array access --- wadsrc/static/zscript/engine/ui/menu/optionmenu.zs | 1 + 1 file changed, 1 insertion(+) diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs index b45b160ad..e6c477018 100644 --- a/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs +++ b/wadsrc/static/zscript/engine/ui/menu/optionmenu.zs @@ -734,6 +734,7 @@ class OptionMenu : Menu { i += mDesc.mScrollPos; if (i >= mDesc.mItems.Size()) break; // skipped beyond end of menu + if (i < 0) i = 0; } if (!mDesc.mItems[i].Visible()) From f6481b2876bd8d18016aede6a870829b4ce52318 Mon Sep 17 00:00:00 2001 From: "Dileep V. Reddy" Date: Sun, 6 Jul 2025 15:05:20 -0600 Subject: [PATCH 305/384] Stacked-sector portal and reflective flat stencils rewrite to address rendering bug with stencil cap. --- src/rendering/hwrenderer/scene/hw_portal.cpp | 4 +-- src/rendering/hwrenderer/scene/hw_sky.cpp | 38 ++++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/rendering/hwrenderer/scene/hw_portal.cpp b/src/rendering/hwrenderer/scene/hw_portal.cpp index 4d1ce3f8b..1cdc10857 100644 --- a/src/rendering/hwrenderer/scene/hw_portal.cpp +++ b/src/rendering/hwrenderer/scene/hw_portal.cpp @@ -839,7 +839,7 @@ bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *c void HWSectorStackPortal::DrawPortalStencil(FRenderState &state, int pass) { - if (mState->vpIsAllowedOoB) + if (true) // mState->vpIsAllowedOoB) { bool isceiling = planesused & (1 << sector_t::ceiling); for (unsigned i = 0; ivpIsAllowedOoB) + if (true) // mState->vpIsAllowedOoB) { bool isceiling = planesused & (1 << sector_t::ceiling); for (unsigned int i = 0; i < lines.Size(); i++) diff --git a/src/rendering/hwrenderer/scene/hw_sky.cpp b/src/rendering/hwrenderer/scene/hw_sky.cpp index 9ff480884..e8645e8dc 100644 --- a/src/rendering/hwrenderer/scene/hw_sky.cpp +++ b/src/rendering/hwrenderer/scene/hw_sky.cpp @@ -318,15 +318,16 @@ void HWWall::SkyTop(HWWallDispatcher *di, seg_t * seg,sector_t * fs,sector_t * b float frontreflect = fs->GetReflect(sector_t::ceiling); if (frontreflect > 0) { - float backreflect = bs->GetReflect(sector_t::ceiling); - if (backreflect > 0 && bs->ceilingplane.fD() == fs->ceilingplane.fD() && !bs->isClosed()) - { - // Don't add intra-portal line to the portal. - if (!(di->di && di->di->Viewpoint.IsAllowedOoB())) - { - return; - } - } + // [DVR] Changed the stencil for planemirrors, so now I need intra-portal lines + // float backreflect = bs->GetReflect(sector_t::ceiling); + // if (backreflect > 0 && bs->ceilingplane.fD() == fs->ceilingplane.fD() && !bs->isClosed()) + // { + // Don't add intra-portal line to the portal. + // if (!(di->di && di->di->Viewpoint.IsAllowedOoB())) + // { + // return; + // } + // } } else { @@ -400,15 +401,16 @@ void HWWall::SkyBottom(HWWallDispatcher *di, seg_t * seg,sector_t * fs,sector_t float frontreflect = fs->GetReflect(sector_t::floor); if (frontreflect > 0) { - float backreflect = bs->GetReflect(sector_t::floor); - if (backreflect > 0 && bs->floorplane.fD() == fs->floorplane.fD() && !bs->isClosed()) - { - // Don't add intra-portal line to the portal. - if (!(di->di && di->di->Viewpoint.IsAllowedOoB())) - { - return; - } - } + // [DVR] Changed the stencil for planemirrors, so now I need intra-portal lines + // float backreflect = bs->GetReflect(sector_t::floor); + // if (backreflect > 0 && bs->floorplane.fD() == fs->floorplane.fD() && !bs->isClosed()) + // { + // // Don't add intra-portal line to the portal. + // if (!(di->di && di->di->Viewpoint.IsAllowedOoB())) + // { + // return; + // } + // } } else { From 0909e12d621385c894e83ddce7894fee6ac1a9b3 Mon Sep 17 00:00:00 2001 From: Marcus Minhorst Date: Tue, 15 Jul 2025 11:51:50 -0400 Subject: [PATCH 306/384] Expose m_simpleoptions_view cvar to menus --- src/menu/doommenu.cpp | 59 ++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/menu/doommenu.cpp b/src/menu/doommenu.cpp index 48a03a039..43adc082e 100644 --- a/src/menu/doommenu.cpp +++ b/src/menu/doommenu.cpp @@ -32,42 +32,31 @@ ** */ -#include "c_dispatch.h" -#include "d_gui.h" -#include "c_buttons.h" -#include "c_console.h" #include "c_bind.h" -#include "d_eventbase.h" -#include "g_input.h" -#include "configfile.h" +#include "c_dispatch.h" +#include "d_event.h" +#include "d_main.h" +#include "d_player.h" +#include "doommenu.h" +#include "g_game.h" +#include "g_level.h" +#include "gameconfigfile.h" +#include "gamestate.h" +#include "gi.h" #include "gstrings.h" +#include "hwrenderer/scene/hw_drawinfo.h" +#include "i_interface.h" +#include "i_time.h" #include "menu.h" -#include "vm.h" -#include "v_video.h" -#include "i_system.h" -#include "types.h" +#include "p_tick.h" +#include "r_utility.h" +#include "s_music.h" +#include "shiftstate.h" +#include "startscreen.h" +#include "teaminfo.h" #include "texturemanager.h" #include "v_draw.h" #include "vm.h" -#include "gamestate.h" -#include "i_interface.h" -#include "gi.h" -#include "g_game.h" -#include "g_level.h" -#include "d_event.h" -#include "p_tick.h" -#include "startscreen.h" -#include "d_main.h" -#include "i_system.h" -#include "doommenu.h" -#include "r_utility.h" -#include "gameconfigfile.h" -#include "d_player.h" -#include "teaminfo.h" -#include "i_time.h" -#include "shiftstate.h" -#include "s_music.h" -#include "hwrenderer/scene/hw_drawinfo.h" EXTERN_CVAR(Int, cl_gfxlocalization) EXTERN_CVAR(Bool, m_quickexit) @@ -76,7 +65,8 @@ EXTERN_CVAR(Bool, quicksaverotation) EXTERN_CVAR(Bool, show_messages) EXTERN_CVAR(Float, hud_scalefactor) -CVAR(Bool, m_simpleoptions, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) +CVAR(Bool, m_simpleoptions, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG); +CVAR(Bool, m_simpleoptions_view, true, 0); typedef void(*hfunc)(); DMenu* CreateMessageBoxMenu(DMenu* parent, const char* message, int messagemode, bool playsound, FName action = NAME_None, hfunc handler = nullptr); @@ -269,10 +259,17 @@ bool M_SetSpecialMenu(FName& menu, int param) break; case NAME_OptionsMenu: + if (m_simpleoptions_view != m_simpleoptions) + m_simpleoptions_view->SetGenericRep(m_simpleoptions->ToInt(), CVAR_Bool); if (m_simpleoptions) menu = NAME_OptionsMenuSimple; break; + case NAME_OptionsMenuSimple: + if (!m_simpleoptions_view) m_simpleoptions_view->SetGenericRep(true, CVAR_Bool); + break; + case NAME_OptionsMenuFull: + if (m_simpleoptions_view) m_simpleoptions_view->SetGenericRep(false, CVAR_Bool); menu = NAME_OptionsMenu; break; From e2e9e76709149193bb3691c1b2cc7d51ffd8a61d Mon Sep 17 00:00:00 2001 From: VileCornstarch Date: Sat, 12 Jul 2025 16:25:24 -0300 Subject: [PATCH 307/384] Expose CloseDialog --- src/p_conversation.cpp | 1 + wadsrc/static/zscript/ui/menu/conversationmenu.zs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/p_conversation.cpp b/src/p_conversation.cpp index a43204e64..a8a04db43 100644 --- a/src/p_conversation.cpp +++ b/src/p_conversation.cpp @@ -761,3 +761,4 @@ DEFINE_FIELD(FStrifeDialogueReply, LogString); DEFINE_FIELD(FStrifeDialogueReply, NextNode); DEFINE_FIELD(FStrifeDialogueReply, LogNumber); DEFINE_FIELD(FStrifeDialogueReply, NeedsGold); +DEFINE_FIELD(FStrifeDialogueReply, CloseDialog); diff --git a/wadsrc/static/zscript/ui/menu/conversationmenu.zs b/wadsrc/static/zscript/ui/menu/conversationmenu.zs index 4e8210b1a..96d24da00 100644 --- a/wadsrc/static/zscript/ui/menu/conversationmenu.zs +++ b/wadsrc/static/zscript/ui/menu/conversationmenu.zs @@ -65,6 +65,7 @@ struct StrifeDialogueReply native version("2.4") native int NextNode; // index into StrifeDialogues native int LogNumber; native bool NeedsGold; + native bool CloseDialog; native bool ShouldSkipReply(PlayerInfo player); } From 0432a7b6dfbc844145b44b3b695470bdf2b1ad81 Mon Sep 17 00:00:00 2001 From: Boondorl Date: Sat, 5 Jul 2025 20:39:24 -0400 Subject: [PATCH 308/384] Support WAD loading tweaks Disable auto loading support wads (e.g. id24) in net games (this should always be done explicitly). Add an option for it in the launcher settings. --- src/d_iwad.cpp | 10 ++++++---- src/launcher/settingspage.cpp | 9 +++++++++ src/launcher/settingspage.h | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/d_iwad.cpp b/src/d_iwad.cpp index 755dfac3e..f5db8b4a9 100644 --- a/src/d_iwad.cpp +++ b/src/d_iwad.cpp @@ -58,7 +58,7 @@ EXTERN_CVAR(Bool, autoloadbrightmaps) EXTERN_CVAR(Bool, autoloadwidescreen) EXTERN_CVAR(String, language) -CVAR(Int, i_loadsupportwad, 1, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // 0=never, 1=singleplayer only, 2=always +CVAR(Bool, i_loadsupportwad, true, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // Disabled in net games. bool foundprio = false; // global to prevent iwad box from appearing @@ -772,6 +772,7 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char if (autoloadlights) flags |= 2; if (autoloadbrightmaps) flags |= 4; if (autoloadwidescreen) flags |= 8; + if (i_loadsupportwad) flags |= 16; FStartupSelectionInfo info = FStartupSelectionInfo(wads, *Args, flags); if (I_PickIWad(queryiwad, info)) @@ -781,6 +782,7 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char autoloadlights = !!(info.DefaultStartFlags & 2); autoloadbrightmaps = !!(info.DefaultStartFlags & 4); autoloadwidescreen = !!(info.DefaultStartFlags & 8); + i_loadsupportwad = !!(info.DefaultStartFlags & 16); } else { @@ -813,9 +815,9 @@ int FIWadManager::IdentifyVersion (std::vector&wadfiles, const char if(info.SupportWAD.IsNotEmpty()) { - bool wantsnetgame = (Args->CheckParm("-join") || Args->CheckParm("-host")); - - if ((!wantsnetgame && i_loadsupportwad == 1) || (i_loadsupportwad == 2)) + // For net games all wads must be explicitly named to make it easier for the host to know + // exactly what's being loaded. + if (i_loadsupportwad && !Args->CheckParm("-join") && !Args->CheckParm("-host")) { FString supportWAD = IWADPathFileSearch(info.SupportWAD); diff --git a/src/launcher/settingspage.cpp b/src/launcher/settingspage.cpp index 14b9f2db0..f1f257a48 100644 --- a/src/launcher/settingspage.cpp +++ b/src/launcher/settingspage.cpp @@ -20,6 +20,7 @@ SettingsPage::SettingsPage(LauncherWindow* launcher, const FStartupSelectionInfo LightsCheckbox = new CheckboxLabel(this); BrightmapsCheckbox = new CheckboxLabel(this); WidescreenCheckbox = new CheckboxLabel(this); + SupportWadsCheckbox = new CheckboxLabel(this); FullscreenCheckbox->SetChecked(info.DefaultFullscreen); DontAskAgainCheckbox->SetChecked(!info.DefaultQueryIWAD); @@ -28,6 +29,7 @@ SettingsPage::SettingsPage(LauncherWindow* launcher, const FStartupSelectionInfo LightsCheckbox->SetChecked(info.DefaultStartFlags & 2); BrightmapsCheckbox->SetChecked(info.DefaultStartFlags & 4); WidescreenCheckbox->SetChecked(info.DefaultStartFlags & 8); + SupportWadsCheckbox->SetChecked(info.DefaultStartFlags & 16); #ifdef RENDER_BACKENDS BackendLabel = new TextLabel(this); @@ -110,6 +112,7 @@ void SettingsPage::SetValues(FStartupSelectionInfo& info) const if (LightsCheckbox->GetChecked()) flags |= 2; if (BrightmapsCheckbox->GetChecked()) flags |= 4; if (WidescreenCheckbox->GetChecked()) flags |= 8; + if (SupportWadsCheckbox->GetChecked()) flags |= 16; info.DefaultStartFlags = flags; #ifdef RENDER_BACKENDS @@ -132,6 +135,7 @@ void SettingsPage::UpdateLanguage() LightsCheckbox->SetText(GStrings.GetString("PICKER_LIGHTS")); BrightmapsCheckbox->SetText(GStrings.GetString("PICKER_BRIGHTMAPS")); WidescreenCheckbox->SetText(GStrings.GetString("PICKER_WIDESCREEN")); + SupportWadsCheckbox->SetText(GStrings.GetString("PICKER_SUPPORTWADS")); #ifdef RENDER_BACKENDS BackendLabel->SetText(GStrings.GetString("PICKER_PREFERBACKEND")); @@ -172,6 +176,10 @@ void SettingsPage::OnGeometryChanged() WidescreenCheckbox->SetFrameGeometry(w - panelWidth, y, panelWidth, WidescreenCheckbox->GetPreferredHeight()); y += DontAskAgainCheckbox->GetPreferredHeight(); + SupportWadsCheckbox->SetFrameGeometry(0.0, y, 190.0, SupportWadsCheckbox->GetPreferredHeight()); + y += SupportWadsCheckbox->GetPreferredHeight(); + const double optionsBottom = y; + #ifdef RENDER_BACKENDS double x = w / 2 - panelWidth / 2; y = 0; @@ -188,6 +196,7 @@ void SettingsPage::OnGeometryChanged() y += GLESCheckbox->GetPreferredHeight(); #endif + y = max(y, optionsBottom); if (!hideLanguage) { LangLabel->SetFrameGeometry(0.0, y, w, LangLabel->GetPreferredHeight()); diff --git a/src/launcher/settingspage.h b/src/launcher/settingspage.h index 1b96a1e6c..c15d179ad 100644 --- a/src/launcher/settingspage.h +++ b/src/launcher/settingspage.h @@ -33,6 +33,7 @@ private: CheckboxLabel* LightsCheckbox = nullptr; CheckboxLabel* BrightmapsCheckbox = nullptr; CheckboxLabel* WidescreenCheckbox = nullptr; + CheckboxLabel* SupportWadsCheckbox = nullptr; #ifdef RENDER_BACKENDS TextLabel* BackendLabel = nullptr; CheckboxLabel* VulkanCheckbox = nullptr; From 38373500a5c759b9ebefc2c383920b37314ecea7 Mon Sep 17 00:00:00 2001 From: Florian Piesche Date: Mon, 7 Jul 2025 17:30:46 +0100 Subject: [PATCH 309/384] Add and install FreeDesktop metadata for Linux --- src/CMakeLists.txt | 13 ++ .../freedesktop/org.zdoom.GZDoom.desktop | 11 ++ .../freedesktop/org.zdoom.GZDoom.metainfo.xml | 149 ++++++++++++++++++ src/posix/freedesktop/org.zdoom.GZDoom.svg | 114 ++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 src/posix/freedesktop/org.zdoom.GZDoom.desktop create mode 100644 src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml create mode 100644 src/posix/freedesktop/org.zdoom.GZDoom.svg diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e305535c4..515a7e769 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1420,6 +1420,19 @@ install(DIRECTORY "${PROJECT_BINARY_DIR}/soundfonts" "${PROJECT_BINARY_DIR}/fm_b DESTINATION ${INSTALL_SOUNDFONT_PATH} COMPONENT "Soundfont resources") +# Install Linux desktop launcher, icon and metadata +if( UNIX AND NOT APPLE ) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/posix/freedesktop/org.zdoom.GZDoom.desktop + DESTINATION share/applications + PERMISSIONS WORLD_EXECUTE WORLD_READ GROUP_READ GROUP_EXECUTE OWNER_READ OWNER_WRITE OWNER_EXECUTE) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml + DESTINATION share/metainfo + PERMISSIONS WORLD_READ GROUP_READ GROUP_EXECUTE OWNER_READ OWNER_WRITE) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/posix/org.zdoom.GZDoom.svg + DESTINATION share/icons/hicolor/scalable/apps + PERMISSIONS WORLD_READ GROUP_READ GROUP_EXECUTE OWNER_READ OWNER_WRITE) +endif() + if( DEM_CMAKE_COMPILER_IS_GNUCXX_COMPATIBLE ) # Need to enable intrinsics for these files. set_property( SOURCE diff --git a/src/posix/freedesktop/org.zdoom.GZDoom.desktop b/src/posix/freedesktop/org.zdoom.GZDoom.desktop new file mode 100644 index 000000000..748253b86 --- /dev/null +++ b/src/posix/freedesktop/org.zdoom.GZDoom.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=GZDoom +Comment=Multi-game launcher for Doom era games +Icon=org.zdoom.GZDoom +Categories=Game;Shooter; +Exec=gzdoom +StartupNotify=true +PrefersNonDefaultGPU=true +Terminal=false +Type=Application +Keywords=Doom;Heretic;Hexen;strife;pwad;iwad;first;person;shooter; diff --git a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml new file mode 100644 index 000000000..e4261f524 --- /dev/null +++ b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml @@ -0,0 +1,149 @@ + + + org.zdoom.GZDoom + org.zdoom.GZDoom.desktop + CC0-1.0 + GPL-3.0 + + GZDoom + + ZDoom team + https://github.com/zdoom/GZDDoom/graphs/contributors + + Game engine for Classic Doom + + https://zdoom.org/index + https://github.com/coelckers/gzdoom + https://zdoom.org/wiki/Main_Page + https://zdoom.org/about + https://forum.zdoom.org/ + https://github.com/ZDoom/gzdoom + + + #999999 + #222222 + + + +

    GZDoom is a source port for the modern era, supporting current hardware and operating systems + and sporting a vast array of user options. Make Doom your own again!

    +

    In addition to Doom, GZDoom supports Heretic, Hexen, Strife, Chex Quest, and fan-created + games like Harmony and Hacx. Meet the entire idTech 1 family!

    +

    Experience mind-bending user-created mods, made possible by ZDoom's advanced mapping features + and the new ZScript language. Or make a mod of your own!

    +
      +
    • Can play all Doom engine games, including Ultimate Doom, Doom II, Heretic, + Hexen, Strife, and more
    • +
    • Supports all the editing features of Hexen. (ACS, hubs, new map format, etc.)
    • +
    • Supports most of the Boom editing features
    • +
    • Features complete translations of Doom, Heretic, Hexen, Strife and other games into + over ten different languages with Unicode support for Latin, Cyrillic, and Hangul so far
    • +
    • All Doom limits are gone
    • +
    • Several softsynths for MUS and MIDI playback, including an OPL softsynth for + an authentic "oldschool" flavor
    • +
    • High resolutions
    • +
    • Quake-style console and key bindings
    • +
    • Crosshairs
    • +
    • Free look (look up/down)
    • +
    • Jumping, crouching, swimming, and flying
    • +
    • Up to 8 player network games using UDP/IP, including team-based gameplay
    • +
    • Support for the Bloodbath announcer from the classic Monolith game Blood
    • +
    • Walk over/under monsters and other things
    • +
    +

    Commercial data files are required to run the supported games. For more info about all + supported games and their data files, see Help -> List of supported games.

    +

    For the Flatpak distribution of GZDoom, all file access is restricted to + ~/.var/app/org.zdoom.GZDoom/.config/gzdoom by default for security reasons. + You will need to place your IWADs and PWADS there, or use an application like Flatseal + to give GZDoom additional permissions.

    + + + + + https://cdn.jsdelivr.net/gh/flathub/org.zdoom.GZDoom/images/image_02.png + Doom screenshot + + + https://cdn.jsdelivr.net/gh/flathub/org.zdoom.GZDoom/images/image_01.png + Strife screenshot + + + https://cdn.jsdelivr.net/gh/flathub/org.zdoom.GZDoom/images/image_03.png + Heretic screenshot + + + https://cdn.jsdelivr.net/gh/flathub/org.zdoom.GZDoom/images/image_04.png + Hexen screenshot + + + + + gamepad + + + keyboard + pointing + + + gzdoom + + + + retro + fps + vulkan + shooter + sourceport + doom + + + + Game + Shooter + + + + + https://forum.zdoom.org/viewtopic.php?t=80447 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + intense + intense + moderate + + +
    diff --git a/src/posix/freedesktop/org.zdoom.GZDoom.svg b/src/posix/freedesktop/org.zdoom.GZDoom.svg new file mode 100644 index 000000000..1598de7bb --- /dev/null +++ b/src/posix/freedesktop/org.zdoom.GZDoom.svg @@ -0,0 +1,114 @@ + + From d1304ec74f423a4e9f7a7bcc6785c0d999fa7fe9 Mon Sep 17 00:00:00 2001 From: Florian Piesche Date: Tue, 8 Jul 2025 11:54:42 +0100 Subject: [PATCH 310/384] Update org.zdoom.GZDoom.metainfo.xml --- src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml index e4261f524..abccc05ad 100644 --- a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml +++ b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml @@ -30,7 +30,7 @@

    In addition to Doom, GZDoom supports Heretic, Hexen, Strife, Chex Quest, and fan-created games like Harmony and Hacx. Meet the entire idTech 1 family!

    Experience mind-bending user-created mods, made possible by ZDoom's advanced mapping features - and the new ZScript language. Or make a mod of your own!

    + and the ZScript language for complex game mechanics and behaviors. Or make a mod of your own!

    • Can play all Doom engine games, including Ultimate Doom, Doom II, Heretic, Hexen, Strife, and more
    • From 88c2eac56bc19022b3702ec9045eddc765166080 Mon Sep 17 00:00:00 2001 From: Florian Piesche Date: Tue, 8 Jul 2025 17:07:18 +0100 Subject: [PATCH 311/384] Update org.zdoom.GZDoom.metainfo.xml --- src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml index abccc05ad..5c4459516 100644 --- a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml +++ b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml @@ -46,7 +46,7 @@
    • Crosshairs
    • Free look (look up/down)
    • Jumping, crouching, swimming, and flying
    • -
    • Up to 8 player network games using UDP/IP, including team-based gameplay
    • +
    • Network games for up to 64 players, including team-based gameplay
    • Support for the Bloodbath announcer from the classic Monolith game Blood
    • Walk over/under monsters and other things
    From fcf529590fe6e3182ab0168e1e2b601c0b235002 Mon Sep 17 00:00:00 2001 From: Florian Piesche Date: Tue, 8 Jul 2025 18:38:58 +0100 Subject: [PATCH 312/384] Fix malformed XML --- src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml index 5c4459516..6e621dd2e 100644 --- a/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml +++ b/src/posix/freedesktop/org.zdoom.GZDoom.metainfo.xml @@ -56,7 +56,7 @@ ~/.var/app/org.zdoom.GZDoom/.config/gzdoom by default for security reasons. You will need to place your IWADs and PWADS there, or use an application like Flatseal to give GZDoom additional permissions.

    - + From 45ac7c1d9eab7df86e21b83131ba04a9c530f334 Mon Sep 17 00:00:00 2001 From: Nikita <69168929+nikitalita@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:01:18 -0700 Subject: [PATCH 313/384] ZScript DAP Debug server (#3009) * Add STL-standard type traits and functions to TMap to enable for loop iteration * add third-party range-map library I have e-mailed the author for clarifcation on the license, will update this when they respond * vcpkg: Add cppdap and eventpp libraries, update baseline * DAP implementation * Add `FileSystem::FileHash()` to get the CRC32 hash * add `starting_offset` param to `VMDisasm()` for debugger disassembly view Defaults to `0`, should not change output if it's not set * Add `VMFrameStack::HasFrames()` to prevent assertions when inspecting in the debugger * Add `PC` field to VMFrame, ensure that it is updated whenever vm increments/decrements the pc Does not change alignment, the offsets used in VMFrame still work We need this for the debugger because we otherwise have no way to get the pc; it was a local in `ExecScriptFunc()` * `ZCCCompiler::CreateClassTypes()`: ensure SourceLumpName is set for classes derived from non-native classes * start debug server in d_main, add `vm_debug` cvars and `-debug` CLI arg * Add documentation for `vm_debug` cvars * vm_exec: Add debugger hooks for instruction execution events * c_console: Add debugger hooks for logging events in `PrintString` * add `.cache/` to gitignore * vendor cppdap on main @ 6464cd7 Patches: removed .gitmodules (submodules were thirdparty/json, thirdparty/googletest) removed thirdparty/googletest, not needed removed thirdparty/json/docs, thirdparty/json/test, thirdparty/json/benchmarks to prevent massive bloat * vendor eventpp on master @ 1224dd6 * build: use internal cppdap and eventpp by default * dap: fix Binary::GetFunctionLineRange() * fix bug in range_map::find_ranges * make Binary dap::Source dynamic * cache source code upon retrieval * refactor Binary into a class, cleanup PexCache * fix ending session gracefully * d_main: Stop debug server in D_Cleanup() * Fix connecting to debugger when session already started * cleanup unused stuff in ZScriptDebugger * always send TerminateEvents on disconnect if initialized * tweak color display * Don't cache disassembly lines when scanning scripts * Cache nodes when getting runtime state * WIP display locals * Fix display of static arrays * Ensure names display in proper order * dap: Fix struct locals display, add args display * Support `start` parameter * Support `filter` parameter * remove struct unbound native data display Practically useless for debugging zscript and didn't work properly anyway * d_main: fix vm_jit and vm_jit not being disabled soon enough * support breaking on abort exceptions * dap: refactor game event emitter functions into seperate header * dap: show native functions on stack * dap: Remove "Native" from exception handling, simplify exception event emit * dap: add instruction breakpoints * dap: fix display of locals not in scope yet * dap: Make disassembly view display invalid instructions for non-code addresses * dap: remove dot initializers * dap: fix local structs in scope * dap: don't parse the non-used options in the launch/attach requests * dap: fix local struct view * dap: Fix displaying objects that aren't their actual types * dap: Fix action and state handling * dap: stack display view * dap: fix object display view * d_dehacked: set qualified name in addition to the printable name * dap: fix displaying breakpoint errors when script isn't loaded * dap: remove debug print * dap: Display parameter names * dap: Turn down verbosity of logging * dap: fix disassembly view * dap: fix performance problems with arrays * c_console: emit event only if not PRINT_NODAPEVENT * dap: improve logging * dap: update upstream cppdap library to fix deadlocks on no bind * dap: Fix ending session on client socket closed * dap: prevent DebugServer.h from pulling in `dap` and `ZScriptDebugger.h` * dap: fix pause event not being emitted on pause * dap: remove eventpp emitters, way too slow * Remove eventpp dependency * dap: Display correct register names * dap: Show special inits in registers * dap: Add stack offset to VMLocalVariable * dap: fix display of static arrays and local variables on stack vs. registers * dap: fix displaying function pointers * dap: tweak color display * dap: fix scalar display < 4 * dap: add Globals display to debugger * dap: unify methods to get vmvalue * dap: rename free method to freeValue to avoid running afoul of macro defs * fix windows builds * fix compile on linux * cleanup * Fix display of function breakpoints * dap: Don't send back binary files * dap: include sbarinfo in script types (no debug support for anything but zscript yet, this is just for returning source info) * dap: don't show ending session message unless initialized * fix erroneous commit * dap: handle evaluate requests * Fix getting bitfield values * Add CVars Scope and evaluation * add running console commands from repl * dap: disable commands via repl for now * fix loading functions DECORATE scripts * Add source information to Dehacked VM functions, add debugging support * dap: cleanup * fix resolving archive paths * don't send source back on native stack frames * handle `modules` request * cleanup * allow evaluating cvars on hover * fix oob bpinfos * fix restarting the game blowing out the debug server * dap: process input events while paused to prevent deadlocks * fix getting local state * dap: fix LocalState alignment * dap: fix DumpStateHelper * update cppdap protocol version to 1.68.0 * remove cppdap from vcpkg deps We can't use the upstream version anyway because the maintainers are not merging our patches * dap: make named variable nodes derive from the same class * dap: make cvar scope available in native stack frames * handle local variables with conflicting names * add I_GetWindowEvent() to win32 to only process window events when debugging is paused * dap: fix evaluate * dap: fix display of `out` variables --- .gitignore | 1 + CMakeLists.txt | 17 + docs/console.html | 8 + libraries/cppdap/.gitattributes | 4 + libraries/cppdap/.github/workflows/main.yml | 48 + libraries/cppdap/.gitignore | 6 + libraries/cppdap/CMakeLists.txt | 394 + libraries/cppdap/CONTRIBUTING | 28 + libraries/cppdap/LICENSE | 202 + libraries/cppdap/README.md | 79 + libraries/cppdap/clang-format-all.sh | 21 + libraries/cppdap/cmake/Config.cmake.in | 25 + libraries/cppdap/examples/hello_debugger.cpp | 469 + .../examples/simple_net_client_server.cpp | 109 + libraries/cppdap/examples/vscode/package.json | 28 + libraries/cppdap/fuzz/dictionary.txt | 372 + libraries/cppdap/fuzz/fuzz.cpp | 128 + libraries/cppdap/fuzz/fuzz.h | 76 + libraries/cppdap/fuzz/run.sh | 19 + libraries/cppdap/fuzz/seed/empty_json | 1 + libraries/cppdap/fuzz/seed/request | 8 + libraries/cppdap/include/dap/any.h | 211 + libraries/cppdap/include/dap/dap.h | 35 + libraries/cppdap/include/dap/future.h | 179 + libraries/cppdap/include/dap/io.h | 97 + libraries/cppdap/include/dap/network.h | 71 + libraries/cppdap/include/dap/optional.h | 263 + libraries/cppdap/include/dap/protocol.h | 2909 ++ libraries/cppdap/include/dap/serialization.h | 253 + libraries/cppdap/include/dap/session.h | 460 + libraries/cppdap/include/dap/traits.h | 159 + libraries/cppdap/include/dap/typeinfo.h | 59 + libraries/cppdap/include/dap/typeof.h | 266 + libraries/cppdap/include/dap/types.h | 104 + libraries/cppdap/include/dap/variant.h | 108 + .../cppdap/kokoro/license-check/build.sh | 25 + .../cppdap/kokoro/license-check/presubmit.cfg | 4 + .../macos/clang-x64/cmake/presubmit.cfg | 14 + libraries/cppdap/kokoro/macos/presubmit.sh | 37 + .../ubuntu/gcc-x64/cmake/asan/presubmit.cfg | 19 + .../kokoro/ubuntu/gcc-x64/cmake/presubmit.cfg | 14 + .../ubuntu/gcc-x64/cmake/tsan/presubmit.cfg | 19 + .../cppdap/kokoro/ubuntu/presubmit-docker.sh | 56 + libraries/cppdap/kokoro/ubuntu/presubmit.sh | 32 + libraries/cppdap/kokoro/windows/presubmit.bat | 45 + .../windows/vs2022-amd64/cmake/presubmit.cfg | 19 + .../windows/vs2022-x86/cmake/presubmit.cfg | 19 + libraries/cppdap/license-checker.cfg | 28 + libraries/cppdap/src/any_test.cpp | 262 + libraries/cppdap/src/chan.h | 90 + libraries/cppdap/src/chan_test.cpp | 35 + libraries/cppdap/src/content_stream.cpp | 198 + libraries/cppdap/src/content_stream.h | 73 + libraries/cppdap/src/content_stream_test.cpp | 126 + libraries/cppdap/src/dap_test.cpp | 72 + libraries/cppdap/src/io.cpp | 258 + libraries/cppdap/src/json_serializer.h | 47 + libraries/cppdap/src/json_serializer_test.cpp | 266 + .../cppdap/src/jsoncpp_json_serializer.cpp | 272 + .../cppdap/src/jsoncpp_json_serializer.h | 134 + libraries/cppdap/src/network.cpp | 107 + libraries/cppdap/src/network_test.cpp | 110 + .../cppdap/src/nlohmann_json_serializer.cpp | 260 + .../cppdap/src/nlohmann_json_serializer.h | 133 + libraries/cppdap/src/null_json_serializer.cpp | 23 + libraries/cppdap/src/null_json_serializer.h | 47 + libraries/cppdap/src/optional_test.cpp | 169 + libraries/cppdap/src/protocol_events.cpp | 127 + libraries/cppdap/src/protocol_requests.cpp | 293 + libraries/cppdap/src/protocol_response.cpp | 262 + libraries/cppdap/src/protocol_types.cpp | 333 + .../cppdap/src/rapid_json_serializer.cpp | 290 + libraries/cppdap/src/rapid_json_serializer.h | 138 + libraries/cppdap/src/rwmutex.h | 172 + libraries/cppdap/src/rwmutex_test.cpp | 113 + libraries/cppdap/src/session.cpp | 521 + libraries/cppdap/src/session_test.cpp | 625 + libraries/cppdap/src/socket.cpp | 337 + libraries/cppdap/src/socket.h | 47 + libraries/cppdap/src/socket_test.cpp | 104 + libraries/cppdap/src/string_buffer.h | 95 + libraries/cppdap/src/traits_test.cpp | 387 + libraries/cppdap/src/typeinfo.cpp | 21 + libraries/cppdap/src/typeinfo_test.cpp | 65 + libraries/cppdap/src/typeof.cpp | 144 + libraries/cppdap/src/variant_test.cpp | 94 + .../third_party/json/.circleci/config.yml | 27 + libraries/cppdap/third_party/json/.clang-tidy | 26 + .../cppdap/third_party/json/.doozer.json | 82 + .../third_party/json/.github/CODEOWNERS | 6 + .../third_party/json/.github/CONTRIBUTING.md | 71 + .../third_party/json/.github/FUNDING.yml | 1 + .../json/.github/ISSUE_TEMPLATE/Bug_report.md | 22 + .../.github/ISSUE_TEMPLATE/Feature_request.md | 12 + .../json/.github/ISSUE_TEMPLATE/question.md | 16 + .../json/.github/PULL_REQUEST_TEMPLATE.md | 19 + .../third_party/json/.github/SECURITY.md | 5 + .../third_party/json/.github/config.yml | 19 + .../cppdap/third_party/json/.github/stale.yml | 17 + .../json/.github/workflows/ccpp.yml | 19 + libraries/cppdap/third_party/json/.gitignore | 25 + libraries/cppdap/third_party/json/.travis.yml | 345 + .../cppdap/third_party/json/CMakeLists.txt | 131 + .../third_party/json/CODE_OF_CONDUCT.md | 46 + .../cppdap/third_party/json/ChangeLog.md | 1670 ++ libraries/cppdap/third_party/json/LICENSE.MIT | 21 + libraries/cppdap/third_party/json/Makefile | 629 + libraries/cppdap/third_party/json/README.md | 1371 + .../cppdap/third_party/json/appveyor.yml | 111 + .../third_party/json/cmake/config.cmake.in | 15 + .../json/include/nlohmann/adl_serializer.hpp | 49 + .../nlohmann/detail/conversions/from_json.hpp | 389 + .../nlohmann/detail/conversions/to_chars.hpp | 1106 + .../nlohmann/detail/conversions/to_json.hpp | 347 + .../include/nlohmann/detail/exceptions.hpp | 356 + .../nlohmann/detail/input/binary_reader.hpp | 1983 ++ .../nlohmann/detail/input/input_adapters.hpp | 442 + .../nlohmann/detail/input/json_sax.hpp | 701 + .../include/nlohmann/detail/input/lexer.hpp | 1512 + .../include/nlohmann/detail/input/parser.hpp | 498 + .../nlohmann/detail/input/position_t.hpp | 27 + .../detail/iterators/internal_iterator.hpp | 25 + .../nlohmann/detail/iterators/iter_impl.hpp | 638 + .../detail/iterators/iteration_proxy.hpp | 176 + .../detail/iterators/iterator_traits.hpp | 51 + .../iterators/json_reverse_iterator.hpp | 119 + .../detail/iterators/primitive_iterator.hpp | 120 + .../include/nlohmann/detail/json_pointer.hpp | 1011 + .../json/include/nlohmann/detail/json_ref.hpp | 69 + .../include/nlohmann/detail/macro_scope.hpp | 121 + .../include/nlohmann/detail/macro_unscope.hpp | 21 + .../nlohmann/detail/meta/cpp_future.hpp | 63 + .../include/nlohmann/detail/meta/detected.hpp | 58 + .../include/nlohmann/detail/meta/is_sax.hpp | 142 + .../nlohmann/detail/meta/type_traits.hpp | 361 + .../include/nlohmann/detail/meta/void_t.hpp | 13 + .../nlohmann/detail/output/binary_writer.hpp | 1335 + .../detail/output/output_adapters.hpp | 123 + .../nlohmann/detail/output/serializer.hpp | 866 + .../json/include/nlohmann/detail/value_t.hpp | 77 + .../json/include/nlohmann/json.hpp | 8134 ++++++ .../json/include/nlohmann/json_fwd.hpp | 64 + .../nlohmann/thirdparty/hedley/hedley.hpp | 1595 ++ .../thirdparty/hedley/hedley_undef.hpp | 128 + libraries/cppdap/third_party/json/meson.build | 23 + .../third_party/json/nlohmann_json.natvis | 32 + .../json/single_include/nlohmann/json.hpp | 22815 ++++++++++++++++ .../json/third_party/amalgamate/CHANGES.md | 10 + .../json/third_party/amalgamate/LICENSE.md | 27 + .../json/third_party/amalgamate/README.md | 66 + .../json/third_party/amalgamate/amalgamate.py | 299 + .../json/third_party/amalgamate/config.json | 8 + .../json/third_party/cpplint/LICENSE | 27 + .../json/third_party/cpplint/README.rst | 80 + .../json/third_party/cpplint/cpplint.py | 6583 +++++ .../json/third_party/cpplint/update.sh | 5 + .../cppdap/tools/protocol_gen/protocol_gen.go | 982 + .../include/range_map/internal/rm_base.h | 160 + .../include/range_map/internal/rm_iter.h | 208 + .../include/range_map/internal/rm_node.h | 271 + .../range_map/include/range_map/range_map.h | 1342 + src/CMakeLists.txt | 37 +- src/common/console/c_console.cpp | 7 +- src/common/engine/i_net.cpp | 11 +- src/common/engine/printf.h | 1 + src/common/filesystem/include/fs_filesystem.h | 1 + src/common/filesystem/include/resourcefile.h | 5 + src/common/filesystem/source/filesystem.cpp | 18 + src/common/platform/win32/i_input.cpp | 84 +- src/common/platform/win32/i_input.h | 2 + src/common/scripting/backend/codegen.cpp | 5 + src/common/scripting/backend/vmbuilder.cpp | 13 + src/common/scripting/backend/vmbuilder.h | 8 +- src/common/scripting/core/vmdisasm.cpp | 8 +- .../scripting/dap/BreakpointManager.cpp | 492 + src/common/scripting/dap/BreakpointManager.h | 76 + .../scripting/dap/DebugExecutionManager.cpp | 388 + .../scripting/dap/DebugExecutionManager.h | 90 + src/common/scripting/dap/DebugServer.cpp | 161 + src/common/scripting/dap/DebugServer.h | 46 + src/common/scripting/dap/GameEventEmit.h | 15 + src/common/scripting/dap/GameInterfaces.h | 1159 + src/common/scripting/dap/IdHandleBase.cpp | 1 + src/common/scripting/dap/IdHandleBase.h | 20 + src/common/scripting/dap/IdMap.cpp | 1 + src/common/scripting/dap/IdMap.h | 101 + src/common/scripting/dap/IdProvider.cpp | 10 + src/common/scripting/dap/IdProvider.h | 13 + .../scripting/dap/Nodes/ArrayStateNode.cpp | 236 + .../scripting/dap/Nodes/ArrayStateNode.h | 28 + .../dap/Nodes/CVarScopeStateNode.cpp | 195 + .../scripting/dap/Nodes/CVarScopeStateNode.h | 48 + src/common/scripting/dap/Nodes/DummyNode.cpp | 49 + src/common/scripting/dap/Nodes/DummyNode.h | 30 + .../dap/Nodes/GlobalScopeStateNode.cpp | 141 + .../dap/Nodes/GlobalScopeStateNode.h | 20 + .../dap/Nodes/LocalScopeStateNode.cpp | 146 + .../scripting/dap/Nodes/LocalScopeStateNode.h | 27 + .../scripting/dap/Nodes/ObjectStateNode.cpp | 175 + .../scripting/dap/Nodes/ObjectStateNode.h | 31 + .../dap/Nodes/RegistersScopeStateNode.cpp | 261 + .../dap/Nodes/RegistersScopeStateNode.h | 135 + .../dap/Nodes/StackFrameStateNode.cpp | 139 + .../scripting/dap/Nodes/StackFrameStateNode.h | 35 + .../scripting/dap/Nodes/StackStateNode.cpp | 83 + .../scripting/dap/Nodes/StackStateNode.h | 22 + .../scripting/dap/Nodes/StateNodeBase.cpp | 43 + .../scripting/dap/Nodes/StateNodeBase.h | 67 + .../scripting/dap/Nodes/StatePointerNode.cpp | 260 + .../scripting/dap/Nodes/StatePointerNode.h | 21 + .../scripting/dap/Nodes/StructStateNode.cpp | 83 + .../scripting/dap/Nodes/StructStateNode.h | 25 + .../scripting/dap/Nodes/ValueStateNode.cpp | 243 + .../scripting/dap/Nodes/ValueStateNode.h | 20 + src/common/scripting/dap/PexCache.cpp | 949 + src/common/scripting/dap/PexCache.h | 135 + src/common/scripting/dap/Protocol/debugger.h | 77 + .../dap/Protocol/struct_extensions.cpp | 22 + .../dap/Protocol/struct_extensions.h | 33 + src/common/scripting/dap/RuntimeEvents.cpp | 86 + src/common/scripting/dap/RuntimeEvents.h | 38 + src/common/scripting/dap/RuntimeState.cpp | 302 + src/common/scripting/dap/RuntimeState.h | 35 + src/common/scripting/dap/Utilities.h | 275 + src/common/scripting/dap/ZScriptDebugger.cpp | 807 + src/common/scripting/dap/ZScriptDebugger.h | 104 + src/common/scripting/frontend/zcc_compile.cpp | 1 + src/common/scripting/vm/vmexec.cpp | 3 +- src/common/scripting/vm/vmexec.h | 13 +- src/common/scripting/vm/vmframe.cpp | 25 +- src/common/scripting/vm/vmintern.h | 22 +- src/common/utility/tarray.h | 111 +- src/d_main.cpp | 58 +- src/gamedata/d_dehacked.cpp | 32 +- vcpkg.json | 2 +- 235 files changed, 83166 insertions(+), 62 deletions(-) create mode 100644 libraries/cppdap/.gitattributes create mode 100644 libraries/cppdap/.github/workflows/main.yml create mode 100644 libraries/cppdap/.gitignore create mode 100644 libraries/cppdap/CMakeLists.txt create mode 100644 libraries/cppdap/CONTRIBUTING create mode 100644 libraries/cppdap/LICENSE create mode 100644 libraries/cppdap/README.md create mode 100755 libraries/cppdap/clang-format-all.sh create mode 100644 libraries/cppdap/cmake/Config.cmake.in create mode 100644 libraries/cppdap/examples/hello_debugger.cpp create mode 100644 libraries/cppdap/examples/simple_net_client_server.cpp create mode 100644 libraries/cppdap/examples/vscode/package.json create mode 100644 libraries/cppdap/fuzz/dictionary.txt create mode 100644 libraries/cppdap/fuzz/fuzz.cpp create mode 100644 libraries/cppdap/fuzz/fuzz.h create mode 100755 libraries/cppdap/fuzz/run.sh create mode 100644 libraries/cppdap/fuzz/seed/empty_json create mode 100644 libraries/cppdap/fuzz/seed/request create mode 100644 libraries/cppdap/include/dap/any.h create mode 100644 libraries/cppdap/include/dap/dap.h create mode 100644 libraries/cppdap/include/dap/future.h create mode 100644 libraries/cppdap/include/dap/io.h create mode 100644 libraries/cppdap/include/dap/network.h create mode 100644 libraries/cppdap/include/dap/optional.h create mode 100644 libraries/cppdap/include/dap/protocol.h create mode 100644 libraries/cppdap/include/dap/serialization.h create mode 100644 libraries/cppdap/include/dap/session.h create mode 100644 libraries/cppdap/include/dap/traits.h create mode 100644 libraries/cppdap/include/dap/typeinfo.h create mode 100644 libraries/cppdap/include/dap/typeof.h create mode 100644 libraries/cppdap/include/dap/types.h create mode 100644 libraries/cppdap/include/dap/variant.h create mode 100755 libraries/cppdap/kokoro/license-check/build.sh create mode 100644 libraries/cppdap/kokoro/license-check/presubmit.cfg create mode 100644 libraries/cppdap/kokoro/macos/clang-x64/cmake/presubmit.cfg create mode 100755 libraries/cppdap/kokoro/macos/presubmit.sh create mode 100644 libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/asan/presubmit.cfg create mode 100644 libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/presubmit.cfg create mode 100644 libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/tsan/presubmit.cfg create mode 100755 libraries/cppdap/kokoro/ubuntu/presubmit-docker.sh create mode 100755 libraries/cppdap/kokoro/ubuntu/presubmit.sh create mode 100644 libraries/cppdap/kokoro/windows/presubmit.bat create mode 100644 libraries/cppdap/kokoro/windows/vs2022-amd64/cmake/presubmit.cfg create mode 100644 libraries/cppdap/kokoro/windows/vs2022-x86/cmake/presubmit.cfg create mode 100644 libraries/cppdap/license-checker.cfg create mode 100644 libraries/cppdap/src/any_test.cpp create mode 100644 libraries/cppdap/src/chan.h create mode 100644 libraries/cppdap/src/chan_test.cpp create mode 100644 libraries/cppdap/src/content_stream.cpp create mode 100644 libraries/cppdap/src/content_stream.h create mode 100644 libraries/cppdap/src/content_stream_test.cpp create mode 100644 libraries/cppdap/src/dap_test.cpp create mode 100644 libraries/cppdap/src/io.cpp create mode 100644 libraries/cppdap/src/json_serializer.h create mode 100644 libraries/cppdap/src/json_serializer_test.cpp create mode 100644 libraries/cppdap/src/jsoncpp_json_serializer.cpp create mode 100644 libraries/cppdap/src/jsoncpp_json_serializer.h create mode 100644 libraries/cppdap/src/network.cpp create mode 100644 libraries/cppdap/src/network_test.cpp create mode 100644 libraries/cppdap/src/nlohmann_json_serializer.cpp create mode 100644 libraries/cppdap/src/nlohmann_json_serializer.h create mode 100644 libraries/cppdap/src/null_json_serializer.cpp create mode 100644 libraries/cppdap/src/null_json_serializer.h create mode 100644 libraries/cppdap/src/optional_test.cpp create mode 100644 libraries/cppdap/src/protocol_events.cpp create mode 100644 libraries/cppdap/src/protocol_requests.cpp create mode 100644 libraries/cppdap/src/protocol_response.cpp create mode 100644 libraries/cppdap/src/protocol_types.cpp create mode 100644 libraries/cppdap/src/rapid_json_serializer.cpp create mode 100644 libraries/cppdap/src/rapid_json_serializer.h create mode 100644 libraries/cppdap/src/rwmutex.h create mode 100644 libraries/cppdap/src/rwmutex_test.cpp create mode 100644 libraries/cppdap/src/session.cpp create mode 100644 libraries/cppdap/src/session_test.cpp create mode 100644 libraries/cppdap/src/socket.cpp create mode 100644 libraries/cppdap/src/socket.h create mode 100644 libraries/cppdap/src/socket_test.cpp create mode 100644 libraries/cppdap/src/string_buffer.h create mode 100644 libraries/cppdap/src/traits_test.cpp create mode 100644 libraries/cppdap/src/typeinfo.cpp create mode 100644 libraries/cppdap/src/typeinfo_test.cpp create mode 100644 libraries/cppdap/src/typeof.cpp create mode 100644 libraries/cppdap/src/variant_test.cpp create mode 100644 libraries/cppdap/third_party/json/.circleci/config.yml create mode 100644 libraries/cppdap/third_party/json/.clang-tidy create mode 100644 libraries/cppdap/third_party/json/.doozer.json create mode 100644 libraries/cppdap/third_party/json/.github/CODEOWNERS create mode 100644 libraries/cppdap/third_party/json/.github/CONTRIBUTING.md create mode 100644 libraries/cppdap/third_party/json/.github/FUNDING.yml create mode 100644 libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Bug_report.md create mode 100644 libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Feature_request.md create mode 100644 libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/question.md create mode 100644 libraries/cppdap/third_party/json/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 libraries/cppdap/third_party/json/.github/SECURITY.md create mode 100644 libraries/cppdap/third_party/json/.github/config.yml create mode 100644 libraries/cppdap/third_party/json/.github/stale.yml create mode 100644 libraries/cppdap/third_party/json/.github/workflows/ccpp.yml create mode 100644 libraries/cppdap/third_party/json/.gitignore create mode 100644 libraries/cppdap/third_party/json/.travis.yml create mode 100644 libraries/cppdap/third_party/json/CMakeLists.txt create mode 100644 libraries/cppdap/third_party/json/CODE_OF_CONDUCT.md create mode 100644 libraries/cppdap/third_party/json/ChangeLog.md create mode 100644 libraries/cppdap/third_party/json/LICENSE.MIT create mode 100644 libraries/cppdap/third_party/json/Makefile create mode 100644 libraries/cppdap/third_party/json/README.md create mode 100644 libraries/cppdap/third_party/json/appveyor.yml create mode 100644 libraries/cppdap/third_party/json/cmake/config.cmake.in create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/adl_serializer.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/from_json.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_chars.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_json.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/exceptions.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/input/binary_reader.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/input/input_adapters.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/input/json_sax.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/input/lexer.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/input/parser.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/input/position_t.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/internal_iterator.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iter_impl.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iteration_proxy.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iterator_traits.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/json_reverse_iterator.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/primitive_iterator.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/json_pointer.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/json_ref.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/macro_scope.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/macro_unscope.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/meta/cpp_future.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/meta/detected.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/meta/is_sax.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/meta/type_traits.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/meta/void_t.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/output/binary_writer.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/output/output_adapters.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/output/serializer.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/detail/value_t.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/json.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/json_fwd.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/thirdparty/hedley/hedley.hpp create mode 100644 libraries/cppdap/third_party/json/include/nlohmann/thirdparty/hedley/hedley_undef.hpp create mode 100644 libraries/cppdap/third_party/json/meson.build create mode 100644 libraries/cppdap/third_party/json/nlohmann_json.natvis create mode 100644 libraries/cppdap/third_party/json/single_include/nlohmann/json.hpp create mode 100644 libraries/cppdap/third_party/json/third_party/amalgamate/CHANGES.md create mode 100644 libraries/cppdap/third_party/json/third_party/amalgamate/LICENSE.md create mode 100644 libraries/cppdap/third_party/json/third_party/amalgamate/README.md create mode 100755 libraries/cppdap/third_party/json/third_party/amalgamate/amalgamate.py create mode 100644 libraries/cppdap/third_party/json/third_party/amalgamate/config.json create mode 100644 libraries/cppdap/third_party/json/third_party/cpplint/LICENSE create mode 100644 libraries/cppdap/third_party/json/third_party/cpplint/README.rst create mode 100755 libraries/cppdap/third_party/json/third_party/cpplint/cpplint.py create mode 100755 libraries/cppdap/third_party/json/third_party/cpplint/update.sh create mode 100644 libraries/cppdap/tools/protocol_gen/protocol_gen.go create mode 100644 libraries/range_map/include/range_map/internal/rm_base.h create mode 100644 libraries/range_map/include/range_map/internal/rm_iter.h create mode 100644 libraries/range_map/include/range_map/internal/rm_node.h create mode 100644 libraries/range_map/include/range_map/range_map.h create mode 100644 src/common/scripting/dap/BreakpointManager.cpp create mode 100644 src/common/scripting/dap/BreakpointManager.h create mode 100644 src/common/scripting/dap/DebugExecutionManager.cpp create mode 100644 src/common/scripting/dap/DebugExecutionManager.h create mode 100644 src/common/scripting/dap/DebugServer.cpp create mode 100644 src/common/scripting/dap/DebugServer.h create mode 100644 src/common/scripting/dap/GameEventEmit.h create mode 100644 src/common/scripting/dap/GameInterfaces.h create mode 100644 src/common/scripting/dap/IdHandleBase.cpp create mode 100644 src/common/scripting/dap/IdHandleBase.h create mode 100644 src/common/scripting/dap/IdMap.cpp create mode 100644 src/common/scripting/dap/IdMap.h create mode 100644 src/common/scripting/dap/IdProvider.cpp create mode 100644 src/common/scripting/dap/IdProvider.h create mode 100644 src/common/scripting/dap/Nodes/ArrayStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/ArrayStateNode.h create mode 100644 src/common/scripting/dap/Nodes/CVarScopeStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/CVarScopeStateNode.h create mode 100644 src/common/scripting/dap/Nodes/DummyNode.cpp create mode 100644 src/common/scripting/dap/Nodes/DummyNode.h create mode 100644 src/common/scripting/dap/Nodes/GlobalScopeStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/GlobalScopeStateNode.h create mode 100644 src/common/scripting/dap/Nodes/LocalScopeStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/LocalScopeStateNode.h create mode 100644 src/common/scripting/dap/Nodes/ObjectStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/ObjectStateNode.h create mode 100644 src/common/scripting/dap/Nodes/RegistersScopeStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/RegistersScopeStateNode.h create mode 100644 src/common/scripting/dap/Nodes/StackFrameStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/StackFrameStateNode.h create mode 100644 src/common/scripting/dap/Nodes/StackStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/StackStateNode.h create mode 100644 src/common/scripting/dap/Nodes/StateNodeBase.cpp create mode 100644 src/common/scripting/dap/Nodes/StateNodeBase.h create mode 100644 src/common/scripting/dap/Nodes/StatePointerNode.cpp create mode 100644 src/common/scripting/dap/Nodes/StatePointerNode.h create mode 100644 src/common/scripting/dap/Nodes/StructStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/StructStateNode.h create mode 100644 src/common/scripting/dap/Nodes/ValueStateNode.cpp create mode 100644 src/common/scripting/dap/Nodes/ValueStateNode.h create mode 100644 src/common/scripting/dap/PexCache.cpp create mode 100644 src/common/scripting/dap/PexCache.h create mode 100644 src/common/scripting/dap/Protocol/debugger.h create mode 100644 src/common/scripting/dap/Protocol/struct_extensions.cpp create mode 100644 src/common/scripting/dap/Protocol/struct_extensions.h create mode 100644 src/common/scripting/dap/RuntimeEvents.cpp create mode 100644 src/common/scripting/dap/RuntimeEvents.h create mode 100644 src/common/scripting/dap/RuntimeState.cpp create mode 100644 src/common/scripting/dap/RuntimeState.h create mode 100644 src/common/scripting/dap/Utilities.h create mode 100644 src/common/scripting/dap/ZScriptDebugger.cpp create mode 100644 src/common/scripting/dap/ZScriptDebugger.h diff --git a/.gitignore b/.gitignore index 3a3b7cc3d..d8d361a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ fmodapi*linux/ /src/gl/unused /mapfiles_release/*.map /AppDir +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1088d2483..ba2d6f9ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,9 @@ option( NO_OPENAL "Disable OpenAL sound support" OFF ) find_package( BZip2 ) find_package( VPX ) +if (NOT FORCE_INTERNAL_CPPDAP) + find_package( cppdap CONFIG ) +endif() include( TargetArch ) @@ -334,6 +337,9 @@ set( CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${DEB_C_FLAGS} -D_DEBUG" ) option(FORCE_INTERNAL_BZIP2 "Use internal bzip2") option(FORCE_INTERNAL_ASMJIT "Use internal asmjit" ON) +option(FORCE_INTERNAL_CPPDAP "Use internal cppdap" ON) + + mark_as_advanced( FORCE_INTERNAL_ASMJIT ) if (HAVE_VULKAN) @@ -385,6 +391,17 @@ else() set( BZIP2_LIBRARY bz2 ) endif() +if ( CPPDAP_FOUND AND NOT FORCE_INTERNAL_CPPDAP ) + message( STATUS "Using system cppdap library, includes found at ${CPPDAP_INCLUDE_DIR}" ) +else() + message( STATUS "Using internal cppdap library" ) + add_subdirectory( libraries/cppdap ) + set( CPPDAP_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/cppdap" ) + set( CPPDAP_LIBRARIES cppdap ) + set( CPPDAP_LIBRARY cppdap ) + set( CPPDAP_FOUND TRUE ) +endif() + set( LZMA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/lzma/C" ) if( NOT CMAKE_CROSSCOMPILING ) diff --git a/docs/console.html b/docs/console.html index 7fb5d4c8c..4dd6a0fe5 100644 --- a/docs/console.html +++ b/docs/console.html @@ -1814,6 +1814,14 @@ default: 1.0
    Sends you to the specified coordinates immediately. This can be used with idmypos to debug problems in a specific area of a map.
    +
    vm_debug
    +
    boolean: false
    +
    When set to true, the game will enable the ZScript debug + server on the port specified in vm_debug_port. Note that enabling this + will disable JIT and requires restarting the game.
    +
    vm_debug_port
    +
    integer: 19021
    +
    This is the port that the ZScript debug server will listen on.

    Effects

    diff --git a/libraries/cppdap/.gitattributes b/libraries/cppdap/.gitattributes new file mode 100644 index 000000000..175297ae8 --- /dev/null +++ b/libraries/cppdap/.gitattributes @@ -0,0 +1,4 @@ +* text=auto + +*.sh eol=lf +*.bat eol=crlf diff --git a/libraries/cppdap/.github/workflows/main.yml b/libraries/cppdap/.github/workflows/main.yml new file mode 100644 index 000000000..72d2934e9 --- /dev/null +++ b/libraries/cppdap/.github/workflows/main.yml @@ -0,0 +1,48 @@ +on: + push: + pull_request: + +jobs: + linux: + name: ci + runs-on: ubuntu-latest + + container: + image: ubuntu:22.04 + + strategy: + matrix: + include: + - CC: gcc + CXX: g++ + - CC: clang + CXX: clang++ + + # don't run pull requests from local branches twice + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + + env: + DEBIAN_FRONTEND: noninteractive + + steps: + - name: Install packages + run: | + apt-get -q -y update + apt-get -q -y install build-essential cmake git clang + + - uses: actions/checkout@v3 + with: + submodules: 'true' + + - name: Build source + run: | + mkdir -p build + cd build + CC=${{ matrix.CC }} CXX=${{ matrix.CXX }} cmake .. + cmake --build . + DESTDIR=../out cmake --install . + + - uses: actions/upload-artifact@v4 + with: + name: cppdap-${{ matrix.CC }} + path: out/usr/local/ diff --git a/libraries/cppdap/.gitignore b/libraries/cppdap/.gitignore new file mode 100644 index 000000000..af8d40aae --- /dev/null +++ b/libraries/cppdap/.gitignore @@ -0,0 +1,6 @@ +build/ +fuzz/corpus +fuzz/logs +.vs/ +.vscode/settings.json +CMakeSettings.json diff --git a/libraries/cppdap/CMakeLists.txt b/libraries/cppdap/CMakeLists.txt new file mode 100644 index 000000000..a20152d77 --- /dev/null +++ b/libraries/cppdap/CMakeLists.txt @@ -0,0 +1,394 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.22) + +project(cppdap VERSION 1.68.0 LANGUAGES CXX C) + +set (CMAKE_CXX_STANDARD 11) + +include(GNUInstallDirs) + +########################################################### +# Options +########################################################### +function (option_if_not_defined name description default) + if(NOT DEFINED ${name}) + option(${name} ${description} ${default}) + endif() +endfunction() + +option_if_not_defined(CPPDAP_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) +option_if_not_defined(CPPDAP_BUILD_EXAMPLES "Build example applications" OFF) +option_if_not_defined(CPPDAP_BUILD_TESTS "Build tests" OFF) +option_if_not_defined(CPPDAP_BUILD_FUZZER "Build fuzzer" OFF) +option_if_not_defined(CPPDAP_ASAN "Build dap with address sanitizer" OFF) +option_if_not_defined(CPPDAP_MSAN "Build dap with memory sanitizer" OFF) +option_if_not_defined(CPPDAP_TSAN "Build dap with thread sanitizer" OFF) +option_if_not_defined(CPPDAP_INSTALL_VSCODE_EXAMPLES "Build and install dap examples into vscode extensions directory" OFF) +option_if_not_defined(CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE "Use nlohmann_json with find_package() instead of building internal submodule" OFF) +option_if_not_defined(CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE "Use RapidJSON with find_package()" OFF) +option_if_not_defined(CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE "Use JsonCpp with find_package()" OFF) +option_if_not_defined(CPPDAP_USE_EXTERNAL_GTEST_PACKAGE "Use googletest with find_package()" OFF) + +########################################################### +# Directories +########################################################### +function (set_if_not_defined name value) + if(NOT DEFINED ${name}) + set(${name} ${value} PARENT_SCOPE) + endif() +endfunction() + +set(CPPDAP_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) +set(CPPDAP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) +set_if_not_defined(CPPDAP_THIRD_PARTY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party) +set_if_not_defined(CPPDAP_JSON_DIR ${CPPDAP_THIRD_PARTY_DIR}/json) +set_if_not_defined(CPPDAP_GOOGLETEST_DIR ${CPPDAP_THIRD_PARTY_DIR}/googletest) + +########################################################### +# Submodules +########################################################### +if(CPPDAP_BUILD_TESTS AND NOT CPPDAP_USE_EXTERNAL_GTEST_PACKAGE) + if(NOT EXISTS ${CPPDAP_GOOGLETEST_DIR}/.git) + message(WARNING "third_party/googletest submodule missing.") + message(WARNING "Run: `git submodule update --init` to build tests.") + set(CPPDAP_BUILD_TESTS OFF) + endif() +endif() + +########################################################### +# JSON library +########################################################### + +if(CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE) + list(APPEND CPPDAP_EXTERNAL_JSON_PACKAGES "CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE") +endif() +if(CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE) + list(APPEND CPPDAP_EXTERNAL_JSON_PACKAGES "CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE") +endif() +if(CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE) + list(APPEND CPPDAP_EXTERNAL_JSON_PACKAGES "CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE") +endif() +list(LENGTH CPPDAP_EXTERNAL_JSON_PACKAGES CPPDAP_EXTERNAL_JSON_PACKAGES_COUNT) + +if(CPPDAP_EXTERNAL_JSON_PACKAGES_COUNT GREATER 1) + message(FATAL_ERROR "At most one of CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE, \ +CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE, and CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE can \ +be set, but ${CPPDAP_EXTERNAL_JSON_PACKAGES} were all set.") +elseif(CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE) + find_package(nlohmann_json CONFIG REQUIRED) + set(CPPDAP_JSON_LIBRARY "nlohmann") +elseif(CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE) + find_package(RapidJSON CONFIG REQUIRED) + set(CPPDAP_JSON_LIBRARY "rapid") +elseif(CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE) + find_package(jsoncpp CONFIG REQUIRED) + set(CPPDAP_JSON_LIBRARY "jsoncpp") +endif() + +if(NOT DEFINED CPPDAP_JSON_LIBRARY) + # Attempt to detect JSON library from CPPDAP_JSON_DIR + if(NOT EXISTS "${CPPDAP_JSON_DIR}") + message(FATAL_ERROR "CPPDAP_JSON_DIR '${CPPDAP_JSON_DIR}' does not exist") + endif() + + if(EXISTS "${CPPDAP_JSON_DIR}/include/nlohmann") + set(CPPDAP_JSON_LIBRARY "nlohmann") + elseif(EXISTS "${CPPDAP_JSON_DIR}/include/rapidjson") + set(CPPDAP_JSON_LIBRARY "rapid") + elseif(EXISTS "${CPPDAP_JSON_DIR}/include/json") + set(CPPDAP_JSON_LIBRARY "jsoncpp") + else() + message(FATAL_ERROR "Could not determine JSON library from ${CPPDAP_JSON_DIR}") + endif() +endif() +string(TOUPPER ${CPPDAP_JSON_LIBRARY} CPPDAP_JSON_LIBRARY_UPPER) + +########################################################### +# File lists +########################################################### +set(CPPDAP_LIST + ${CPPDAP_SRC_DIR}/content_stream.cpp + ${CPPDAP_SRC_DIR}/io.cpp + ${CPPDAP_SRC_DIR}/${CPPDAP_JSON_LIBRARY}_json_serializer.cpp + ${CPPDAP_SRC_DIR}/network.cpp + ${CPPDAP_SRC_DIR}/null_json_serializer.cpp + ${CPPDAP_SRC_DIR}/protocol_events.cpp + ${CPPDAP_SRC_DIR}/protocol_requests.cpp + ${CPPDAP_SRC_DIR}/protocol_response.cpp + ${CPPDAP_SRC_DIR}/protocol_types.cpp + ${CPPDAP_SRC_DIR}/session.cpp + ${CPPDAP_SRC_DIR}/socket.cpp + ${CPPDAP_SRC_DIR}/typeinfo.cpp + ${CPPDAP_SRC_DIR}/typeof.cpp +) + +########################################################### +# OS libraries +########################################################### +if(CMAKE_SYSTEM_NAME MATCHES "Windows") + set(CPPDAP_OS_LIBS WS2_32) + set(CPPDAP_OS_EXE_EXT ".exe") +elseif(CMAKE_SYSTEM_NAME MATCHES "Linux") + set(CPPDAP_OS_LIBS pthread) + set(CPPDAP_OS_EXE_EXT "") +elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") + set(CPPDAP_OS_LIBS) + set(CPPDAP_OS_EXE_EXT "") +endif() + +########################################################### +# Functions +########################################################### + +function(cppdap_set_json_links target) + if (CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE) + target_link_libraries(${target} PRIVATE "$") + elseif(CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE) + target_link_libraries(${target} PRIVATE rapidjson) + elseif(CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE) + target_link_libraries(${target} PRIVATE JsonCpp::JsonCpp) + else() + target_include_directories(${target} PRIVATE "${CPPDAP_JSON_DIR}/include/") + endif() +endfunction(cppdap_set_json_links) + +function(cppdap_set_target_options target) + # Enable all warnings + if(MSVC) + target_compile_options(${target} PRIVATE "-W4") + else() + target_compile_options(${target} PRIVATE "-Wall") + endif() + + # Disable specific, pedantic warnings + if(MSVC) + target_compile_options(${target} PRIVATE + "-D_CRT_SECURE_NO_WARNINGS" + + # Warnings from nlohmann/json headers. + "/wd4267" # 'argument': conversion from 'size_t' to 'int', possible loss of data + ) + endif() + + # Add define for JSON library in use + target_compile_definitions(${target} + PRIVATE "CPPDAP_JSON_${CPPDAP_JSON_LIBRARY_UPPER}=1" + ) + + # Treat all warnings as errors + if(CPPDAP_WARNINGS_AS_ERRORS) + if(MSVC) + target_compile_options(${target} PRIVATE "/WX") + else() + target_compile_options(${target} PRIVATE "-Werror") + endif() + endif(CPPDAP_WARNINGS_AS_ERRORS) + + if(CPPDAP_ASAN) + target_compile_options(${target} PUBLIC "-fsanitize=address") + target_link_options(${target} PUBLIC "-fsanitize=address") + elseif(CPPDAP_MSAN) + target_compile_options(${target} PUBLIC "-fsanitize=memory") + target_link_options(${target} PUBLIC "-fsanitize=memory") + elseif(CPPDAP_TSAN) + target_compile_options(${target} PUBLIC "-fsanitize=thread") + target_link_options(${target} PUBLIC "-fsanitize=thread") + endif() + + # Error on undefined symbols + # if(NOT MSVC) + # target_compile_options(${target} PRIVATE "-Wl,--no-undefined") + # endif() + target_include_directories( + "${target}" + PUBLIC + "$" + "$" + ) + cppdap_set_json_links(${target}) + target_link_libraries(${target} PRIVATE ${CPPDAP_OS_LIBS}) +endfunction(cppdap_set_target_options) + +########################################################### +# Targets +########################################################### + +# dap +add_library(cppdap ${CPPDAP_LIST}) +set_target_properties(cppdap PROPERTIES POSITION_INDEPENDENT_CODE 1) + +cppdap_set_target_options(cppdap) + +set(CPPDAP_TARGET_NAME ${PROJECT_NAME}) +set(CPPDAP_CONFIG_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" CACHE INTERNAL "") +set(CPPDAP_INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_INCLUDEDIR}") +set(CPPDAP_TARGETS_EXPORT_NAME "${PROJECT_NAME}-targets") +set(CPPDAP_CMAKE_CONFIG_TEMPLATE "cmake/Config.cmake.in") +set(CPPDAP_CMAKE_CONFIG_DIR "${CMAKE_CURRENT_BINARY_DIR}") +set(CPPDAP_CMAKE_VERSION_CONFIG_FILE "${CPPDAP_CMAKE_CONFIG_DIR}/${PROJECT_NAME}ConfigVersion.cmake") +set(CPPDAP_CMAKE_PROJECT_CONFIG_FILE "${CPPDAP_CMAKE_CONFIG_DIR}/${PROJECT_NAME}Config.cmake") +set(CPPDAP_CMAKE_PROJECT_TARGETS_FILE "${CPPDAP_CMAKE_CONFIG_DIR}/${PROJECT_NAME}-targets.cmake") + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + ${CPPDAP_CMAKE_VERSION_CONFIG_FILE} COMPATIBILITY SameMinorVersion +) +configure_package_config_file( + ${CPPDAP_CMAKE_CONFIG_TEMPLATE} + "${CPPDAP_CMAKE_PROJECT_CONFIG_FILE}" + INSTALL_DESTINATION ${CPPDAP_CONFIG_INSTALL_DIR} +) + +install( + TARGETS "${CPPDAP_TARGET_NAME}" + EXPORT "${CPPDAP_TARGETS_EXPORT_NAME}" + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + INCLUDES DESTINATION "${CPPDAP_INCLUDE_INSTALL_DIR}" +) + +install( + EXPORT "${CPPDAP_TARGETS_EXPORT_NAME}" + NAMESPACE "${CPPDAP_TARGET_NAME}::" + DESTINATION "${CPPDAP_CONFIG_INSTALL_DIR}" +) + +install( + DIRECTORY + "include/dap" + DESTINATION "${CPPDAP_INCLUDE_INSTALL_DIR}" +) +install(FILES ${CPPDAP_CMAKE_VERSION_CONFIG_FILE} ${CPPDAP_CMAKE_PROJECT_CONFIG_FILE} +DESTINATION ${CPPDAP_CONFIG_INSTALL_DIR}) + +# tests +if(CPPDAP_BUILD_TESTS) + enable_testing() + + set(DAP_TEST_LIST + ${CPPDAP_SRC_DIR}/any_test.cpp + ${CPPDAP_SRC_DIR}/chan_test.cpp + ${CPPDAP_SRC_DIR}/content_stream_test.cpp + ${CPPDAP_SRC_DIR}/dap_test.cpp + ${CPPDAP_SRC_DIR}/json_serializer_test.cpp + ${CPPDAP_SRC_DIR}/network_test.cpp + ${CPPDAP_SRC_DIR}/optional_test.cpp + ${CPPDAP_SRC_DIR}/rwmutex_test.cpp + ${CPPDAP_SRC_DIR}/session_test.cpp + ${CPPDAP_SRC_DIR}/socket_test.cpp + ${CPPDAP_SRC_DIR}/traits_test.cpp + ${CPPDAP_SRC_DIR}/typeinfo_test.cpp + ${CPPDAP_SRC_DIR}/variant_test.cpp + ) + + if(CPPDAP_USE_EXTERNAL_GTEST_PACKAGE) + find_package(GTest REQUIRED) + else() + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) + add_subdirectory(${CPPDAP_GOOGLETEST_DIR}) + # googletest has -Werror=maybe-uninitialized problems. + # Disable all warnings in googletest code. + target_compile_options(gtest PRIVATE -w) + # gmock has -Werror=deprecated-copy problems. + target_compile_options(gmock PRIVATE -w) + endif() + + add_executable(cppdap-unittests ${DAP_TEST_LIST}) + add_test(NAME cppdap-unittests COMMAND cppdap-unittests) + + set_target_properties(cppdap-unittests PROPERTIES + FOLDER "Tests" + ) + + if(MSVC) + # googletest emits warning C4244: 'initializing': conversion from 'double' to 'testing::internal::BiggestInt', possible loss of data + target_compile_options(cppdap-unittests PRIVATE "/wd4244") + endif() + + cppdap_set_target_options(cppdap-unittests) + if(CPPDAP_USE_EXTERNAL_GTEST_PACKAGE) + target_link_libraries(cppdap-unittests PRIVATE cppdap GTest::gtest) + else() + target_link_libraries(cppdap-unittests PRIVATE cppdap gtest gmock) + endif() +endif(CPPDAP_BUILD_TESTS) + +# fuzzer +if(CPPDAP_BUILD_FUZZER) + if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + message(FATAL_ERROR "CPPDAP_BUILD_FUZZER can currently only be used with the clang toolchain") + endif() + set(DAP_FUZZER_LIST + ${CPPDAP_LIST} + ${CMAKE_CURRENT_SOURCE_DIR}/fuzz/fuzz.cpp + ) + add_executable(cppdap-fuzzer ${DAP_FUZZER_LIST}) + if(CPPDAP_ASAN) + target_compile_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,address") + target_link_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,address") + elseif(CPPDAP_MSAN) + target_compile_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,memory") + target_link_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,memory") + elseif(CPPDAP_TSAN) + target_compile_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,thread") + target_link_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer,thread") + else() + target_compile_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer") + target_link_options(cppdap-fuzzer PUBLIC "-fsanitize=fuzzer") + endif() + target_include_directories(cppdap-fuzzer PUBLIC + ${CPPDAP_INCLUDE_DIR} + ${CPPDAP_SRC_DIR} + ) + cppdap_set_json_links(cppdap-fuzzer) + target_link_libraries(cppdap-fuzzer PRIVATE cppdap "${CPPDAP_OS_LIBS}") + +endif(CPPDAP_BUILD_FUZZER) + +# examples +if(CPPDAP_BUILD_EXAMPLES) + function(build_example target) + add_executable(${target} "${CMAKE_CURRENT_SOURCE_DIR}/examples/${target}.cpp") + set_target_properties(${target} PROPERTIES + FOLDER "Examples" + ) + cppdap_set_target_options(${target}) + target_link_libraries(${target} PRIVATE cppdap) + + if(CPPDAP_INSTALL_VSCODE_EXAMPLES) + if(CMAKE_SYSTEM_NAME MATCHES "Windows") + set(extroot "$ENV{USERPROFILE}\\.vscode\\extensions") + else() + set(extroot "$ENV{HOME}/.vscode/extensions") + endif() + if(EXISTS ${extroot}) + set(extdir "${extroot}/google.cppdap-example-${target}-1.0.0") + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/examples/vscode/package.json ${extdir}/package.json) + add_custom_command(TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ ${extdir}) + else() + message(WARNING "Could not install vscode example extension as '${extroot}' does not exist") + endif() + endif(CPPDAP_INSTALL_VSCODE_EXAMPLES) + endfunction(build_example) + + build_example(hello_debugger) + build_example(simple_net_client_server) + +endif(CPPDAP_BUILD_EXAMPLES) diff --git a/libraries/cppdap/CONTRIBUTING b/libraries/cppdap/CONTRIBUTING new file mode 100644 index 000000000..9a86ba025 --- /dev/null +++ b/libraries/cppdap/CONTRIBUTING @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/libraries/cppdap/LICENSE b/libraries/cppdap/LICENSE new file mode 100644 index 000000000..7a4a3ea24 --- /dev/null +++ b/libraries/cppdap/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/libraries/cppdap/README.md b/libraries/cppdap/README.md new file mode 100644 index 000000000..cd45bed78 --- /dev/null +++ b/libraries/cppdap/README.md @@ -0,0 +1,79 @@ +# cppdap + +## About + +`cppdap` is a C++11 library (["SDK"](https://microsoft.github.io/debug-adapter-protocol/implementors/sdks/)) implementation of the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/), providing an API for implementing a DAP client or server. + +`cppdap` provides C++ type-safe structures for the full [DAP specification](https://microsoft.github.io/debug-adapter-protocol/specification), and provides a simple way to add custom protocol messages. + +## Fetching dependencies + +`cppdap` provides CMake build files to build the library, unit tests and examples. + +`cppdap` depends on the [`nlohmann/json` library](https://github.com/nlohmann/json), and the unit tests depend on the [`googletest` library](https://github.com/google/googletest). Both are referenced as a git submodules. + +Before building, fetch the git submodules with: + +```bash +cd +git submodule update --init +``` + +Alternatively, `cppdap` can use the [`RapidJSON` library](https://rapidjson.org/) or the [`JsonCpp` library](https://github.com/open-source-parsers/jsoncpp) for JSON serialization. Use the `CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE`, `CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE`, and `CPPDAP_USE_EXTERNAL_JSONCPP_PACKAGE` CMake cache variables to select which library to use. + +## Building + +### Linux and macOS + +Next, generate the build files: + +```bash +cd +mkdir build +cd build +cmake .. +``` + +You may wish to suffix the `cmake ..` line with any of the following flags: + +* `-DCPPDAP_BUILD_TESTS=1` - Builds the `cppdap` unit tests +* `-DCPPDAP_BUILD_EXAMPLES=1` - Builds the `cppdap` examples +* `-DCPPDAP_INSTALL_VSCODE_EXAMPLES=1` - Installs the `cppdap` examples as Visual Studio Code extensions +* `-DCPPDAP_WARNINGS_AS_ERRORS=1` - Treats all compiler warnings as errors. + +Finally, build the project: + +`make` + +### Windows + +`cppdap` can be built using [Visual Studio 2019's CMake integration](https://docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=vs-2019). + + +### Using `cppdap` in your CMake project + +You can build and link `cppdap` using `add_subdirectory()` in your project's `CMakeLists.txt` file: +```cmake +set(CPPDAP_DIR ) # example : "${CMAKE_CURRENT_SOURCE_DIR}/third_party/cppdap" +add_subdirectory(${CPPDAP_DIR}) +``` + +This will define the `cppdap` library target, which you can pass to `target_link_libraries()`: + +```cmake +target_link_libraries( cppdap) # replace with the name of your project's target +``` + +You may also wish to specify your own paths to the third party libraries used by `cppdap`. +You can do this by setting any of the following variables before the call to `add_subdirectory()`: + +```cmake +set(CPPDAP_THIRD_PARTY_DIR ) # defaults to ${CPPDAP_DIR}/third_party +set(CPPDAP_JSON_DIR ) # defaults to ${CPPDAP_THIRD_PARTY_DIR}/json +set(CPPDAP_GOOGLETEST_DIR ) # defaults to ${CPPDAP_THIRD_PARTY_DIR}/googletest +add_subdirectory(${CPPDAP_DIR}) +``` + +--- + +Note: This is not an officially supported Google product diff --git a/libraries/cppdap/clang-format-all.sh b/libraries/cppdap/clang-format-all.sh new file mode 100755 index 000000000..8bfb593b9 --- /dev/null +++ b/libraries/cppdap/clang-format-all.sh @@ -0,0 +1,21 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +SRC_DIR=${ROOT_DIR}/src +CLANG_FORMAT=${CLANG_FORMAT:-clang-format} + +# Double clang-format, as it seems that one pass isn't always enough +find ${SRC_DIR} -iname "*.h" -o -iname "*.cpp" | xargs ${CLANG_FORMAT} -i -style=file +find ${SRC_DIR} -iname "*.h" -o -iname "*.cpp" | xargs ${CLANG_FORMAT} -i -style=file diff --git a/libraries/cppdap/cmake/Config.cmake.in b/libraries/cppdap/cmake/Config.cmake.in new file mode 100644 index 000000000..384aa3f50 --- /dev/null +++ b/libraries/cppdap/cmake/Config.cmake.in @@ -0,0 +1,25 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@PACKAGE_INIT@ +include(CMakeFindDependencyMacro) + +include("${CMAKE_CURRENT_LIST_DIR}/@CPPDAP_TARGETS_EXPORT_NAME@.cmake") +check_required_components("@CPPDAP_TARGET_NAME@") + +if ( @CPPDAP_USE_EXTERNAL_NLOHMANN_JSON_PACKAGE@ ) + find_dependency(nlohmann_json CONFIG) +elseif( @CPPDAP_USE_EXTERNAL_RAPIDJSON_PACKAGE@ ) + find_dependency(RapidJSON CONFIG) +endif() \ No newline at end of file diff --git a/libraries/cppdap/examples/hello_debugger.cpp b/libraries/cppdap/examples/hello_debugger.cpp new file mode 100644 index 000000000..62b619f9e --- /dev/null +++ b/libraries/cppdap/examples/hello_debugger.cpp @@ -0,0 +1,469 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// hello_debugger is an example DAP server that provides single line stepping +// through a synthetic file. + +#include "dap/io.h" +#include "dap/protocol.h" +#include "dap/session.h" + +#include +#include +#include +#include + +#ifdef _MSC_VER +#define OS_WINDOWS 1 +#endif + +// Uncomment the line below and change to a file path to +// write all DAP communications to the given path. +// +// #define LOG_TO_FILE "" + +#ifdef OS_WINDOWS +#include // _O_BINARY +#include // _setmode +#endif // OS_WINDOWS + +namespace { + +// sourceContent holds the synthetic file source. +constexpr char sourceContent[] = R"(// Hello Debugger! + +This is a synthetic source file provided by the DAP debugger. + +You can set breakpoints, and single line step. + +You may also notice that the locals contains a single variable for the currently executing line number.)"; + +// Total number of newlines in source. +constexpr int64_t numSourceLines = 7; + +// Debugger holds the dummy debugger state and fires events to the EventHandler +// passed to the constructor. +class Debugger { + public: + enum class Event { BreakpointHit, Stepped, Paused }; + using EventHandler = std::function; + + Debugger(const EventHandler&); + + // run() instructs the debugger to continue execution. + void run(); + + // pause() instructs the debugger to pause execution. + void pause(); + + // currentLine() returns the currently executing line number. + int64_t currentLine(); + + // stepForward() instructs the debugger to step forward one line. + void stepForward(); + + // clearBreakpoints() clears all set breakpoints. + void clearBreakpoints(); + + // addBreakpoint() sets a new breakpoint on the given line. + void addBreakpoint(int64_t line); + + private: + EventHandler onEvent; + std::mutex mutex; + int64_t line = 1; + std::unordered_set breakpoints; +}; + +Debugger::Debugger(const EventHandler& onEvent) : onEvent(onEvent) {} + +void Debugger::run() { + std::unique_lock lock(mutex); + for (int64_t i = 0; i < numSourceLines; i++) { + int64_t l = ((line + i) % numSourceLines) + 1; + if (breakpoints.count(l)) { + line = l; + lock.unlock(); + onEvent(Event::BreakpointHit); + return; + } + } +} + +void Debugger::pause() { + onEvent(Event::Paused); +} + +int64_t Debugger::currentLine() { + std::unique_lock lock(mutex); + return line; +} + +void Debugger::stepForward() { + std::unique_lock lock(mutex); + line = (line % numSourceLines) + 1; + lock.unlock(); + onEvent(Event::Stepped); +} + +void Debugger::clearBreakpoints() { + std::unique_lock lock(mutex); + this->breakpoints.clear(); +} + +void Debugger::addBreakpoint(int64_t l) { + std::unique_lock lock(mutex); + this->breakpoints.emplace(l); +} + +// Event provides a basic wait and signal synchronization primitive. +class Event { + public: + // wait() blocks until the event is fired. + void wait(); + + // fire() sets signals the event, and unblocks any calls to wait(). + void fire(); + + private: + std::mutex mutex; + std::condition_variable cv; + bool fired = false; +}; + +void Event::wait() { + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return fired; }); +} + +void Event::fire() { + std::unique_lock lock(mutex); + fired = true; + cv.notify_all(); +} + +} // anonymous namespace + +// main() entry point to the DAP server. +int main(int, char*[]) { +#ifdef OS_WINDOWS + // Change stdin & stdout from text mode to binary mode. + // This ensures sequences of \r\n are not changed to \n. + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif // OS_WINDOWS + + std::shared_ptr log; +#ifdef LOG_TO_FILE + log = dap::file(LOG_TO_FILE); +#endif + + // Create the DAP session. + // This is used to implement the DAP server. + auto session = dap::Session::create(); + + // Hard-coded identifiers for the one thread, frame, variable and source. + // These numbers have no meaning, and just need to remain constant for the + // duration of the service. + const dap::integer threadId = 100; + const dap::integer frameId = 200; + const dap::integer variablesReferenceId = 300; + const dap::integer sourceReferenceId = 400; + + // Signal events + Event configured; + Event terminate; + + // Event handlers from the Debugger. + auto onDebuggerEvent = [&](Debugger::Event onEvent) { + switch (onEvent) { + case Debugger::Event::Stepped: { + // The debugger has single-line stepped. Inform the client. + dap::StoppedEvent event; + event.reason = "step"; + event.threadId = threadId; + session->send(event); + break; + } + case Debugger::Event::BreakpointHit: { + // The debugger has hit a breakpoint. Inform the client. + dap::StoppedEvent event; + event.reason = "breakpoint"; + event.threadId = threadId; + session->send(event); + break; + } + case Debugger::Event::Paused: { + // The debugger has been suspended. Inform the client. + dap::StoppedEvent event; + event.reason = "pause"; + event.threadId = threadId; + session->send(event); + break; + } + } + }; + + // Construct the debugger. + Debugger debugger(onDebuggerEvent); + + // Handle errors reported by the Session. These errors include protocol + // parsing errors and receiving messages with no handler. + session->onError([&](const char* msg) { + if (log) { + dap::writef(log, "dap::Session error: %s\n", msg); + log->close(); + } + terminate.fire(); + }); + + // The Initialize request is the first message sent from the client and + // the response reports debugger capabilities. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + session->registerHandler([](const dap::InitializeRequest&) { + dap::InitializeResponse response; + response.supportsConfigurationDoneRequest = true; + return response; + }); + + // When the Initialize response has been sent, we need to send the initialized + // event. + // We use the registerSentHandler() to ensure the event is sent *after* the + // initialize response. + // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized + session->registerSentHandler( + [&](const dap::ResponseOrError&) { + session->send(dap::InitializedEvent()); + }); + + // The Threads request queries the debugger's list of active threads. + // This example debugger only exposes a single thread. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads + session->registerHandler([&](const dap::ThreadsRequest&) { + dap::ThreadsResponse response; + dap::Thread thread; + thread.id = threadId; + thread.name = "TheThread"; + response.threads.push_back(thread); + return response; + }); + + // The StackTrace request reports the stack frames (call stack) for a given + // thread. This example debugger only exposes a single stack frame for the + // single thread. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace + session->registerHandler( + [&](const dap::StackTraceRequest& request) + -> dap::ResponseOrError { + if (request.threadId != threadId) { + return dap::Error("Unknown threadId '%d'", int(request.threadId)); + } + + dap::Source source; + source.sourceReference = sourceReferenceId; + source.name = "HelloDebuggerSource"; + + dap::StackFrame frame; + frame.line = debugger.currentLine(); + frame.column = 1; + frame.name = "HelloDebugger"; + frame.id = frameId; + frame.source = source; + + dap::StackTraceResponse response; + response.stackFrames.push_back(frame); + return response; + }); + + // The Scopes request reports all the scopes of the given stack frame. + // This example debugger only exposes a single 'Locals' scope for the single + // frame. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes + session->registerHandler([&](const dap::ScopesRequest& request) + -> dap::ResponseOrError { + if (request.frameId != frameId) { + return dap::Error("Unknown frameId '%d'", int(request.frameId)); + } + + dap::Scope scope; + scope.name = "Locals"; + scope.presentationHint = "locals"; + scope.variablesReference = variablesReferenceId; + + dap::ScopesResponse response; + response.scopes.push_back(scope); + return response; + }); + + // The Variables request reports all the variables for the given scope. + // This example debugger only exposes a single 'currentLine' variable for the + // single 'Locals' scope. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables + session->registerHandler([&](const dap::VariablesRequest& request) + -> dap::ResponseOrError { + if (request.variablesReference != variablesReferenceId) { + return dap::Error("Unknown variablesReference '%d'", + int(request.variablesReference)); + } + + dap::Variable currentLineVar; + currentLineVar.name = "currentLine"; + currentLineVar.value = std::to_string(debugger.currentLine()); + currentLineVar.type = "int"; + + dap::VariablesResponse response; + response.variables.push_back(currentLineVar); + return response; + }); + + // The Pause request instructs the debugger to pause execution of one or all + // threads. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause + session->registerHandler([&](const dap::PauseRequest&) { + debugger.pause(); + return dap::PauseResponse(); + }); + + // The Continue request instructs the debugger to resume execution of one or + // all threads. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue + session->registerHandler([&](const dap::ContinueRequest&) { + debugger.run(); + return dap::ContinueResponse(); + }); + + // The Next request instructs the debugger to single line step for a specific + // thread. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next + session->registerHandler([&](const dap::NextRequest&) { + debugger.stepForward(); + return dap::NextResponse(); + }); + + // The StepIn request instructs the debugger to step-in for a specific thread. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn + session->registerHandler([&](const dap::StepInRequest&) { + // Step-in treated as step-over as there's only one stack frame. + debugger.stepForward(); + return dap::StepInResponse(); + }); + + // The StepOut request instructs the debugger to step-out for a specific + // thread. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut + session->registerHandler([&](const dap::StepOutRequest&) { + // Step-out is not supported as there's only one stack frame. + return dap::StepOutResponse(); + }); + + // The SetBreakpoints request instructs the debugger to clear and set a number + // of line breakpoints for a specific source file. + // This example debugger only exposes a single source file. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints + session->registerHandler([&](const dap::SetBreakpointsRequest& request) { + dap::SetBreakpointsResponse response; + + auto breakpoints = request.breakpoints.value({}); + if (request.source.sourceReference.value(0) == sourceReferenceId) { + debugger.clearBreakpoints(); + response.breakpoints.resize(breakpoints.size()); + for (size_t i = 0; i < breakpoints.size(); i++) { + debugger.addBreakpoint(breakpoints[i].line); + response.breakpoints[i].verified = breakpoints[i].line < numSourceLines; + } + } else { + response.breakpoints.resize(breakpoints.size()); + } + + return response; + }); + + // The SetExceptionBreakpoints request configures the debugger's handling of + // thrown exceptions. + // This example debugger does not use any exceptions, so this is a no-op. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExceptionBreakpoints + session->registerHandler([&](const dap::SetExceptionBreakpointsRequest&) { + return dap::SetExceptionBreakpointsResponse(); + }); + + // The Source request retrieves the source code for a given source file. + // This example debugger only exposes one synthetic source file. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Source + session->registerHandler([&](const dap::SourceRequest& request) + -> dap::ResponseOrError { + if (request.sourceReference != sourceReferenceId) { + return dap::Error("Unknown source reference '%d'", + int(request.sourceReference)); + } + + dap::SourceResponse response; + response.content = sourceContent; + return response; + }); + + // The Launch request is made when the client instructs the debugger adapter + // to start the debuggee. This request contains the launch arguments. + // This example debugger does nothing with this request. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Launch + session->registerHandler( + [&](const dap::LaunchRequest&) { return dap::LaunchResponse(); }); + + // Handler for disconnect requests + session->registerHandler([&](const dap::DisconnectRequest& request) { + if (request.terminateDebuggee.value(false)) { + terminate.fire(); + } + return dap::DisconnectResponse(); + }); + + // The ConfigurationDone request is made by the client once all configuration + // requests have been made. + // This example debugger uses this request to 'start' the debugger. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ConfigurationDone + session->registerHandler([&](const dap::ConfigurationDoneRequest&) { + configured.fire(); + return dap::ConfigurationDoneResponse(); + }); + + // All the handlers we care about have now been registered. + // We now bind the session to stdin and stdout to connect to the client. + // After the call to bind() we should start receiving requests, starting with + // the Initialize request. + std::shared_ptr in = dap::file(stdin, false); + std::shared_ptr out = dap::file(stdout, false); + if (log) { + session->bind(spy(in, log), spy(out, log)); + } else { + session->bind(in, out); + } + + // Wait for the ConfigurationDone request to be made. + configured.wait(); + + // Broadcast the existance of the single thread to the client. + dap::ThreadEvent threadStartedEvent; + threadStartedEvent.reason = "started"; + threadStartedEvent.threadId = threadId; + session->send(threadStartedEvent); + + // Start the debugger in a paused state. + // This sends a stopped event to the client. + debugger.pause(); + + // Block until we receive a 'terminateDebuggee' request or encounter a session + // error. + terminate.wait(); + + return 0; +} diff --git a/libraries/cppdap/examples/simple_net_client_server.cpp b/libraries/cppdap/examples/simple_net_client_server.cpp new file mode 100644 index 000000000..c93cc9c1d --- /dev/null +++ b/libraries/cppdap/examples/simple_net_client_server.cpp @@ -0,0 +1,109 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// simple_net_client_server demonstrates a minimal DAP network connection +// between a server and client. + +#include "dap/io.h" +#include "dap/network.h" +#include "dap/protocol.h" +#include "dap/session.h" + +#include +#include + +int main(int, char*[]) { + constexpr int kPort = 19021; + + // Callback handler for a socket connection to the server + auto onClientConnected = + [&](const std::shared_ptr& socket) { + auto session = dap::Session::create(); + + // Set the session to close on invalid data. This ensures that data received over the network + // receives a baseline level of validation before being processed. + session->setOnInvalidData(dap::kClose); + + session->bind(socket); + + // The Initialize request is the first message sent from the client and + // the response reports debugger capabilities. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + session->registerHandler([&](const dap::InitializeRequest&) { + dap::InitializeResponse response; + printf("Server received initialize request from client\n"); + return response; + }); + + // Signal used to terminate the server session when a DisconnectRequest + // is made by the client. + bool terminate = false; + std::condition_variable cv; + std::mutex mutex; // guards 'terminate' + + // The Disconnect request is made by the client before it disconnects + // from the server. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disconnect + session->registerHandler([&](const dap::DisconnectRequest&) { + // Client wants to disconnect. Set terminate to true, and signal the + // condition variable to unblock the server thread. + std::unique_lock lock(mutex); + terminate = true; + cv.notify_one(); + return dap::DisconnectResponse{}; + }); + + // Wait for the client to disconnect (or reach a 5 second timeout) + // before releasing the session and disconnecting the socket to the + // client. + std::unique_lock lock(mutex); + cv.wait_for(lock, std::chrono::seconds(5), [&] { return terminate; }); + printf("Server closing connection\n"); + }; + + // Error handler + auto onError = [&](const char* msg) { printf("Server error: %s\n", msg); }; + + // Create the network server + auto server = dap::net::Server::create(); + // Start listening on kPort. + // onClientConnected will be called when a client wants to connect. + // onError will be called on any connection errors. + server->start(kPort, onClientConnected, onError); + + // Create a socket to the server. This will be used for the client side of the + // connection. + auto client = dap::net::connect("localhost", kPort); + if (!client) { + printf("Couldn't connect to server\n"); + return 1; + } + + // Attach a session to the client socket. + auto session = dap::Session::create(); + session->bind(client); + + // Set an initialize request to the server. + auto future = session->send(dap::InitializeRequest{}); + printf("Client sent initialize request to server\n"); + printf("Waiting for response from server...\n"); + // Wait on the response. + auto response = future.get(); + printf("Response received from server\n"); + printf("Disconnecting...\n"); + // Disconnect. + session->send(dap::DisconnectRequest{}); + + return 0; +} diff --git a/libraries/cppdap/examples/vscode/package.json b/libraries/cppdap/examples/vscode/package.json new file mode 100644 index 000000000..9a524131d --- /dev/null +++ b/libraries/cppdap/examples/vscode/package.json @@ -0,0 +1,28 @@ +{ + "name": "cppdap-example-@target@", + "displayName": "cppdap example: @target@", + "description": "cppdap example: @target@", + "version": "1.0.0", + "preview": false, + "publisher": "Google LLC", + "author": { + "name": "Google LLC" + }, + "license": "SEE LICENSE IN LICENSE.txt", + "engines": { + "vscode": "^1.32.0" + }, + "categories": [ + "Debuggers" + ], + "contributes": { + "debuggers": [ + { + "type": "@target@", + "program": "@target@@CPPDAP_OS_EXE_EXT@", + "label": "cppdap example: @target@", + "configurationAttributes": {} + } + ] + } +} \ No newline at end of file diff --git a/libraries/cppdap/fuzz/dictionary.txt b/libraries/cppdap/fuzz/dictionary.txt new file mode 100644 index 000000000..639a8b0e8 --- /dev/null +++ b/libraries/cppdap/fuzz/dictionary.txt @@ -0,0 +1,372 @@ +"MD5" +"SHA1" +"SHA256" +"__restart" +"accessType" +"accessTypes" +"adapterData" +"adapterID" +"additionalModuleColumns" +"address" +"addressRange" +"algorithm" +"all" +"allThreadsContinued" +"allThreadsStopped" +"allowPartial" +"always" +"appliesTo" +"areas" +"args" +"argsCanBeInterpretedByShell" +"arguments" +"asAddress" +"attach" +"attachForSuspendedLaunch" +"attributeName" +"attributes" +"baseClass" +"body" +"boolean" +"breakMode" +"breakpoint" +"breakpointLocations" +"breakpointModes" +"breakpoints" +"bytes" +"bytesWritten" +"canPersist" +"canRestart" +"cancel" +"cancellable" +"cancelled" +"capabilities" +"category" +"changed" +"checksum" +"checksums" +"class" +"clientID" +"clientName" +"clipboard" +"color" +"column" +"columnsStartAt1" +"command" +"completionTriggerCharacters" +"completions" +"condition" +"conditionDescription" +"configuration" +"configurationDone" +"console" +"constructor" +"content" +"context" +"continue" +"continued" +"count" +"customcolor" +"cwd" +"data" +"data breakpoint" +"dataBreakpoint" +"dataBreakpointInfo" +"dataId" +"dateTimeStamp" +"declarationLocationReference" +"deemphasize" +"default" +"description" +"detail" +"details" +"disassemble" +"disconnect" +"emphasize" +"end" +"endColumn" +"endLine" +"entry" +"enum" +"env" +"error" +"evaluate" +"evaluateName" +"event" +"exception" +"exceptionBreakpointFilters" +"exceptionId" +"exceptionInfo" +"exceptionOptions" +"exitCode" +"exited" +"expensive" +"expression" +"external" +"failed" +"field" +"file" +"filter" +"filterId" +"filterOptions" +"filters" +"final" +"format" +"frameId" +"fullTypeName" +"function" +"function breakpoint" +"goto" +"gotoTargets" +"granularity" +"group" +"hex" +"hitBreakpointIds" +"hitCondition" +"hover" +"id" +"important" +"includeAll" +"indexed" +"indexedVariables" +"initialize" +"initialized" +"innerClass" +"innerException" +"instruction" +"instruction breakpoint" +"instructionBytes" +"instructionCount" +"instructionOffset" +"instructionPointerReference" +"instructionReference" +"instructions" +"integrated" +"interface" +"internal" +"invalid" +"invalidated" +"isLocalProcess" +"isOptimized" +"isUserCode" +"keyword" +"kind" +"label" +"launch" +"lazy" +"length" +"levels" +"line" +"lines" +"linesStartAt1" +"loadedSource" +"loadedSources" +"locale" +"locals" +"location" +"locationReference" +"locations" +"logMessage" +"memory" +"memoryReference" +"message" +"method" +"mimeType" +"mode" +"module" +"moduleCount" +"moduleId" +"modules" +"mostDerivedClass" +"name" +"named" +"namedVariables" +"names" +"negate" +"never" +"new" +"next" +"noDebug" +"normal" +"notStopped" +"number" +"offset" +"origin" +"output" +"parameterNames" +"parameterTypes" +"parameterValues" +"parameters" +"path" +"pathFormat" +"pause" +"pending" +"percentage" +"pointerSize" +"presentationHint" +"preserveFocusHint" +"private" +"process" +"processId" +"progressEnd" +"progressId" +"progressStart" +"progressUpdate" +"property" +"protected" +"public" +"read" +"readMemory" +"readWrite" +"reason" +"reference" +"registers" +"removed" +"repl" +"request" +"requestId" +"request_seq" +"resolveSymbols" +"response" +"restart" +"restartFrame" +"result" +"returnValue" +"reverseContinue" +"runInTerminal" +"scopes" +"selectionLength" +"selectionStart" +"sendTelemetry" +"seq" +"setBreakpoints" +"setDataBreakpoints" +"setExceptionBreakpoints" +"setExpression" +"setFunctionBreakpoints" +"setInstructionBreakpoints" +"setVariable" +"shellProcessId" +"showUser" +"singleThread" +"snippet" +"sortText" +"source" +"sourceModified" +"sourceReference" +"sources" +"stackFrameId" +"stackFrames" +"stackTrace" +"stacks" +"start" +"startCollapsed" +"startDebugging" +"startFrame" +"startMethod" +"startModule" +"started" +"statement" +"stderr" +"stdout" +"step" +"stepBack" +"stepIn" +"stepInTargets" +"stepOut" +"stopped" +"string" +"subtle" +"success" +"supportSuspendDebuggee" +"supportTerminateDebuggee" +"supportedChecksumAlgorithms" +"supportsANSIStyling" +"supportsArgsCanBeInterpretedByShell" +"supportsBreakpointLocationsRequest" +"supportsCancelRequest" +"supportsClipboardContext" +"supportsCompletionsRequest" +"supportsCondition" +"supportsConditionalBreakpoints" +"supportsConfigurationDoneRequest" +"supportsDataBreakpointBytes" +"supportsDataBreakpoints" +"supportsDelayedStackTraceLoading" +"supportsDisassembleRequest" +"supportsEvaluateForHovers" +"supportsExceptionFilterOptions" +"supportsExceptionInfoRequest" +"supportsExceptionOptions" +"supportsFunctionBreakpoints" +"supportsGotoTargetsRequest" +"supportsHitConditionalBreakpoints" +"supportsInstructionBreakpoints" +"supportsInvalidatedEvent" +"supportsLoadedSourcesRequest" +"supportsLogPoints" +"supportsMemoryEvent" +"supportsMemoryReferences" +"supportsModulesRequest" +"supportsProgressReporting" +"supportsReadMemoryRequest" +"supportsRestartFrame" +"supportsRestartRequest" +"supportsRunInTerminalRequest" +"supportsSetExpression" +"supportsSetVariable" +"supportsSingleThreadExecutionRequests" +"supportsStartDebuggingRequest" +"supportsStepBack" +"supportsStepInTargetsRequest" +"supportsSteppingGranularity" +"supportsTerminateRequest" +"supportsTerminateThreadsRequest" +"supportsValueFormattingOptions" +"supportsVariablePaging" +"supportsVariableType" +"supportsWriteMemoryRequest" +"suspendDebuggee" +"symbol" +"symbolFilePath" +"symbolStatus" +"systemProcessId" +"targetId" +"targets" +"telemetry" +"terminate" +"terminateDebuggee" +"terminateThreads" +"terminated" +"text" +"thread" +"threadId" +"threadIds" +"threads" +"timestamp" +"title" +"totalFrames" +"totalModules" +"type" +"typeName" +"unhandled" +"unit" +"unixTimestampUTC" +"unreadableBytes" +"uri" +"url" +"urlLabel" +"userUnhandled" +"value" +"valueLocationReference" +"variable" +"variables" +"variablesReference" +"verified" +"version" +"virtual" +"visibility" +"watch" +"width" +"write" +"writeMemory" \ No newline at end of file diff --git a/libraries/cppdap/fuzz/fuzz.cpp b/libraries/cppdap/fuzz/fuzz.cpp new file mode 100644 index 000000000..02a21cc6f --- /dev/null +++ b/libraries/cppdap/fuzz/fuzz.cpp @@ -0,0 +1,128 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// cppdap fuzzer program. +// Run with: ${CPPDAP_PATH}/fuzz/run.sh +// Requires modern clang toolchain. + +#include "content_stream.h" +#include "string_buffer.h" + +#include "dap/protocol.h" +#include "dap/session.h" + +#include "fuzz.h" + +#include +#include + +namespace { + +// Event provides a basic wait and signal synchronization primitive. +class Event { + public: + // wait() blocks until the event is fired or the given timeout is reached. + template + inline void wait(const DURATION& duration) { + std::unique_lock lock(mutex); + cv.wait_for(lock, duration, [&] { return fired; }); + } + + // fire() sets signals the event, and unblocks any calls to wait(). + inline void fire() { + std::unique_lock lock(mutex); + fired = true; + cv.notify_all(); + } + + private: + std::mutex mutex; + std::condition_variable cv; + bool fired = false; +}; + +} // namespace + +// Fuzzing main function. +// See http://llvm.org/docs/LibFuzzer.html for details. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // The first byte can optionally control fuzzing mode. + enum class ControlMode { + // Don't wrap the input data with a stream writer. Allows testing for stream + // writing. + TestStreamWriter, + + // Don't append a 'done' request. This may cause the test to take longer to + // complete (it may have to block on a timeout), but exercises the + // unrecognised-message cases. + DontAppendDoneRequest, + + // Number of control modes in this enum. + Count, + }; + + // Scan first byte for control mode. + bool useContentStreamWriter = true; + bool appendDoneRequest = true; + if (size > 0 && data[0] < static_cast(ControlMode::Count)) { + useContentStreamWriter = + data[0] != static_cast(ControlMode::TestStreamWriter); + appendDoneRequest = + data[0] != static_cast(ControlMode::DontAppendDoneRequest); + data++; + size--; + } + + // in contains the input data + auto in = std::make_shared(); + + dap::ContentWriter writer(in); + if (useContentStreamWriter) { + writer.write(std::string(reinterpret_cast(data), size)); + } else { + in->write(data, size); + } + + if (appendDoneRequest) { + writer.write(R"( + { + "seq": 10, + "type": "request", + "command": "done", + } + )"); + } + + // Each test is done if we receive a request, or report an error. + Event requestOrError; + +#define DAP_REQUEST(REQUEST, RESPONSE) \ + session->registerHandler([&](const REQUEST&) { \ + requestOrError.fire(); \ + return RESPONSE{}; \ + }); + + auto session = dap::Session::create(); + DAP_REQUEST_LIST(); + + session->onError([&](const char*) { requestOrError.fire(); }); + + auto out = std::make_shared(); + session->bind(dap::ReaderWriter::create(in, out)); + + // Give up after a second if we don't get a request or error reported. + requestOrError.wait(std::chrono::seconds(1)); + + return 0; +} \ No newline at end of file diff --git a/libraries/cppdap/fuzz/fuzz.h b/libraries/cppdap/fuzz/fuzz.h new file mode 100644 index 000000000..995f54025 --- /dev/null +++ b/libraries/cppdap/fuzz/fuzz.h @@ -0,0 +1,76 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated with protocol_gen.go -- do not edit this file. +// go run scripts/protocol_gen/protocol_gen.go +// +// DAP version 1.68.0 + +#ifndef dap_fuzzer_h +#define dap_fuzzer_h + +#include "dap/protocol.h" + +#define DAP_REQUEST_LIST() \ + DAP_REQUEST(dap::AttachRequest, dap::AttachResponse) \ + DAP_REQUEST(dap::BreakpointLocationsRequest, \ + dap::BreakpointLocationsResponse) \ + DAP_REQUEST(dap::CancelRequest, dap::CancelResponse) \ + DAP_REQUEST(dap::CompletionsRequest, dap::CompletionsResponse) \ + DAP_REQUEST(dap::ConfigurationDoneRequest, dap::ConfigurationDoneResponse) \ + DAP_REQUEST(dap::ContinueRequest, dap::ContinueResponse) \ + DAP_REQUEST(dap::DataBreakpointInfoRequest, dap::DataBreakpointInfoResponse) \ + DAP_REQUEST(dap::DisassembleRequest, dap::DisassembleResponse) \ + DAP_REQUEST(dap::DisconnectRequest, dap::DisconnectResponse) \ + DAP_REQUEST(dap::EvaluateRequest, dap::EvaluateResponse) \ + DAP_REQUEST(dap::ExceptionInfoRequest, dap::ExceptionInfoResponse) \ + DAP_REQUEST(dap::GotoRequest, dap::GotoResponse) \ + DAP_REQUEST(dap::GotoTargetsRequest, dap::GotoTargetsResponse) \ + DAP_REQUEST(dap::InitializeRequest, dap::InitializeResponse) \ + DAP_REQUEST(dap::LaunchRequest, dap::LaunchResponse) \ + DAP_REQUEST(dap::LoadedSourcesRequest, dap::LoadedSourcesResponse) \ + DAP_REQUEST(dap::LocationsRequest, dap::LocationsResponse) \ + DAP_REQUEST(dap::ModulesRequest, dap::ModulesResponse) \ + DAP_REQUEST(dap::NextRequest, dap::NextResponse) \ + DAP_REQUEST(dap::PauseRequest, dap::PauseResponse) \ + DAP_REQUEST(dap::ReadMemoryRequest, dap::ReadMemoryResponse) \ + DAP_REQUEST(dap::RestartFrameRequest, dap::RestartFrameResponse) \ + DAP_REQUEST(dap::RestartRequest, dap::RestartResponse) \ + DAP_REQUEST(dap::ReverseContinueRequest, dap::ReverseContinueResponse) \ + DAP_REQUEST(dap::RunInTerminalRequest, dap::RunInTerminalResponse) \ + DAP_REQUEST(dap::ScopesRequest, dap::ScopesResponse) \ + DAP_REQUEST(dap::SetBreakpointsRequest, dap::SetBreakpointsResponse) \ + DAP_REQUEST(dap::SetDataBreakpointsRequest, dap::SetDataBreakpointsResponse) \ + DAP_REQUEST(dap::SetExceptionBreakpointsRequest, \ + dap::SetExceptionBreakpointsResponse) \ + DAP_REQUEST(dap::SetExpressionRequest, dap::SetExpressionResponse) \ + DAP_REQUEST(dap::SetFunctionBreakpointsRequest, \ + dap::SetFunctionBreakpointsResponse) \ + DAP_REQUEST(dap::SetInstructionBreakpointsRequest, \ + dap::SetInstructionBreakpointsResponse) \ + DAP_REQUEST(dap::SetVariableRequest, dap::SetVariableResponse) \ + DAP_REQUEST(dap::SourceRequest, dap::SourceResponse) \ + DAP_REQUEST(dap::StackTraceRequest, dap::StackTraceResponse) \ + DAP_REQUEST(dap::StartDebuggingRequest, dap::StartDebuggingResponse) \ + DAP_REQUEST(dap::StepBackRequest, dap::StepBackResponse) \ + DAP_REQUEST(dap::StepInRequest, dap::StepInResponse) \ + DAP_REQUEST(dap::StepInTargetsRequest, dap::StepInTargetsResponse) \ + DAP_REQUEST(dap::StepOutRequest, dap::StepOutResponse) \ + DAP_REQUEST(dap::TerminateRequest, dap::TerminateResponse) \ + DAP_REQUEST(dap::TerminateThreadsRequest, dap::TerminateThreadsResponse) \ + DAP_REQUEST(dap::ThreadsRequest, dap::ThreadsResponse) \ + DAP_REQUEST(dap::VariablesRequest, dap::VariablesResponse) \ + DAP_REQUEST(dap::WriteMemoryRequest, dap::WriteMemoryResponse) + +#endif // dap_fuzzer_h diff --git a/libraries/cppdap/fuzz/run.sh b/libraries/cppdap/fuzz/run.sh new file mode 100755 index 000000000..2d5403909 --- /dev/null +++ b/libraries/cppdap/fuzz/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e # Fail on any error. + +FUZZ_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )" +cd ${FUZZ_DIR} + +# Ensure we're testing with latest build +[ ! -d "build" ] && mkdir "build" +cd "build" +cmake ../.. -GNinja -DCPPDAP_BUILD_FUZZER=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo +ninja + +cd ${FUZZ_DIR} +[ ! -d "corpus" ] && mkdir "corpus" +[ ! -d "logs" ] && mkdir "logs" +cd "logs" +rm crash-* fuzz-* || true +${FUZZ_DIR}/build/cppdap-fuzzer ${FUZZ_DIR}/corpus ${FUZZ_DIR}/seed -dict=${FUZZ_DIR}/dictionary.txt -jobs=128 diff --git a/libraries/cppdap/fuzz/seed/empty_json b/libraries/cppdap/fuzz/seed/empty_json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/libraries/cppdap/fuzz/seed/empty_json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/libraries/cppdap/fuzz/seed/request b/libraries/cppdap/fuzz/seed/request new file mode 100644 index 000000000..7a183aded --- /dev/null +++ b/libraries/cppdap/fuzz/seed/request @@ -0,0 +1,8 @@ +{ + "seq": 153, + "type": "request", + "command": "next", + "arguments": { + "threadId": 3 + } +} \ No newline at end of file diff --git a/libraries/cppdap/include/dap/any.h b/libraries/cppdap/include/dap/any.h new file mode 100644 index 000000000..e799c44cc --- /dev/null +++ b/libraries/cppdap/include/dap/any.h @@ -0,0 +1,211 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_any_h +#define dap_any_h + +#include "typeinfo.h" + +#include +#include + +namespace dap { + +template +struct TypeOf; +class Deserializer; +class Serializer; + +// any provides a type-safe container for values of any of dap type (boolean, +// integer, number, array, variant, any, null, dap-structs). +class any { + public: + // constructors + inline any() = default; + inline any(const any& other) noexcept; + inline any(any&& other) noexcept; + + template + inline any(const T& val); + + // destructors + inline ~any(); + + // replaces the contained value with a null. + inline void reset(); + + // assignment + inline any& operator=(const any& rhs); + inline any& operator=(any&& rhs) noexcept; + template + inline any& operator=(const T& val); + inline any& operator=(const std::nullptr_t& val); + + // get() returns the contained value of the type T. + // If the any does not contain a value of type T, then get() will assert. + template + inline T& get() const; + + // is() returns true iff the contained value is of type T. + template + inline bool is() const; + + private: + friend class Deserializer; + friend class Serializer; + + static inline void* alignUp(void* val, size_t alignment); + inline void alloc(size_t size, size_t align); + inline void freeValue(); + inline bool isInBuffer(void* ptr) const; + + void* value = nullptr; + const TypeInfo* type = nullptr; + void* heap = nullptr; // heap allocation + uint8_t buffer[32]; // or internal allocation +}; + +inline any::~any() { + reset(); +} + +template +inline any::any(const T& val) { + *this = val; +} + +any::any(const any& other) noexcept : type(other.type) { + if (other.value != nullptr) { + alloc(type->size(), type->alignment()); + type->copyConstruct(value, other.value); + } +} + +any::any(any&& other) noexcept : type(other.type) { + if (other.isInBuffer(other.value)) { + alloc(type->size(), type->alignment()); + type->copyConstruct(value, other.value); + } else { + value = other.value; + } + other.value = nullptr; + other.type = nullptr; +} + +void any::reset() { + if (value != nullptr) { + type->destruct(value); + freeValue(); + } + value = nullptr; + type = nullptr; +} + +any& any::operator=(const any& rhs) { + reset(); + type = rhs.type; + if (rhs.value != nullptr) { + alloc(type->size(), type->alignment()); + type->copyConstruct(value, rhs.value); + } + return *this; +} + +any& any::operator=(any&& rhs) noexcept { + reset(); + type = rhs.type; + if (rhs.isInBuffer(rhs.value)) { + alloc(type->size(), type->alignment()); + type->copyConstruct(value, rhs.value); + } else { + value = rhs.value; + } + rhs.value = nullptr; + rhs.type = nullptr; + return *this; +} + +template +any& any::operator=(const T& val) { + if (!is()) { + reset(); + type = TypeOf::type(); + alloc(type->size(), type->alignment()); + type->copyConstruct(value, &val); + } else { +#ifdef __clang_analyzer__ + assert(value != nullptr); +#endif + *reinterpret_cast(value) = val; + } + return *this; +} + +any& any::operator=(const std::nullptr_t&) { + reset(); + return *this; +} + +template +T& any::get() const { + static_assert(!std::is_same(), + "Cannot get nullptr from 'any'."); + assert(is()); + return *reinterpret_cast(value); +} + +template +bool any::is() const { + return type == TypeOf::type(); +} + +template <> +inline bool any::is() const { + return value == nullptr; +} + +void* any::alignUp(void* val, size_t alignment) { + auto ptr = reinterpret_cast(val); + return reinterpret_cast(alignment * + ((ptr + alignment - 1) / alignment)); +} + +void any::alloc(size_t size, size_t align) { + assert(value == nullptr); + value = alignUp(buffer, align); + if (isInBuffer(reinterpret_cast(value) + size - 1)) { + return; + } + heap = new uint8_t[size + align]; + value = alignUp(heap, align); +} + +void any::freeValue() { + assert(value != nullptr); + if (heap != nullptr) { + delete[] reinterpret_cast(heap); + heap = nullptr; + } + value = nullptr; +} + +bool any::isInBuffer(void* ptr) const { + auto addr = reinterpret_cast(ptr); + return addr >= reinterpret_cast(buffer) && + addr < reinterpret_cast(buffer + sizeof(buffer)); +} + +} // namespace dap + +#endif // dap_any_h diff --git a/libraries/cppdap/include/dap/dap.h b/libraries/cppdap/include/dap/dap.h new file mode 100644 index 000000000..587e80c35 --- /dev/null +++ b/libraries/cppdap/include/dap/dap.h @@ -0,0 +1,35 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_dap_h +#define dap_dap_h + +namespace dap { + +// Explicit library initialization and termination functions. +// +// cppdap automatically initializes and terminates its internal state using lazy +// static initialization, and so will usually work fine without explicit calls +// to these functions. +// However, if you use cppdap types in global state, you may need to call these +// functions to ensure that cppdap is not uninitialized before the last usage. +// +// Each call to initialize() must have a corresponding call to terminate(). +// It is undefined behaviour to call initialize() after terminate(). +void initialize(); +void terminate(); + +} // namespace dap + +#endif // dap_dap_h diff --git a/libraries/cppdap/include/dap/future.h b/libraries/cppdap/include/dap/future.h new file mode 100644 index 000000000..af103c33b --- /dev/null +++ b/libraries/cppdap/include/dap/future.h @@ -0,0 +1,179 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_future_h +#define dap_future_h + +#include +#include +#include + +namespace dap { + +// internal functionality +namespace detail { +template +struct promise_state { + T val; + std::mutex mutex; + std::condition_variable cv; + bool hasVal = false; +}; +} // namespace detail + +// forward declaration +template +class promise; + +// future_status is the enumeration returned by future::wait_for and +// future::wait_until. +enum class future_status { + ready, + timeout, +}; + +// future is a minimal reimplementation of std::future, that does not suffer +// from TSAN false positives. See: +// https://gcc.gnu.org/bugzilla//show_bug.cgi?id=69204 +template +class future { + public: + using State = detail::promise_state; + + // constructors + inline future() = default; + inline future(future&&) = default; + + // valid() returns true if the future has an internal state. + bool valid() const; + + // get() blocks until the future has a valid result, and returns it. + // The future must have a valid internal state to call this method. + inline T get(); + + // wait() blocks until the future has a valid result. + // The future must have a valid internal state to call this method. + void wait() const; + + // wait_for() blocks until the future has a valid result, or the timeout is + // reached. + // The future must have a valid internal state to call this method. + template + future_status wait_for( + const std::chrono::duration& timeout) const; + + // wait_until() blocks until the future has a valid result, or the timeout is + // reached. + // The future must have a valid internal state to call this method. + template + future_status wait_until( + const std::chrono::time_point& timeout) const; + + private: + friend promise; + future(const future&) = delete; + inline future(const std::shared_ptr& state); + + std::shared_ptr state = std::make_shared(); +}; + +template +future::future(const std::shared_ptr& s) : state(s) {} + +template +bool future::valid() const { + return static_cast(state); +} + +template +T future::get() { + std::unique_lock lock(state->mutex); + state->cv.wait(lock, [&] { return state->hasVal; }); + return state->val; +} + +template +void future::wait() const { + std::unique_lock lock(state->mutex); + state->cv.wait(lock, [&] { return state->hasVal; }); +} + +template +template +future_status future::wait_for( + const std::chrono::duration& timeout) const { + std::unique_lock lock(state->mutex); + return state->cv.wait_for(lock, timeout, [&] { return state->hasVal; }) + ? future_status::ready + : future_status::timeout; +} + +template +template +future_status future::wait_until( + const std::chrono::time_point& timeout) const { + std::unique_lock lock(state->mutex); + return state->cv.wait_until(lock, timeout, [&] { return state->hasVal; }) + ? future_status::ready + : future_status::timeout; +} + +// promise is a minimal reimplementation of std::promise, that does not suffer +// from TSAN false positives. See: +// https://gcc.gnu.org/bugzilla//show_bug.cgi?id=69204 +template +class promise { + public: + // constructors + inline promise() = default; + inline promise(promise&& other) = default; + inline promise(const promise& other) = default; + + // set_value() stores value to the shared state. + // set_value() must only be called once. + inline void set_value(const T& value) const; + inline void set_value(T&& value) const; + + // get_future() returns a future sharing this promise's state. + future get_future(); + + private: + using State = detail::promise_state; + std::shared_ptr state = std::make_shared(); +}; + +template +future promise::get_future() { + return future(state); +} + +template +void promise::set_value(const T& value) const { + std::unique_lock lock(state->mutex); + state->val = value; + state->hasVal = true; + state->cv.notify_all(); +} + +template +void promise::set_value(T&& value) const { + std::unique_lock lock(state->mutex); + state->val = std::move(value); + state->hasVal = true; + state->cv.notify_all(); +} + +} // namespace dap + +#endif // dap_future_h diff --git a/libraries/cppdap/include/dap/io.h b/libraries/cppdap/include/dap/io.h new file mode 100644 index 000000000..61681cc4a --- /dev/null +++ b/libraries/cppdap/include/dap/io.h @@ -0,0 +1,97 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_io_h +#define dap_io_h + +#include // size_t +#include // FILE +#include // std::unique_ptr +#include // std::pair + +namespace dap { + +class Closable { + public: + virtual ~Closable() = default; + + // isOpen() returns true if the stream has not been closed. + virtual bool isOpen() = 0; + + // close() closes the stream. + virtual void close() = 0; +}; + +// Reader is an interface for reading from a byte stream. +class Reader : virtual public Closable { + public: + // read() attempts to read at most n bytes into buffer, returning the number + // of bytes read. + // read() will block until the stream is closed or at least one byte is read. + virtual size_t read(void* buffer, size_t n) = 0; +}; + +// Writer is an interface for writing to a byte stream. +class Writer : virtual public Closable { + public: + // write() writes n bytes from buffer into the stream. + // Returns true on success, or false if there was an error or the stream was + // closed. + virtual bool write(const void* buffer, size_t n) = 0; +}; + +// ReaderWriter is an interface that combines the Reader and Writer interfaces. +class ReaderWriter : public Reader, public Writer { + public: + // create() returns a ReaderWriter that delegates the interface methods on to + // the provided Reader and Writer. + // isOpen() returns true if the Reader and Writer both return true for + // isOpen(). + // close() closes both the Reader and Writer. + static std::shared_ptr create(const std::shared_ptr&, + const std::shared_ptr&); +}; + +// pipe() returns a ReaderWriter where the Writer streams to the Reader. +// Writes are internally buffered. +// Calling close() on either the Reader or Writer will close both ends of the +// stream. +std::shared_ptr pipe(); + +// file() wraps file with a ReaderWriter. +// If closable is false, then a call to ReaderWriter::close() will not close the +// underlying file. +std::shared_ptr file(FILE* file, bool closable = true); + +// file() opens (or creates) the file with the given path. +std::shared_ptr file(const char* path); + +// spy() returns a Reader that copies all reads from the Reader r to the Writer +// s, using the given optional prefix. +std::shared_ptr spy(const std::shared_ptr& r, + const std::shared_ptr& s, + const char* prefix = "\n->"); + +// spy() returns a Writer that copies all writes to the Writer w to the Writer +// s, using the given optional prefix. +std::shared_ptr spy(const std::shared_ptr& w, + const std::shared_ptr& s, + const char* prefix = "\n<-"); + +// writef writes the printf style string to the writer w. +bool writef(const std::shared_ptr& w, const char* msg, ...); + +} // namespace dap + +#endif // dap_io_h diff --git a/libraries/cppdap/include/dap/network.h b/libraries/cppdap/include/dap/network.h new file mode 100644 index 000000000..03b940925 --- /dev/null +++ b/libraries/cppdap/include/dap/network.h @@ -0,0 +1,71 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_network_h +#define dap_network_h + +#include +#include +#include + +namespace dap { +class ReaderWriter; + +namespace net { + +// connect() connects to the given TCP address and port. +// If timeoutMillis is non-zero and no connection was made before timeoutMillis +// milliseconds, then nullptr is returned. +std::shared_ptr connect(const char* addr, + int port, + uint32_t timeoutMillis = 0); + +// Server implements a basic TCP server. +class Server { + // ignoreErrors() matches the OnError signature, and does nothing. + static inline void ignoreErrors(const char*) {} + + public: + using OnError = std::function; + using OnConnect = std::function&)>; + + virtual ~Server() = default; + + // create() constructs and returns a new Server. + static std::unique_ptr create(); + + // start() begins listening for connections on localhost and the given port. + // callback will be called for each connection. + // onError will be called for any connection errors. + virtual bool start(int port, + const OnConnect& callback, + const OnError& onError = ignoreErrors) = 0; + + // start() begins listening for connections on the given specific address and port. + // callback will be called for each connection. + // onError will be called for any connection errors. + virtual bool start(const char* address, + int port, + const OnConnect& callback, + const OnError& onError = ignoreErrors) = 0; + + // stop() stops listening for connections. + // stop() is implicitly called on destruction. + virtual void stop() = 0; +}; + +} // namespace net +} // namespace dap + +#endif // dap_network_h diff --git a/libraries/cppdap/include/dap/optional.h b/libraries/cppdap/include/dap/optional.h new file mode 100644 index 000000000..9a3d21667 --- /dev/null +++ b/libraries/cppdap/include/dap/optional.h @@ -0,0 +1,263 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_optional_h +#define dap_optional_h + +#include +#include +#include // std::move, std::forward + +namespace dap { + +// optional holds an 'optional' contained value. +// This is similar to C++17's std::optional. +template +class optional { + template + using IsConvertibleToT = + typename std::enable_if::value>::type; + + public: + using value_type = T; + + // constructors + inline optional() = default; + inline optional(const optional& other); + inline optional(optional&& other); + template + inline optional(const optional& other); + template + inline optional(optional&& other); + template > + inline optional(U&& value); + + // value() returns the contained value. + // If the optional does not contain a value, then value() will assert. + inline T& value(); + inline const T& value() const; + + // value() returns the contained value, or defaultValue if the optional does + // not contain a value. + inline const T& value(const T& defaultValue) const; + + // operator bool() returns true if the optional contains a value. + inline explicit operator bool() const noexcept; + + // has_value() returns true if the optional contains a value. + inline bool has_value() const; + + // assignment + inline optional& operator=(const optional& other); + inline optional& operator=(optional&& other) noexcept; + template > + inline optional& operator=(U&& value); + template + inline optional& operator=(const optional& other); + template + inline optional& operator=(optional&& other); + + // value access + inline const T* operator->() const; + inline T* operator->(); + inline const T& operator*() const; + inline T& operator*(); + + private: + T val{}; + bool set = false; +}; + +template +optional::optional(const optional& other) : val(other.val), set(other.set) {} + +template +optional::optional(optional&& other) + : val(std::move(other.val)), set(other.set) {} + +template +template +optional::optional(const optional& other) : set(other.has_value()) { + if (set) { + val = static_cast(other.value()); + } +} + +template +template +optional::optional(optional&& other) : set(other.has_value()) { + if (set) { + val = static_cast(std::move(other.value())); + } +} + +template +template +optional::optional(U&& value) : val(std::forward(value)), set(true) {} + +template +T& optional::value() { + assert(set); + return val; +} + +template +const T& optional::value() const { + assert(set); + return val; +} + +template +const T& optional::value(const T& defaultValue) const { + if (!has_value()) { + return defaultValue; + } + return val; +} + +template +optional::operator bool() const noexcept { + return set; +} + +template +bool optional::has_value() const { + return set; +} + +template +optional& optional::operator=(const optional& other) { + val = other.val; + set = other.set; + return *this; +} + +template +optional& optional::operator=(optional&& other) noexcept { + val = std::move(other.val); + set = other.set; + return *this; +} + +template +template +optional& optional::operator=(U&& value) { + val = std::forward(value); + set = true; + return *this; +} + +template +template +optional& optional::operator=(const optional& other) { + val = other.val; + set = other.set; + return *this; +} + +template +template +optional& optional::operator=(optional&& other) { + val = std::move(other.val); + set = other.set; + return *this; +} + +template +const T* optional::operator->() const { + assert(set); + return &val; +} + +template +T* optional::operator->() { + assert(set); + return &val; +} + +template +const T& optional::operator*() const { + assert(set); + return val; +} + +template +T& optional::operator*() { + assert(set); + return val; +} + +template +inline bool operator==(const optional& lhs, const optional& rhs) { + if (!lhs.has_value() && !rhs.has_value()) { + return true; + } + if (!lhs.has_value() || !rhs.has_value()) { + return false; + } + return lhs.value() == rhs.value(); +} + +template +inline bool operator!=(const optional& lhs, const optional& rhs) { + return !(lhs == rhs); +} + +template +inline bool operator<(const optional& lhs, const optional& rhs) { + if (!rhs.has_value()) { + return false; + } + if (!lhs.has_value()) { + return true; + } + return lhs.value() < rhs.value(); +} + +template +inline bool operator<=(const optional& lhs, const optional& rhs) { + if (!lhs.has_value()) { + return true; + } + if (!rhs.has_value()) { + return false; + } + return lhs.value() <= rhs.value(); +} + +template +inline bool operator>(const optional& lhs, const optional& rhs) { + if (!lhs.has_value()) { + return false; + } + if (!rhs.has_value()) { + return true; + } + return lhs.value() > rhs.value(); +} + +template +inline bool operator>=(const optional& lhs, const optional& rhs) { + if (!rhs.has_value()) { + return true; + } + if (!lhs.has_value()) { + return false; + } + return lhs.value() >= rhs.value(); +} + +} // namespace dap + +#endif // dap_optional_h diff --git a/libraries/cppdap/include/dap/protocol.h b/libraries/cppdap/include/dap/protocol.h new file mode 100644 index 000000000..db5a0ef9a --- /dev/null +++ b/libraries/cppdap/include/dap/protocol.h @@ -0,0 +1,2909 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated with protocol_gen.go -- do not edit this file. +// go run scripts/protocol_gen/protocol_gen.go +// +// DAP version 1.68.0 + +#ifndef dap_protocol_h +#define dap_protocol_h + +#include "optional.h" +#include "typeinfo.h" +#include "typeof.h" +#include "variant.h" + +#include +#include +#include + +namespace dap { + +struct Request {}; +struct Response {}; +struct Event {}; + +// Response to `attach` request. This is just an acknowledgement, so no body +// field is required. +struct AttachResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(AttachResponse); + +// The `attach` request is sent from the client to the debug adapter to attach +// to a debuggee that is already running. Since attaching is debugger/runtime +// specific, the arguments for this request are not part of this specification. +struct AttachRequest : public Request { + using Response = AttachResponse; + // Arbitrary data from the previous, restarted session. + // The data is sent as the `restart` attribute of the `terminated` event. + // The client should leave the data intact. + optional, boolean, integer, null, number, object, string>> + restart; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(AttachRequest); + +// Names of checksum algorithms that may be supported by a debug adapter. +// +// Must be one of the following enumeration values: +// 'MD5', 'SHA1', 'SHA256', 'timestamp' +using ChecksumAlgorithm = string; + +// The checksum of an item calculated by the specified algorithm. +struct Checksum { + // The algorithm used to calculate this checksum. + ChecksumAlgorithm algorithm = "MD5"; + // Value of the checksum, encoded as a hexadecimal value. + string checksum; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Checksum); + +// A `Source` is a descriptor for source code. +// It is returned from the debug adapter as part of a `StackFrame` and it is +// used by clients when specifying breakpoints. +struct Source { + // Additional data that a debug adapter might want to loop through the client. + // The client should leave the data intact and persist it across sessions. The + // client should not interpret the data. + optional, boolean, integer, null, number, object, string>> + adapterData; + // The checksums associated with this file. + optional> checksums; + // The short name of the source. Every source returned from the debug adapter + // has a name. When sending a source to the debug adapter this name is + // optional. + optional name; + // The origin of this source. For example, 'internal module', 'inlined content + // from source map', etc. + optional origin; + // The path of the source to be shown in the UI. + // It is only used to locate and load the content of the source if no + // `sourceReference` is specified (or its value is 0). + optional path; + // A hint for how to present the source in the UI. + // A value of `deemphasize` can be used to indicate that the source is not + // available or that it is skipped on stepping. + // + // Must be one of the following enumeration values: + // 'normal', 'emphasize', 'deemphasize' + optional presentationHint; + // If the value > 0 the contents of the source must be retrieved through the + // `source` request (even if a path is specified). Since a `sourceReference` + // is only valid for a session, it can not be used to persist a source. The + // value should be less than or equal to 2147483647 (2^31-1). + optional sourceReference; + // A list of sources that are related to this source. These may be the source + // that generated this source. + optional> sources; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Source); + +// Information about a breakpoint created in `setBreakpoints`, +// `setFunctionBreakpoints`, `setInstructionBreakpoints`, or +// `setDataBreakpoints` requests. +struct Breakpoint { + // Start position of the source range covered by the breakpoint. It is + // measured in UTF-16 code units and the client capability `columnsStartAt1` + // determines whether it is 0- or 1-based. + optional column; + // End position of the source range covered by the breakpoint. It is measured + // in UTF-16 code units and the client capability `columnsStartAt1` determines + // whether it is 0- or 1-based. If no end line is given, then the end column + // is assumed to be in the start line. + optional endColumn; + // The end line of the actual range covered by the breakpoint. + optional endLine; + // The identifier for the breakpoint. It is needed if breakpoint events are + // used to update or remove breakpoints. + optional id; + // A memory reference to where the breakpoint is set. + optional instructionReference; + // The start line of the actual range covered by the breakpoint. + optional line; + // A message about the state of the breakpoint. + // This is shown to the user and can be used to explain why a breakpoint could + // not be verified. + optional message; + // The offset from the instruction reference. + // This can be negative. + optional offset; + // A machine-readable explanation of why a breakpoint may not be verified. If + // a breakpoint is verified or a specific reason is not known, the adapter + // should omit this property. Possible values include: + // + // - `pending`: Indicates a breakpoint might be verified in the future, but + // the adapter cannot verify it in the current state. + // - `failed`: Indicates a breakpoint was not able to be verified, and the + // adapter does not believe it can be verified without intervention. + // + // Must be one of the following enumeration values: + // 'pending', 'failed' + optional reason; + // The source where the breakpoint is located. + optional source; + // If true, the breakpoint could be set (but not necessarily at the desired + // location). + boolean verified; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Breakpoint); + +// The event indicates that some information about a breakpoint has changed. +struct BreakpointEvent : public Event { + // The `id` attribute is used to find the target breakpoint, the other + // attributes are used as the new values. + Breakpoint breakpoint; + // The reason for the event. + // + // May be one of the following enumeration values: + // 'changed', 'new', 'removed' + string reason; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(BreakpointEvent); + +// Properties of a breakpoint location returned from the `breakpointLocations` +// request. +struct BreakpointLocation { + // The start position of a breakpoint location. Position is measured in UTF-16 + // code units and the client capability `columnsStartAt1` determines whether + // it is 0- or 1-based. + optional column; + // The end position of a breakpoint location (if the location covers a range). + // Position is measured in UTF-16 code units and the client capability + // `columnsStartAt1` determines whether it is 0- or 1-based. + optional endColumn; + // The end line of breakpoint location if the location covers a range. + optional endLine; + // Start line of breakpoint location. + integer line; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(BreakpointLocation); + +// Response to `breakpointLocations` request. +// Contains possible locations for source breakpoints. +struct BreakpointLocationsResponse : public Response { + // Sorted set of possible breakpoint locations. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(BreakpointLocationsResponse); + +// The `breakpointLocations` request returns all possible locations for source +// breakpoints in a given range. Clients should only call this request if the +// corresponding capability `supportsBreakpointLocationsRequest` is true. +struct BreakpointLocationsRequest : public Request { + using Response = BreakpointLocationsResponse; + // Start position within `line` to search possible breakpoint locations in. It + // is measured in UTF-16 code units and the client capability + // `columnsStartAt1` determines whether it is 0- or 1-based. If no column is + // given, the first position in the start line is assumed. + optional column; + // End position within `endLine` to search possible breakpoint locations in. + // It is measured in UTF-16 code units and the client capability + // `columnsStartAt1` determines whether it is 0- or 1-based. If no end column + // is given, the last position in the end line is assumed. + optional endColumn; + // End line of range to search possible breakpoint locations in. If no end + // line is given, then the end line is assumed to be the start line. + optional endLine; + // Start line of range to search possible breakpoint locations in. If only the + // line is specified, the request returns all possible locations in that line. + integer line; + // The source location of the breakpoints; either `source.path` or + // `source.sourceReference` must be specified. + Source source; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(BreakpointLocationsRequest); + +// Response to `cancel` request. This is just an acknowledgement, so no body +// field is required. +struct CancelResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(CancelResponse); + +// The `cancel` request is used by the client in two situations: +// - to indicate that it is no longer interested in the result produced by a +// specific request issued earlier +// - to cancel a progress sequence. +// Clients should only call this request if the corresponding capability +// `supportsCancelRequest` is true. This request has a hint characteristic: a +// debug adapter can only be expected to make a 'best effort' in honoring this +// request but there are no guarantees. The `cancel` request may return an error +// if it could not cancel an operation but a client should refrain from +// presenting this error to end users. The request that got cancelled still +// needs to send a response back. This can either be a normal result (`success` +// attribute true) or an error response (`success` attribute false and the +// `message` set to `cancelled`). Returning partial results from a cancelled +// request is possible but please note that a client has no generic way for +// detecting that a response is partial or not. The progress that got cancelled +// still needs to send a `progressEnd` event back. +// A client should not assume that progress just got cancelled after sending +// the `cancel` request. +struct CancelRequest : public Request { + using Response = CancelResponse; + // The ID (attribute `progressId`) of the progress to cancel. If missing no + // progress is cancelled. Both a `requestId` and a `progressId` can be + // specified in one request. + optional progressId; + // The ID (attribute `seq`) of the request to cancel. If missing no request is + // cancelled. Both a `requestId` and a `progressId` can be specified in one + // request. + optional requestId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CancelRequest); + +// A `ColumnDescriptor` specifies what module attribute to show in a column of +// the modules view, how to format it, and what the column's label should be. It +// is only used if the underlying UI actually supports this level of +// customization. +struct ColumnDescriptor { + // Name of the attribute rendered in this column. + string attributeName; + // Format to use for the rendered values in this column. TBD how the format + // strings looks like. + optional format; + // Header UI label of column. + string label; + // Datatype of values in this column. Defaults to `string` if not specified. + // + // Must be one of the following enumeration values: + // 'string', 'number', 'boolean', 'unixTimestampUTC' + optional type; + // Width of this column in characters (hint only). + optional width; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ColumnDescriptor); + +// Describes one or more type of breakpoint a `BreakpointMode` applies to. This +// is a non-exhaustive enumeration and may expand as future breakpoint types are +// added. +using BreakpointModeApplicability = string; + +// A `BreakpointMode` is provided as a option when setting breakpoints on +// sources or instructions. +struct BreakpointMode { + // Describes one or more type of breakpoint this mode applies to. + array appliesTo; + // A help text providing additional information about the breakpoint mode. + // This string is typically shown as a hover and can be translated. + optional description; + // The name of the breakpoint mode. This is shown in the UI. + string label; + // The internal ID of the mode. This value is passed to the `setBreakpoints` + // request. + string mode; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(BreakpointMode); + +// An `ExceptionBreakpointsFilter` is shown in the UI as an filter option for +// configuring how exceptions are dealt with. +struct ExceptionBreakpointsFilter { + // A help text providing information about the condition. This string is shown + // as the placeholder text for a text box and can be translated. + optional conditionDescription; + // Initial value of the filter option. If not specified a value false is + // assumed. + optional def; + // A help text providing additional information about the exception filter. + // This string is typically shown as a hover and can be translated. + optional description; + // The internal ID of the filter option. This value is passed to the + // `setExceptionBreakpoints` request. + string filter; + // The name of the filter option. This is shown in the UI. + string label; + // Controls whether a condition can be specified for this filter option. If + // false or missing, a condition can not be set. + optional supportsCondition; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionBreakpointsFilter); + +// Information about the capabilities of a debug adapter. +struct Capabilities { + // The set of additional module information exposed by the debug adapter. + optional> additionalModuleColumns; + // Modes of breakpoints supported by the debug adapter, such as 'hardware' or + // 'software'. If present, the client may allow the user to select a mode and + // include it in its `setBreakpoints` request. + // + // Clients may present the first applicable mode in this array as the + // 'default' mode in gestures that set breakpoints. + optional> breakpointModes; + // The set of characters that should trigger completion in a REPL. If not + // specified, the UI should assume the `.` character. + optional> completionTriggerCharacters; + // Available exception filter options for the `setExceptionBreakpoints` + // request. + optional> exceptionBreakpointFilters; + // The debug adapter supports the `suspendDebuggee` attribute on the + // `disconnect` request. + optional supportSuspendDebuggee; + // The debug adapter supports the `terminateDebuggee` attribute on the + // `disconnect` request. + optional supportTerminateDebuggee; + // Checksum algorithms supported by the debug adapter. + optional> supportedChecksumAlgorithms; + // The debug adapter supports ANSI escape sequences in styling of + // `OutputEvent.output` and `Variable.value` fields. + optional supportsANSIStyling; + // The debug adapter supports the `breakpointLocations` request. + optional supportsBreakpointLocationsRequest; + // The debug adapter supports the `cancel` request. + optional supportsCancelRequest; + // The debug adapter supports the `clipboard` context value in the `evaluate` + // request. + optional supportsClipboardContext; + // The debug adapter supports the `completions` request. + optional supportsCompletionsRequest; + // The debug adapter supports conditional breakpoints. + optional supportsConditionalBreakpoints; + // The debug adapter supports the `configurationDone` request. + optional supportsConfigurationDoneRequest; + // The debug adapter supports the `asAddress` and `bytes` fields in the + // `dataBreakpointInfo` request. + optional supportsDataBreakpointBytes; + // The debug adapter supports data breakpoints. + optional supportsDataBreakpoints; + // The debug adapter supports the delayed loading of parts of the stack, which + // requires that both the `startFrame` and `levels` arguments and the + // `totalFrames` result of the `stackTrace` request are supported. + optional supportsDelayedStackTraceLoading; + // The debug adapter supports the `disassemble` request. + optional supportsDisassembleRequest; + // The debug adapter supports a (side effect free) `evaluate` request for data + // hovers. + optional supportsEvaluateForHovers; + // The debug adapter supports `filterOptions` as an argument on the + // `setExceptionBreakpoints` request. + optional supportsExceptionFilterOptions; + // The debug adapter supports the `exceptionInfo` request. + optional supportsExceptionInfoRequest; + // The debug adapter supports `exceptionOptions` on the + // `setExceptionBreakpoints` request. + optional supportsExceptionOptions; + // The debug adapter supports function breakpoints. + optional supportsFunctionBreakpoints; + // The debug adapter supports the `gotoTargets` request. + optional supportsGotoTargetsRequest; + // The debug adapter supports breakpoints that break execution after a + // specified number of hits. + optional supportsHitConditionalBreakpoints; + // The debug adapter supports adding breakpoints based on instruction + // references. + optional supportsInstructionBreakpoints; + // The debug adapter supports the `loadedSources` request. + optional supportsLoadedSourcesRequest; + // The debug adapter supports log points by interpreting the `logMessage` + // attribute of the `SourceBreakpoint`. + optional supportsLogPoints; + // The debug adapter supports the `modules` request. + optional supportsModulesRequest; + // The debug adapter supports the `readMemory` request. + optional supportsReadMemoryRequest; + // The debug adapter supports restarting a frame. + optional supportsRestartFrame; + // The debug adapter supports the `restart` request. In this case a client + // should not implement `restart` by terminating and relaunching the adapter + // but by calling the `restart` request. + optional supportsRestartRequest; + // The debug adapter supports the `setExpression` request. + optional supportsSetExpression; + // The debug adapter supports setting a variable to a value. + optional supportsSetVariable; + // The debug adapter supports the `singleThread` property on the execution + // requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, + // `stepBack`). + optional supportsSingleThreadExecutionRequests; + // The debug adapter supports stepping back via the `stepBack` and + // `reverseContinue` requests. + optional supportsStepBack; + // The debug adapter supports the `stepInTargets` request. + optional supportsStepInTargetsRequest; + // The debug adapter supports stepping granularities (argument `granularity`) + // for the stepping requests. + optional supportsSteppingGranularity; + // The debug adapter supports the `terminate` request. + optional supportsTerminateRequest; + // The debug adapter supports the `terminateThreads` request. + optional supportsTerminateThreadsRequest; + // The debug adapter supports a `format` attribute on the `stackTrace`, + // `variables`, and `evaluate` requests. + optional supportsValueFormattingOptions; + // The debug adapter supports the `writeMemory` request. + optional supportsWriteMemoryRequest; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Capabilities); + +// The event indicates that one or more capabilities have changed. +// Since the capabilities are dependent on the client and its UI, it might not +// be possible to change that at random times (or too late). Consequently this +// event has a hint characteristic: a client can only be expected to make a +// 'best effort' in honoring individual capabilities but there are no +// guarantees. Only changed capabilities need to be included, all other +// capabilities keep their values. +struct CapabilitiesEvent : public Event { + // The set of updated capabilities. + Capabilities capabilities; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CapabilitiesEvent); + +// Some predefined types for the CompletionItem. Please note that not all +// clients have specific icons for all of them. +// +// Must be one of the following enumeration values: +// 'method', 'function', 'constructor', 'field', 'variable', 'class', +// 'interface', 'module', 'property', 'unit', 'value', 'enum', 'keyword', +// 'snippet', 'text', 'color', 'file', 'reference', 'customcolor' +using CompletionItemType = string; + +// `CompletionItems` are the suggestions returned from the `completions` +// request. +struct CompletionItem { + // A human-readable string with additional information about this item, like + // type or symbol information. + optional detail; + // The label of this completion item. By default this is also the text that is + // inserted when selecting this completion. + string label; + // Length determines how many characters are overwritten by the completion + // text and it is measured in UTF-16 code units. If missing the value 0 is + // assumed which results in the completion text being inserted. + optional length; + // Determines the length of the new selection after the text has been inserted + // (or replaced) and it is measured in UTF-16 code units. The selection can + // not extend beyond the bounds of the completion text. If omitted the length + // is assumed to be 0. + optional selectionLength; + // Determines the start of the new selection after the text has been inserted + // (or replaced). `selectionStart` is measured in UTF-16 code units and must + // be in the range 0 and length of the completion text. If omitted the + // selection starts at the end of the completion text. + optional selectionStart; + // A string that should be used when comparing this item with other items. If + // not returned or an empty string, the `label` is used instead. + optional sortText; + // Start position (within the `text` attribute of the `completions` request) + // where the completion text is added. The position is measured in UTF-16 code + // units and the client capability `columnsStartAt1` determines whether it is + // 0- or 1-based. If the start position is omitted the text is added at the + // location specified by the `column` attribute of the `completions` request. + optional start; + // If text is returned and not an empty string, then it is inserted instead of + // the label. + optional text; + // The item's type. Typically the client uses this information to render the + // item in the UI with an icon. + optional type; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CompletionItem); + +// Response to `completions` request. +struct CompletionsResponse : public Response { + // The possible completions for . + array targets; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CompletionsResponse); + +// Returns a list of possible completions for a given caret position and text. +// Clients should only call this request if the corresponding capability +// `supportsCompletionsRequest` is true. +struct CompletionsRequest : public Request { + using Response = CompletionsResponse; + // The position within `text` for which to determine the completion proposals. + // It is measured in UTF-16 code units and the client capability + // `columnsStartAt1` determines whether it is 0- or 1-based. + integer column; + // Returns completions in the scope of this stack frame. If not specified, the + // completions are returned for the global scope. + optional frameId; + // A line for which to determine the completion proposals. If missing the + // first line of the text is assumed. + optional line; + // One or more source lines. Typically this is the text users have typed into + // the debug console before they asked for completion. + string text; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CompletionsRequest); + +// Response to `configurationDone` request. This is just an acknowledgement, so +// no body field is required. +struct ConfigurationDoneResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(ConfigurationDoneResponse); + +// This request indicates that the client has finished initialization of the +// debug adapter. So it is the last request in the sequence of configuration +// requests (which was started by the `initialized` event). Clients should only +// call this request if the corresponding capability +// `supportsConfigurationDoneRequest` is true. +struct ConfigurationDoneRequest : public Request { + using Response = ConfigurationDoneResponse; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ConfigurationDoneRequest); + +// Response to `continue` request. +struct ContinueResponse : public Response { + // The value true (or a missing property) signals to the client that all + // threads have been resumed. The value false indicates that not all threads + // were resumed. + optional allThreadsContinued; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ContinueResponse); + +// The request resumes execution of all threads. If the debug adapter supports +// single thread execution (see capability +// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument +// to true resumes only the specified thread. If not all threads were resumed, +// the `allThreadsContinued` attribute of the response should be set to false. +struct ContinueRequest : public Request { + using Response = ContinueResponse; + // If this flag is true, execution is resumed only for the thread with given + // `threadId`. + optional singleThread; + // Specifies the active thread. If the debug adapter supports single thread + // execution (see `supportsSingleThreadExecutionRequests`) and the argument + // `singleThread` is true, only the thread with this ID is resumed. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ContinueRequest); + +// The event indicates that the execution of the debuggee has continued. +// Please note: a debug adapter is not expected to send this event in response +// to a request that implies that execution continues, e.g. `launch` or +// `continue`. It is only necessary to send a `continued` event if there was no +// previous request that implied this. +struct ContinuedEvent : public Event { + // If `allThreadsContinued` is true, a debug adapter can announce that all + // threads have continued. + optional allThreadsContinued; + // The thread which was continued. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ContinuedEvent); + +// This enumeration defines all possible access types for data breakpoints. +// +// Must be one of the following enumeration values: +// 'read', 'write', 'readWrite' +using DataBreakpointAccessType = string; + +// Response to `dataBreakpointInfo` request. +struct DataBreakpointInfoResponse : public Response { + // Attribute lists the available access types for a potential data breakpoint. + // A UI client could surface this information. + optional> accessTypes; + // Attribute indicates that a potential data breakpoint could be persisted + // across sessions. + optional canPersist; + // An identifier for the data on which a data breakpoint can be registered + // with the `setDataBreakpoints` request or null if no data breakpoint is + // available. If a `variablesReference` or `frameId` is passed, the `dataId` + // is valid in the current suspended state, otherwise it's valid indefinitely. + // See 'Lifetime of Object References' in the Overview section for details. + // Breakpoints set using the `dataId` in the `setDataBreakpoints` request may + // outlive the lifetime of the associated `dataId`. + variant dataId; + // UI string that describes on what data the breakpoint is set on or why a + // data breakpoint is not available. + string description; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DataBreakpointInfoResponse); + +// Obtains information on a possible data breakpoint that could be set on an +// expression or variable. Clients should only call this request if the +// corresponding capability `supportsDataBreakpoints` is true. +struct DataBreakpointInfoRequest : public Request { + using Response = DataBreakpointInfoResponse; + // If `true`, the `name` is a memory address and the debugger should interpret + // it as a decimal value, or hex value if it is prefixed with `0x`. + // + // Clients may set this property only if the `supportsDataBreakpointBytes` + // capability is true. + optional asAddress; + // If specified, a debug adapter should return information for the range of + // memory extending `bytes` number of bytes from the address or variable + // specified by `name`. Breakpoints set using the resulting data ID should + // pause on data access anywhere within that range. + // + // Clients may set this property only if the `supportsDataBreakpointBytes` + // capability is true. + optional bytes; + // When `name` is an expression, evaluate it in the scope of this stack frame. + // If not specified, the expression is evaluated in the global scope. When + // `variablesReference` is specified, this property has no effect. + optional frameId; + // The mode of the desired breakpoint. If defined, this must be one of the + // `breakpointModes` the debug adapter advertised in its `Capabilities`. + optional mode; + // The name of the variable's child to obtain data breakpoint information for. + // If `variablesReference` isn't specified, this can be an expression, or an + // address if `asAddress` is also true. + string name; + // Reference to the variable container if the data breakpoint is requested for + // a child of the container. The `variablesReference` must have been obtained + // in the current suspended state. See 'Lifetime of Object References' in the + // Overview section for details. + optional variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DataBreakpointInfoRequest); + +// Represents a single disassembled instruction. +struct DisassembledInstruction { + // The address of the instruction. Treated as a hex value if prefixed with + // `0x`, or as a decimal value otherwise. + string address; + // The column within the line that corresponds to this instruction, if any. + optional column; + // The end column of the range that corresponds to this instruction, if any. + optional endColumn; + // The end line of the range that corresponds to this instruction, if any. + optional endLine; + // Text representing the instruction and its operands, in an + // implementation-defined format. + string instruction; + // Raw bytes representing the instruction and its operands, in an + // implementation-defined format. + optional instructionBytes; + // The line within the source location that corresponds to this instruction, + // if any. + optional line; + // Source location that corresponds to this instruction, if any. + // Should always be set (if available) on the first instruction returned, + // but can be omitted afterwards if this instruction maps to the same source + // file as the previous instruction. + optional location; + // A hint for how to present the instruction in the UI. + // + // A value of `invalid` may be used to indicate this instruction is 'filler' + // and cannot be reached by the program. For example, unreadable memory + // addresses may be presented is 'invalid.' + // + // Must be one of the following enumeration values: + // 'normal', 'invalid' + optional presentationHint; + // Name of the symbol that corresponds with the location of this instruction, + // if any. + optional symbol; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DisassembledInstruction); + +// Response to `disassemble` request. +struct DisassembleResponse : public Response { + // The list of disassembled instructions. + array instructions; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DisassembleResponse); + +// Disassembles code stored at the provided location. +// Clients should only call this request if the corresponding capability +// `supportsDisassembleRequest` is true. +struct DisassembleRequest : public Request { + using Response = DisassembleResponse; + // Number of instructions to disassemble starting at the specified location + // and offset. An adapter must return exactly this number of instructions - + // any unavailable instructions should be replaced with an + // implementation-defined 'invalid instruction' value. + integer instructionCount; + // Offset (in instructions) to be applied after the byte offset (if any) + // before disassembling. Can be negative. + optional instructionOffset; + // Memory reference to the base location containing the instructions to + // disassemble. + string memoryReference; + // Offset (in bytes) to be applied to the reference location before + // disassembling. Can be negative. + optional offset; + // If true, the adapter should attempt to resolve memory addresses and other + // values to symbolic names. + optional resolveSymbols; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DisassembleRequest); + +// Response to `disconnect` request. This is just an acknowledgement, so no body +// field is required. +struct DisconnectResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(DisconnectResponse); + +// The `disconnect` request asks the debug adapter to disconnect from the +// debuggee (thus ending the debug session) and then to shut down itself (the +// debug adapter). In addition, the debug adapter must terminate the debuggee if +// it was started with the `launch` request. If an `attach` request was used to +// connect to the debuggee, then the debug adapter must not terminate the +// debuggee. This implicit behavior of when to terminate the debuggee can be +// overridden with the `terminateDebuggee` argument (which is only supported by +// a debug adapter if the corresponding capability `supportTerminateDebuggee` is +// true). +struct DisconnectRequest : public Request { + using Response = DisconnectResponse; + // A value of true indicates that this `disconnect` request is part of a + // restart sequence. + optional restart; + // Indicates whether the debuggee should stay suspended when the debugger is + // disconnected. If unspecified, the debuggee should resume execution. The + // attribute is only honored by a debug adapter if the corresponding + // capability `supportSuspendDebuggee` is true. + optional suspendDebuggee; + // Indicates whether the debuggee should be terminated when the debugger is + // disconnected. If unspecified, the debug adapter is free to do whatever it + // thinks is best. The attribute is only honored by a debug adapter if the + // corresponding capability `supportTerminateDebuggee` is true. + optional terminateDebuggee; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DisconnectRequest); + +// A structured message object. Used to return errors from requests. +struct Message { + // A format string for the message. Embedded variables have the form `{name}`. + // If variable name starts with an underscore character, the variable does not + // contain user data (PII) and can be safely used for telemetry purposes. + string format; + // Unique (within a debug adapter implementation) identifier for the message. + // The purpose of these error IDs is to help extension authors that have the + // requirement that every user visible error message needs a corresponding + // error number, so that users or customer support can find information about + // the specific error more easily. + integer id; + // If true send to telemetry. + optional sendTelemetry; + // If true show user. + optional showUser; + // A url where additional information about this message can be found. + optional url; + // A label that is presented to the user as the UI for opening the url. + optional urlLabel; + // An object used as a dictionary for looking up the variables in the format + // string. + optional variables; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Message); + +// On error (whenever `success` is false), the body can provide more details. +struct ErrorResponse : public Response { + // A structured error message. + optional error; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ErrorResponse); + +// Properties of a variable that can be used to determine how to render the +// variable in the UI. +struct VariablePresentationHint { + // Set of attributes represented as an array of strings. Before introducing + // additional values, try to use the listed values. + optional> attributes; + // The kind of variable. Before introducing additional values, try to use the + // listed values. + // + // May be one of the following enumeration values: + // 'property', 'method', 'class', 'data', 'event', 'baseClass', 'innerClass', + // 'interface', 'mostDerivedClass', 'virtual', 'dataBreakpoint' + optional kind; + // If true, clients can present the variable with a UI that supports a + // specific gesture to trigger its evaluation. This mechanism can be used for + // properties that require executing code when retrieving their value and + // where the code execution can be expensive and/or produce side-effects. A + // typical example are properties based on a getter function. Please note that + // in addition to the `lazy` flag, the variable's `variablesReference` is + // expected to refer to a variable that will provide the value through another + // `variable` request. + optional lazy; + // Visibility of variable. Before introducing additional values, try to use + // the listed values. + // + // May be one of the following enumeration values: + // 'public', 'private', 'protected', 'internal', 'final' + optional visibility; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(VariablePresentationHint); + +// Response to `evaluate` request. +struct EvaluateResponse : public Response { + // The number of indexed child variables. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. The value should be less than or equal to + // 2147483647 (2^31-1). + optional indexedVariables; + // A memory reference to a location appropriate for this result. + // For pointer type eval results, this is generally a reference to the memory + // address contained in the pointer. This attribute may be returned by a debug + // adapter if corresponding capability `supportsMemoryReferences` is true. + optional memoryReference; + // The number of named child variables. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. The value should be less than or equal to + // 2147483647 (2^31-1). + optional namedVariables; + // Properties of an evaluate result that can be used to determine how to + // render the result in the UI. + optional presentationHint; + // The result of the evaluate request. + string result; + // The type of the evaluate result. + // This attribute should only be returned by a debug adapter if the + // corresponding capability `supportsVariableType` is true. + optional type; + // A reference that allows the client to request the location where the + // returned value is declared. For example, if a function pointer is returned, + // the adapter may be able to look up the function's location. This should be + // present only if the adapter is likely to be able to resolve the location. + // + // This reference shares the same lifetime as the `variablesReference`. See + // 'Lifetime of Object References' in the Overview section for details. + optional valueLocationReference; + // If `variablesReference` is > 0, the evaluate result is structured and its + // children can be retrieved by passing `variablesReference` to the + // `variables` request as long as execution remains suspended. See 'Lifetime + // of Object References' in the Overview section for details. + integer variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(EvaluateResponse); + +// Provides formatting information for a value. +struct ValueFormat { + // Display the value in hex. + optional hex; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ValueFormat); + +// Evaluates the given expression in the context of a stack frame. +// The expression has access to any variables and arguments that are in scope. +struct EvaluateRequest : public Request { + using Response = EvaluateResponse; + // The contextual column where the expression should be evaluated. This may be + // provided if `line` is also provided. + // + // It is measured in UTF-16 code units and the client capability + // `columnsStartAt1` determines whether it is 0- or 1-based. + optional column; + // The context in which the evaluate request is used. + // + // May be one of the following enumeration values: + // 'watch', 'repl', 'hover', 'clipboard', 'variables' + optional context; + // The expression to evaluate. + string expression; + // Specifies details on how to format the result. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsValueFormattingOptions` is true. + optional format; + // Evaluate the expression in the scope of this stack frame. If not specified, + // the expression is evaluated in the global scope. + optional frameId; + // The contextual line where the expression should be evaluated. In the + // 'hover' context, this should be set to the start of the expression being + // hovered. + optional line; + // The contextual source in which the `line` is found. This must be provided + // if `line` is provided. + optional source; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(EvaluateRequest); + +// This enumeration defines all possible conditions when a thrown exception +// should result in a break. never: never breaks, always: always breaks, +// unhandled: breaks when exception unhandled, +// userUnhandled: breaks if the exception is not handled by user code. +// +// Must be one of the following enumeration values: +// 'never', 'always', 'unhandled', 'userUnhandled' +using ExceptionBreakMode = string; + +// Detailed information about an exception that has occurred. +struct ExceptionDetails { + // An expression that can be evaluated in the current scope to obtain the + // exception object. + optional evaluateName; + // Fully-qualified type name of the exception object. + optional fullTypeName; + // Details of the exception contained by this exception, if any. + optional> innerException; + // Message contained in the exception. + optional message; + // Stack trace at the time the exception was thrown. + optional stackTrace; + // Short type name of the exception object. + optional typeName; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionDetails); + +// Response to `exceptionInfo` request. +struct ExceptionInfoResponse : public Response { + // Mode that caused the exception notification to be raised. + ExceptionBreakMode breakMode = "never"; + // Descriptive text for the exception. + optional description; + // Detailed information about the exception. + optional details; + // ID of the exception that was thrown. + string exceptionId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionInfoResponse); + +// Retrieves the details of the exception that caused this event to be raised. +// Clients should only call this request if the corresponding capability +// `supportsExceptionInfoRequest` is true. +struct ExceptionInfoRequest : public Request { + using Response = ExceptionInfoResponse; + // Thread for which exception information should be retrieved. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionInfoRequest); + +// The event indicates that the debuggee has exited and returns its exit code. +struct ExitedEvent : public Event { + // The exit code returned from the debuggee. + integer exitCode; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExitedEvent); + +// Response to `goto` request. This is just an acknowledgement, so no body field +// is required. +struct GotoResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(GotoResponse); + +// The request sets the location where the debuggee will continue to run. +// This makes it possible to skip the execution of code or to execute code +// again. The code between the current location and the goto target is not +// executed but skipped. The debug adapter first sends the response and then a +// `stopped` event with reason `goto`. Clients should only call this request if +// the corresponding capability `supportsGotoTargetsRequest` is true (because +// only then goto targets exist that can be passed as arguments). +struct GotoRequest : public Request { + using Response = GotoResponse; + // The location where the debuggee will continue to run. + integer targetId; + // Set the goto target for this thread. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(GotoRequest); + +// A `GotoTarget` describes a code location that can be used as a target in the +// `goto` request. The possible goto targets can be determined via the +// `gotoTargets` request. +struct GotoTarget { + // The column of the goto target. + optional column; + // The end column of the range covered by the goto target. + optional endColumn; + // The end line of the range covered by the goto target. + optional endLine; + // Unique identifier for a goto target. This is used in the `goto` request. + integer id; + // A memory reference for the instruction pointer value represented by this + // target. + optional instructionPointerReference; + // The name of the goto target (shown in the UI). + string label; + // The line of the goto target. + integer line; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(GotoTarget); + +// Response to `gotoTargets` request. +struct GotoTargetsResponse : public Response { + // The possible goto targets of the specified location. + array targets; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(GotoTargetsResponse); + +// This request retrieves the possible goto targets for the specified source +// location. These targets can be used in the `goto` request. Clients should +// only call this request if the corresponding capability +// `supportsGotoTargetsRequest` is true. +struct GotoTargetsRequest : public Request { + using Response = GotoTargetsResponse; + // The position within `line` for which the goto targets are determined. It is + // measured in UTF-16 code units and the client capability `columnsStartAt1` + // determines whether it is 0- or 1-based. + optional column; + // The line location for which the goto targets are determined. + integer line; + // The source location for which the goto targets are determined. + Source source; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(GotoTargetsRequest); + +// Response to `initialize` request. +struct InitializeResponse : public Response { + // The set of additional module information exposed by the debug adapter. + optional> additionalModuleColumns; + // Modes of breakpoints supported by the debug adapter, such as 'hardware' or + // 'software'. If present, the client may allow the user to select a mode and + // include it in its `setBreakpoints` request. + // + // Clients may present the first applicable mode in this array as the + // 'default' mode in gestures that set breakpoints. + optional> breakpointModes; + // The set of characters that should trigger completion in a REPL. If not + // specified, the UI should assume the `.` character. + optional> completionTriggerCharacters; + // Available exception filter options for the `setExceptionBreakpoints` + // request. + optional> exceptionBreakpointFilters; + // The debug adapter supports the `suspendDebuggee` attribute on the + // `disconnect` request. + optional supportSuspendDebuggee; + // The debug adapter supports the `terminateDebuggee` attribute on the + // `disconnect` request. + optional supportTerminateDebuggee; + // Checksum algorithms supported by the debug adapter. + optional> supportedChecksumAlgorithms; + // The debug adapter supports ANSI escape sequences in styling of + // `OutputEvent.output` and `Variable.value` fields. + optional supportsANSIStyling; + // The debug adapter supports the `breakpointLocations` request. + optional supportsBreakpointLocationsRequest; + // The debug adapter supports the `cancel` request. + optional supportsCancelRequest; + // The debug adapter supports the `clipboard` context value in the `evaluate` + // request. + optional supportsClipboardContext; + // The debug adapter supports the `completions` request. + optional supportsCompletionsRequest; + // The debug adapter supports conditional breakpoints. + optional supportsConditionalBreakpoints; + // The debug adapter supports the `configurationDone` request. + optional supportsConfigurationDoneRequest; + // The debug adapter supports the `asAddress` and `bytes` fields in the + // `dataBreakpointInfo` request. + optional supportsDataBreakpointBytes; + // The debug adapter supports data breakpoints. + optional supportsDataBreakpoints; + // The debug adapter supports the delayed loading of parts of the stack, which + // requires that both the `startFrame` and `levels` arguments and the + // `totalFrames` result of the `stackTrace` request are supported. + optional supportsDelayedStackTraceLoading; + // The debug adapter supports the `disassemble` request. + optional supportsDisassembleRequest; + // The debug adapter supports a (side effect free) `evaluate` request for data + // hovers. + optional supportsEvaluateForHovers; + // The debug adapter supports `filterOptions` as an argument on the + // `setExceptionBreakpoints` request. + optional supportsExceptionFilterOptions; + // The debug adapter supports the `exceptionInfo` request. + optional supportsExceptionInfoRequest; + // The debug adapter supports `exceptionOptions` on the + // `setExceptionBreakpoints` request. + optional supportsExceptionOptions; + // The debug adapter supports function breakpoints. + optional supportsFunctionBreakpoints; + // The debug adapter supports the `gotoTargets` request. + optional supportsGotoTargetsRequest; + // The debug adapter supports breakpoints that break execution after a + // specified number of hits. + optional supportsHitConditionalBreakpoints; + // The debug adapter supports adding breakpoints based on instruction + // references. + optional supportsInstructionBreakpoints; + // The debug adapter supports the `loadedSources` request. + optional supportsLoadedSourcesRequest; + // The debug adapter supports log points by interpreting the `logMessage` + // attribute of the `SourceBreakpoint`. + optional supportsLogPoints; + // The debug adapter supports the `modules` request. + optional supportsModulesRequest; + // The debug adapter supports the `readMemory` request. + optional supportsReadMemoryRequest; + // The debug adapter supports restarting a frame. + optional supportsRestartFrame; + // The debug adapter supports the `restart` request. In this case a client + // should not implement `restart` by terminating and relaunching the adapter + // but by calling the `restart` request. + optional supportsRestartRequest; + // The debug adapter supports the `setExpression` request. + optional supportsSetExpression; + // The debug adapter supports setting a variable to a value. + optional supportsSetVariable; + // The debug adapter supports the `singleThread` property on the execution + // requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, + // `stepBack`). + optional supportsSingleThreadExecutionRequests; + // The debug adapter supports stepping back via the `stepBack` and + // `reverseContinue` requests. + optional supportsStepBack; + // The debug adapter supports the `stepInTargets` request. + optional supportsStepInTargetsRequest; + // The debug adapter supports stepping granularities (argument `granularity`) + // for the stepping requests. + optional supportsSteppingGranularity; + // The debug adapter supports the `terminate` request. + optional supportsTerminateRequest; + // The debug adapter supports the `terminateThreads` request. + optional supportsTerminateThreadsRequest; + // The debug adapter supports a `format` attribute on the `stackTrace`, + // `variables`, and `evaluate` requests. + optional supportsValueFormattingOptions; + // The debug adapter supports the `writeMemory` request. + optional supportsWriteMemoryRequest; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(InitializeResponse); + +// The `initialize` request is sent as the first request from the client to the +// debug adapter in order to configure it with client capabilities and to +// retrieve capabilities from the debug adapter. Until the debug adapter has +// responded with an `initialize` response, the client must not send any +// additional requests or events to the debug adapter. In addition the debug +// adapter is not allowed to send any requests or events to the client until it +// has responded with an `initialize` response. The `initialize` request may +// only be sent once. +struct InitializeRequest : public Request { + using Response = InitializeResponse; + // The ID of the debug adapter. + string adapterID; + // The ID of the client using this adapter. + optional clientID; + // The human-readable name of the client using this adapter. + optional clientName; + // If true all column numbers are 1-based (default). + optional columnsStartAt1; + // If true all line numbers are 1-based (default). + optional linesStartAt1; + // The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH. + optional locale; + // Determines in what format paths are specified. The default is `path`, which + // is the native format. + // + // May be one of the following enumeration values: + // 'path', 'uri' + optional pathFormat; + // The client will interpret ANSI escape sequences in the display of + // `OutputEvent.output` and `Variable.value` fields when + // `Capabilities.supportsANSIStyling` is also enabled. + optional supportsANSIStyling; + // Client supports the `argsCanBeInterpretedByShell` attribute on the + // `runInTerminal` request. + optional supportsArgsCanBeInterpretedByShell; + // Client supports the `invalidated` event. + optional supportsInvalidatedEvent; + // Client supports the `memory` event. + optional supportsMemoryEvent; + // Client supports memory references. + optional supportsMemoryReferences; + // Client supports progress reporting. + optional supportsProgressReporting; + // Client supports the `runInTerminal` request. + optional supportsRunInTerminalRequest; + // Client supports the `startDebugging` request. + optional supportsStartDebuggingRequest; + // Client supports the paging of variables. + optional supportsVariablePaging; + // Client supports the `type` attribute for variables. + optional supportsVariableType; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(InitializeRequest); + +// This event indicates that the debug adapter is ready to accept configuration +// requests (e.g. `setBreakpoints`, `setExceptionBreakpoints`). A debug adapter +// is expected to send this event when it is ready to accept configuration +// requests (but not before the `initialize` request has finished). The sequence +// of events/requests is as follows: +// - adapters sends `initialized` event (after the `initialize` request has +// returned) +// - client sends zero or more `setBreakpoints` requests +// - client sends one `setFunctionBreakpoints` request (if corresponding +// capability `supportsFunctionBreakpoints` is true) +// - client sends a `setExceptionBreakpoints` request if one or more +// `exceptionBreakpointFilters` have been defined (or if +// `supportsConfigurationDoneRequest` is not true) +// - client sends other future configuration requests +// - client sends one `configurationDone` request to indicate the end of the +// configuration. +struct InitializedEvent : public Event {}; + +DAP_DECLARE_STRUCT_TYPEINFO(InitializedEvent); + +// Logical areas that can be invalidated by the `invalidated` event. +using InvalidatedAreas = string; + +// This event signals that some state in the debug adapter has changed and +// requires that the client needs to re-render the data snapshot previously +// requested. Debug adapters do not have to emit this event for runtime changes +// like stopped or thread events because in that case the client refetches the +// new state anyway. But the event can be used for example to refresh the UI +// after rendering formatting has changed in the debug adapter. This event +// should only be sent if the corresponding capability +// `supportsInvalidatedEvent` is true. +struct InvalidatedEvent : public Event { + // Set of logical areas that got invalidated. This property has a hint + // characteristic: a client can only be expected to make a 'best effort' in + // honoring the areas but there are no guarantees. If this property is + // missing, empty, or if values are not understood, the client should assume a + // single value `all`. + optional> areas; + // If specified, the client only needs to refetch data related to this stack + // frame (and the `threadId` is ignored). + optional stackFrameId; + // If specified, the client only needs to refetch data related to this thread. + optional threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(InvalidatedEvent); + +// Response to `launch` request. This is just an acknowledgement, so no body +// field is required. +struct LaunchResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(LaunchResponse); + +// This launch request is sent from the client to the debug adapter to start the +// debuggee with or without debugging (if `noDebug` is true). Since launching is +// debugger/runtime specific, the arguments for this request are not part of +// this specification. +struct LaunchRequest : public Request { + using Response = LaunchResponse; + // Arbitrary data from the previous, restarted session. + // The data is sent as the `restart` attribute of the `terminated` event. + // The client should leave the data intact. + optional, boolean, integer, null, number, object, string>> + restart; + // If true, the launch request should launch the program without enabling + // debugging. + optional noDebug; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(LaunchRequest); + +// The event indicates that some source has been added, changed, or removed from +// the set of all loaded sources. +struct LoadedSourceEvent : public Event { + // The reason for the event. + // + // Must be one of the following enumeration values: + // 'new', 'changed', 'removed' + string reason = "new"; + // The new, changed, or removed source. + Source source; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(LoadedSourceEvent); + +// Response to `loadedSources` request. +struct LoadedSourcesResponse : public Response { + // Set of loaded sources. + array sources; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(LoadedSourcesResponse); + +// Retrieves the set of all sources currently loaded by the debugged process. +// Clients should only call this request if the corresponding capability +// `supportsLoadedSourcesRequest` is true. +struct LoadedSourcesRequest : public Request { + using Response = LoadedSourcesResponse; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(LoadedSourcesRequest); + +// Response to `locations` request. +struct LocationsResponse : public Response { + // Position of the location within the `line`. It is measured in UTF-16 code + // units and the client capability `columnsStartAt1` determines whether it is + // 0- or 1-based. If no column is given, the first position in the start line + // is assumed. + optional column; + // End position of the location within `endLine`, present if the location + // refers to a range. It is measured in UTF-16 code units and the client + // capability `columnsStartAt1` determines whether it is 0- or 1-based. + optional endColumn; + // End line of the location, present if the location refers to a range. The + // client capability `linesStartAt1` determines whether it is 0- or 1-based. + optional endLine; + // The line number of the location. The client capability `linesStartAt1` + // determines whether it is 0- or 1-based. + integer line; + // The source containing the location; either `source.path` or + // `source.sourceReference` must be specified. + Source source; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(LocationsResponse); + +// Looks up information about a location reference previously returned by the +// debug adapter. +struct LocationsRequest : public Request { + using Response = LocationsResponse; + // Location reference to resolve. + integer locationReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(LocationsRequest); + +// This event indicates that some memory range has been updated. It should only +// be sent if the corresponding capability `supportsMemoryEvent` is true. +// Clients typically react to the event by re-issuing a `readMemory` request if +// they show the memory identified by the `memoryReference` and if the updated +// memory range overlaps the displayed range. Clients should not make +// assumptions how individual memory references relate to each other, so they +// should not assume that they are part of a single continuous address range and +// might overlap. Debug adapters can use this event to indicate that the +// contents of a memory range has changed due to some other request like +// `setVariable` or `setExpression`. Debug adapters are not expected to emit +// this event for each and every memory change of a running program, because +// that information is typically not available from debuggers and it would flood +// clients with too many events. +struct MemoryEvent : public Event { + // Number of bytes updated. + integer count; + // Memory reference of a memory range that has been updated. + string memoryReference; + // Starting offset in bytes where memory has been updated. Can be negative. + integer offset; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(MemoryEvent); + +// A Module object represents a row in the modules view. +// The `id` attribute identifies a module in the modules view and is used in a +// `module` event for identifying a module for adding, updating or deleting. The +// `name` attribute is used to minimally render the module in the UI. +// +// Additional attributes can be added to the module. They show up in the module +// view if they have a corresponding `ColumnDescriptor`. +// +// To avoid an unnecessary proliferation of additional attributes with similar +// semantics but different names, we recommend to re-use attributes from the +// 'recommended' list below first, and only introduce new attributes if nothing +// appropriate could be found. +struct Module { + // Address range covered by this module. + optional addressRange; + // Module created or modified, encoded as a RFC 3339 timestamp. + optional dateTimeStamp; + // Unique identifier for the module. + variant id; + // True if the module is optimized. + optional isOptimized; + // True if the module is considered 'user code' by a debugger that supports + // 'Just My Code'. + optional isUserCode; + // A name of the module. + string name; + // Logical full path to the module. The exact definition is implementation + // defined, but usually this would be a full path to the on-disk file for the + // module. + optional path; + // Logical full path to the symbol file. The exact definition is + // implementation defined. + optional symbolFilePath; + // User-understandable description of if symbols were found for the module + // (ex: 'Symbols Loaded', 'Symbols not found', etc.) + optional symbolStatus; + // Version of Module. + optional version; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Module); + +// The event indicates that some information about a module has changed. +struct ModuleEvent : public Event { + // The new, changed, or removed module. In case of `removed` only the module + // id is used. + Module module; + // The reason for the event. + // + // Must be one of the following enumeration values: + // 'new', 'changed', 'removed' + string reason = "new"; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ModuleEvent); + +// Response to `modules` request. +struct ModulesResponse : public Response { + // All modules or range of modules. + array modules; + // The total number of modules available. + optional totalModules; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ModulesResponse); + +// Modules can be retrieved from the debug adapter with this request which can +// either return all modules or a range of modules to support paging. Clients +// should only call this request if the corresponding capability +// `supportsModulesRequest` is true. +struct ModulesRequest : public Request { + using Response = ModulesResponse; + // The number of modules to return. If `moduleCount` is not specified or 0, + // all modules are returned. + optional moduleCount; + // The index of the first module to return; if omitted modules start at 0. + optional startModule; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ModulesRequest); + +// Response to `next` request. This is just an acknowledgement, so no body field +// is required. +struct NextResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(NextResponse); + +// The granularity of one 'step' in the stepping requests `next`, `stepIn`, +// `stepOut`, and `stepBack`. +// +// Must be one of the following enumeration values: +// 'statement', 'line', 'instruction' +using SteppingGranularity = string; + +// The request executes one step (in the given granularity) for the specified +// thread and allows all other threads to run freely by resuming them. If the +// debug adapter supports single thread execution (see capability +// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument +// to true prevents other suspended threads from resuming. The debug adapter +// first sends the response and then a `stopped` event (with reason `step`) +// after the step has completed. +struct NextRequest : public Request { + using Response = NextResponse; + // Stepping granularity. If no granularity is specified, a granularity of + // `statement` is assumed. + optional granularity; + // If this flag is true, all other suspended threads are not resumed. + optional singleThread; + // Specifies the thread for which to resume execution for one step (of the + // given granularity). + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(NextRequest); + +// The event indicates that the target has produced some output. +struct OutputEvent : public Event { + // The output category. If not specified or if the category is not understood + // by the client, `console` is assumed. + // + // May be one of the following enumeration values: + // 'console', 'important', 'stdout', 'stderr', 'telemetry' + optional category; + // The position in `line` where the output was produced. It is measured in + // UTF-16 code units and the client capability `columnsStartAt1` determines + // whether it is 0- or 1-based. + optional column; + // Additional data to report. For the `telemetry` category the data is sent to + // telemetry, for the other categories the data is shown in JSON format. + optional, boolean, integer, null, number, object, string>> + data; + // Support for keeping an output log organized by grouping related messages. + // + // Must be one of the following enumeration values: + // 'start', 'startCollapsed', 'end' + optional group; + // The source location's line where the output was produced. + optional line; + // A reference that allows the client to request the location where the new + // value is declared. For example, if the logged value is function pointer, + // the adapter may be able to look up the function's location. This should be + // present only if the adapter is likely to be able to resolve the location. + // + // This reference shares the same lifetime as the `variablesReference`. See + // 'Lifetime of Object References' in the Overview section for details. + optional locationReference; + // The output to report. + // + // ANSI escape sequences may be used to influence text color and styling if + // `supportsANSIStyling` is present in both the adapter's `Capabilities` and + // the client's `InitializeRequestArguments`. A client may strip any + // unrecognized ANSI sequences. + // + // If the `supportsANSIStyling` capabilities are not both true, then the + // client should display the output literally. + string output; + // The source location where the output was produced. + optional source; + // If an attribute `variablesReference` exists and its value is > 0, the + // output contains objects which can be retrieved by passing + // `variablesReference` to the `variables` request as long as execution + // remains suspended. See 'Lifetime of Object References' in the Overview + // section for details. + optional variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(OutputEvent); + +// Response to `pause` request. This is just an acknowledgement, so no body +// field is required. +struct PauseResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(PauseResponse); + +// The request suspends the debuggee. +// The debug adapter first sends the response and then a `stopped` event (with +// reason `pause`) after the thread has been paused successfully. +struct PauseRequest : public Request { + using Response = PauseResponse; + // Pause execution for this thread. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(PauseRequest); + +// The event indicates that the debugger has begun debugging a new process. +// Either one that it has launched, or one that it has attached to. +struct ProcessEvent : public Event { + // If true, the process is running on the same computer as the debug adapter. + optional isLocalProcess; + // The logical name of the process. This is usually the full path to process's + // executable file. Example: /home/example/myproj/program.js. + string name; + // The size of a pointer or address for this process, in bits. This value may + // be used by clients when formatting addresses for display. + optional pointerSize; + // Describes how the debug engine started debugging this process. + // + // Must be one of the following enumeration values: + // 'launch', 'attach', 'attachForSuspendedLaunch' + optional startMethod; + // The process ID of the debugged process, as assigned by the operating + // system. This property should be omitted for logical processes that do not + // map to operating system processes on the machine. + optional systemProcessId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ProcessEvent); + +// The event signals the end of the progress reporting with a final message. +// This event should only be sent if the corresponding capability +// `supportsProgressReporting` is true. +struct ProgressEndEvent : public Event { + // More detailed progress message. If omitted, the previous message (if any) + // is used. + optional message; + // The ID that was introduced in the initial `ProgressStartEvent`. + string progressId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ProgressEndEvent); + +// The event signals that a long running operation is about to start and +// provides additional information for the client to set up a corresponding +// progress and cancellation UI. The client is free to delay the showing of the +// UI in order to reduce flicker. This event should only be sent if the +// corresponding capability `supportsProgressReporting` is true. +struct ProgressStartEvent : public Event { + // If true, the request that reports progress may be cancelled with a `cancel` + // request. So this property basically controls whether the client should use + // UX that supports cancellation. Clients that don't support cancellation are + // allowed to ignore the setting. + optional cancellable; + // More detailed progress message. + optional message; + // Progress percentage to display (value range: 0 to 100). If omitted no + // percentage is shown. + optional percentage; + // An ID that can be used in subsequent `progressUpdate` and `progressEnd` + // events to make them refer to the same progress reporting. IDs must be + // unique within a debug session. + string progressId; + // The request ID that this progress report is related to. If specified a + // debug adapter is expected to emit progress events for the long running + // request until the request has been either completed or cancelled. If the + // request ID is omitted, the progress report is assumed to be related to some + // general activity of the debug adapter. + optional requestId; + // Short title of the progress reporting. Shown in the UI to describe the long + // running operation. + string title; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ProgressStartEvent); + +// The event signals that the progress reporting needs to be updated with a new +// message and/or percentage. The client does not have to update the UI +// immediately, but the clients needs to keep track of the message and/or +// percentage values. This event should only be sent if the corresponding +// capability `supportsProgressReporting` is true. +struct ProgressUpdateEvent : public Event { + // More detailed progress message. If omitted, the previous message (if any) + // is used. + optional message; + // Progress percentage to display (value range: 0 to 100). If omitted no + // percentage is shown. + optional percentage; + // The ID that was introduced in the initial `progressStart` event. + string progressId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ProgressUpdateEvent); + +// Response to `readMemory` request. +struct ReadMemoryResponse : public Response { + // The address of the first byte of data returned. + // Treated as a hex value if prefixed with `0x`, or as a decimal value + // otherwise. + string address; + // The bytes read from memory, encoded using base64. If the decoded length of + // `data` is less than the requested `count` in the original `readMemory` + // request, and `unreadableBytes` is zero or omitted, then the client should + // assume it's reached the end of readable memory. + optional data; + // The number of unreadable bytes encountered after the last successfully read + // byte. This can be used to determine the number of bytes that should be + // skipped before a subsequent `readMemory` request succeeds. + optional unreadableBytes; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ReadMemoryResponse); + +// Reads bytes from memory at the provided location. +// Clients should only call this request if the corresponding capability +// `supportsReadMemoryRequest` is true. +struct ReadMemoryRequest : public Request { + using Response = ReadMemoryResponse; + // Number of bytes to read at the specified location and offset. + integer count; + // Memory reference to the base location from which data should be read. + string memoryReference; + // Offset (in bytes) to be applied to the reference location before reading + // data. Can be negative. + optional offset; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ReadMemoryRequest); + +// Response to `restartFrame` request. This is just an acknowledgement, so no +// body field is required. +struct RestartFrameResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(RestartFrameResponse); + +// The request restarts execution of the specified stack frame. +// The debug adapter first sends the response and then a `stopped` event (with +// reason `restart`) after the restart has completed. Clients should only call +// this request if the corresponding capability `supportsRestartFrame` is true. +struct RestartFrameRequest : public Request { + using Response = RestartFrameResponse; + // Restart the stack frame identified by `frameId`. The `frameId` must have + // been obtained in the current suspended state. See 'Lifetime of Object + // References' in the Overview section for details. + integer frameId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(RestartFrameRequest); + +// Response to `restart` request. This is just an acknowledgement, so no body +// field is required. +struct RestartResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(RestartResponse); + +// Restarts a debug session. Clients should only call this request if the +// corresponding capability `supportsRestartRequest` is true. If the capability +// is missing or has the value false, a typical client emulates `restart` by +// terminating the debug adapter first and then launching it anew. +struct RestartRequest : public Request { + using Response = RestartResponse; + // The latest version of the `launch` or `attach` configuration. + optional arguments; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(RestartRequest); + +// Response to `reverseContinue` request. This is just an acknowledgement, so no +// body field is required. +struct ReverseContinueResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(ReverseContinueResponse); + +// The request resumes backward execution of all threads. If the debug adapter +// supports single thread execution (see capability +// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument +// to true resumes only the specified thread. If not all threads were resumed, +// the `allThreadsContinued` attribute of the response should be set to false. +// Clients should only call this request if the corresponding capability +// `supportsStepBack` is true. +struct ReverseContinueRequest : public Request { + using Response = ReverseContinueResponse; + // If this flag is true, backward execution is resumed only for the thread + // with given `threadId`. + optional singleThread; + // Specifies the active thread. If the debug adapter supports single thread + // execution (see `supportsSingleThreadExecutionRequests`) and the + // `singleThread` argument is true, only the thread with this ID is resumed. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ReverseContinueRequest); + +// Response to `runInTerminal` request. +struct RunInTerminalResponse : public Response { + // The process ID. The value should be less than or equal to 2147483647 + // (2^31-1). + optional processId; + // The process ID of the terminal shell. The value should be less than or + // equal to 2147483647 (2^31-1). + optional shellProcessId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(RunInTerminalResponse); + +// This request is sent from the debug adapter to the client to run a command in +// a terminal. This is typically used to launch the debuggee in a terminal +// provided by the client. This request should only be called if the +// corresponding client capability `supportsRunInTerminalRequest` is true. +// Client implementations of `runInTerminal` are free to run the command however +// they choose including issuing the command to a command line interpreter (aka +// 'shell'). Argument strings passed to the `runInTerminal` request must arrive +// verbatim in the command to be run. As a consequence, clients which use a +// shell are responsible for escaping any special shell characters in the +// argument strings to prevent them from being interpreted (and modified) by the +// shell. Some users may wish to take advantage of shell processing in the +// argument strings. For clients which implement `runInTerminal` using an +// intermediary shell, the `argsCanBeInterpretedByShell` property can be set to +// true. In this case the client is requested not to escape any special shell +// characters in the argument strings. +struct RunInTerminalRequest : public Request { + using Response = RunInTerminalResponse; + // List of arguments. The first argument is the command to run. + array args; + // This property should only be set if the corresponding capability + // `supportsArgsCanBeInterpretedByShell` is true. If the client uses an + // intermediary shell to launch the application, then the client must not + // attempt to escape characters with special meanings for the shell. The user + // is fully responsible for escaping as needed and that arguments using + // special characters may not be portable across shells. + optional argsCanBeInterpretedByShell; + // Working directory for the command. For non-empty, valid paths this + // typically results in execution of a change directory command. + string cwd; + // Environment key-value pairs that are added to or removed from the default + // environment. + optional env; + // What kind of terminal to launch. Defaults to `integrated` if not specified. + // + // Must be one of the following enumeration values: + // 'integrated', 'external' + optional kind; + // Title of the terminal. + optional title; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(RunInTerminalRequest); + +// A `Scope` is a named container for variables. Optionally a scope can map to a +// source or a range within a source. +struct Scope { + // Start position of the range covered by the scope. It is measured in UTF-16 + // code units and the client capability `columnsStartAt1` determines whether + // it is 0- or 1-based. + optional column; + // End position of the range covered by the scope. It is measured in UTF-16 + // code units and the client capability `columnsStartAt1` determines whether + // it is 0- or 1-based. + optional endColumn; + // The end line of the range covered by this scope. + optional endLine; + // If true, the number of variables in this scope is large or expensive to + // retrieve. + boolean expensive; + // The number of indexed variables in this scope. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. + optional indexedVariables; + // The start line of the range covered by this scope. + optional line; + // Name of the scope such as 'Arguments', 'Locals', or 'Registers'. This + // string is shown in the UI as is and can be translated. + string name; + // The number of named variables in this scope. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. + optional namedVariables; + // A hint for how to present this scope in the UI. If this attribute is + // missing, the scope is shown with a generic UI. + // + // May be one of the following enumeration values: + // 'arguments', 'locals', 'registers', 'returnValue' + optional presentationHint; + // The source for this scope. + optional source; + // The variables of this scope can be retrieved by passing the value of + // `variablesReference` to the `variables` request as long as execution + // remains suspended. See 'Lifetime of Object References' in the Overview + // section for details. + integer variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Scope); + +// Response to `scopes` request. +struct ScopesResponse : public Response { + // The scopes of the stack frame. If the array has length zero, there are no + // scopes available. + array scopes; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ScopesResponse); + +// The request returns the variable scopes for a given stack frame ID. +struct ScopesRequest : public Request { + using Response = ScopesResponse; + // Retrieve the scopes for the stack frame identified by `frameId`. The + // `frameId` must have been obtained in the current suspended state. See + // 'Lifetime of Object References' in the Overview section for details. + integer frameId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ScopesRequest); + +// Response to `setBreakpoints` request. +// Returned is information about each breakpoint created by this request. +// This includes the actual code location and whether the breakpoint could be +// verified. The breakpoints returned are in the same order as the elements of +// the `breakpoints` (or the deprecated `lines`) array in the arguments. +struct SetBreakpointsResponse : public Response { + // Information about the breakpoints. + // The array elements are in the same order as the elements of the + // `breakpoints` (or the deprecated `lines`) array in the arguments. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetBreakpointsResponse); + +// Properties of a breakpoint or logpoint passed to the `setBreakpoints` +// request. +struct SourceBreakpoint { + // Start position within source line of the breakpoint or logpoint. It is + // measured in UTF-16 code units and the client capability `columnsStartAt1` + // determines whether it is 0- or 1-based. + optional column; + // The expression for conditional breakpoints. + // It is only honored by a debug adapter if the corresponding capability + // `supportsConditionalBreakpoints` is true. + optional condition; + // The expression that controls how many hits of the breakpoint are ignored. + // The debug adapter is expected to interpret the expression as needed. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsHitConditionalBreakpoints` is true. If both this + // property and `condition` are specified, `hitCondition` should be evaluated + // only if the `condition` is met, and the debug adapter should stop only if + // both conditions are met. + optional hitCondition; + // The source line of the breakpoint or logpoint. + integer line; + // If this attribute exists and is non-empty, the debug adapter must not + // 'break' (stop) but log the message instead. Expressions within `{}` are + // interpolated. The attribute is only honored by a debug adapter if the + // corresponding capability `supportsLogPoints` is true. If either + // `hitCondition` or `condition` is specified, then the message should only be + // logged if those conditions are met. + optional logMessage; + // The mode of this breakpoint. If defined, this must be one of the + // `breakpointModes` the debug adapter advertised in its `Capabilities`. + optional mode; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SourceBreakpoint); + +// Sets multiple breakpoints for a single source and clears all previous +// breakpoints in that source. To clear all breakpoint for a source, specify an +// empty array. When a breakpoint is hit, a `stopped` event (with reason +// `breakpoint`) is generated. +struct SetBreakpointsRequest : public Request { + using Response = SetBreakpointsResponse; + // The code locations of the breakpoints. + optional> breakpoints; + // Deprecated: The code locations of the breakpoints. + optional> lines; + // The source location of the breakpoints; either `source.path` or + // `source.sourceReference` must be specified. + Source source; + // A value of true indicates that the underlying source has been modified + // which results in new breakpoint locations. + optional sourceModified; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetBreakpointsRequest); + +// Response to `setDataBreakpoints` request. +// Returned is information about each breakpoint created by this request. +struct SetDataBreakpointsResponse : public Response { + // Information about the data breakpoints. The array elements correspond to + // the elements of the input argument `breakpoints` array. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetDataBreakpointsResponse); + +// Properties of a data breakpoint passed to the `setDataBreakpoints` request. +struct DataBreakpoint { + // The access type of the data. + optional accessType; + // An expression for conditional breakpoints. + optional condition; + // An id representing the data. This id is returned from the + // `dataBreakpointInfo` request. + string dataId; + // An expression that controls how many hits of the breakpoint are ignored. + // The debug adapter is expected to interpret the expression as needed. + optional hitCondition; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(DataBreakpoint); + +// Replaces all existing data breakpoints with new data breakpoints. +// To clear all data breakpoints, specify an empty array. +// When a data breakpoint is hit, a `stopped` event (with reason `data +// breakpoint`) is generated. Clients should only call this request if the +// corresponding capability `supportsDataBreakpoints` is true. +struct SetDataBreakpointsRequest : public Request { + using Response = SetDataBreakpointsResponse; + // The contents of this array replaces all existing data breakpoints. An empty + // array clears all data breakpoints. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetDataBreakpointsRequest); + +// Response to `setExceptionBreakpoints` request. +// The response contains an array of `Breakpoint` objects with information about +// each exception breakpoint or filter. The `Breakpoint` objects are in the same +// order as the elements of the `filters`, `filterOptions`, `exceptionOptions` +// arrays given as arguments. If both `filters` and `filterOptions` are given, +// the returned array must start with `filters` information first, followed by +// `filterOptions` information. The `verified` property of a `Breakpoint` object +// signals whether the exception breakpoint or filter could be successfully +// created and whether the condition is valid. In case of an error the `message` +// property explains the problem. The `id` property can be used to introduce a +// unique ID for the exception breakpoint or filter so that it can be updated +// subsequently by sending breakpoint events. For backward compatibility both +// the `breakpoints` array and the enclosing `body` are optional. If these +// elements are missing a client is not able to show problems for individual +// exception breakpoints or filters. +struct SetExceptionBreakpointsResponse : public Response { + // Information about the exception breakpoints or filters. + // The breakpoints returned are in the same order as the elements of the + // `filters`, `filterOptions`, `exceptionOptions` arrays in the arguments. If + // both `filters` and `filterOptions` are given, the returned array must start + // with `filters` information first, followed by `filterOptions` information. + optional> breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetExceptionBreakpointsResponse); + +// An `ExceptionPathSegment` represents a segment in a path that is used to +// match leafs or nodes in a tree of exceptions. If a segment consists of more +// than one name, it matches the names provided if `negate` is false or missing, +// or it matches anything except the names provided if `negate` is true. +struct ExceptionPathSegment { + // Depending on the value of `negate` the names that should match or not + // match. + array names; + // If false or missing this segment matches the names provided, otherwise it + // matches anything except the names provided. + optional negate; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionPathSegment); + +// An `ExceptionOptions` assigns configuration options to a set of exceptions. +struct ExceptionOptions { + // Condition when a thrown exception should result in a break. + ExceptionBreakMode breakMode = "never"; + // A path that selects a single or multiple exceptions in a tree. If `path` is + // missing, the whole tree is selected. By convention the first segment of the + // path is a category that is used to group exceptions in the UI. + optional> path; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionOptions); + +// An `ExceptionFilterOptions` is used to specify an exception filter together +// with a condition for the `setExceptionBreakpoints` request. +struct ExceptionFilterOptions { + // An expression for conditional exceptions. + // The exception breaks into the debugger if the result of the condition is + // true. + optional condition; + // ID of an exception filter returned by the `exceptionBreakpointFilters` + // capability. + string filterId; + // The mode of this exception breakpoint. If defined, this must be one of the + // `breakpointModes` the debug adapter advertised in its `Capabilities`. + optional mode; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ExceptionFilterOptions); + +// The request configures the debugger's response to thrown exceptions. Each of +// the `filters`, `filterOptions`, and `exceptionOptions` in the request are +// independent configurations to a debug adapter indicating a kind of exception +// to catch. An exception thrown in a program should result in a `stopped` event +// from the debug adapter (with reason `exception`) if any of the configured +// filters match. Clients should only call this request if the corresponding +// capability `exceptionBreakpointFilters` returns one or more filters. +struct SetExceptionBreakpointsRequest : public Request { + using Response = SetExceptionBreakpointsResponse; + // Configuration options for selected exceptions. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsExceptionOptions` is true. + optional> exceptionOptions; + // Set of exception filters and their options. The set of all possible + // exception filters is defined by the `exceptionBreakpointFilters` + // capability. This attribute is only honored by a debug adapter if the + // corresponding capability `supportsExceptionFilterOptions` is true. The + // `filter` and `filterOptions` sets are additive. + optional> filterOptions; + // Set of exception filters specified by their ID. The set of all possible + // exception filters is defined by the `exceptionBreakpointFilters` + // capability. The `filter` and `filterOptions` sets are additive. + array filters; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetExceptionBreakpointsRequest); + +// Response to `setExpression` request. +struct SetExpressionResponse : public Response { + // The number of indexed child variables. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. The value should be less than or equal to + // 2147483647 (2^31-1). + optional indexedVariables; + // A memory reference to a location appropriate for this result. + // For pointer type eval results, this is generally a reference to the memory + // address contained in the pointer. This attribute may be returned by a debug + // adapter if corresponding capability `supportsMemoryReferences` is true. + optional memoryReference; + // The number of named child variables. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. The value should be less than or equal to + // 2147483647 (2^31-1). + optional namedVariables; + // Properties of a value that can be used to determine how to render the + // result in the UI. + optional presentationHint; + // The type of the value. + // This attribute should only be returned by a debug adapter if the + // corresponding capability `supportsVariableType` is true. + optional type; + // The new value of the expression. + string value; + // A reference that allows the client to request the location where the new + // value is declared. For example, if the new value is function pointer, the + // adapter may be able to look up the function's location. This should be + // present only if the adapter is likely to be able to resolve the location. + // + // This reference shares the same lifetime as the `variablesReference`. See + // 'Lifetime of Object References' in the Overview section for details. + optional valueLocationReference; + // If `variablesReference` is > 0, the evaluate result is structured and its + // children can be retrieved by passing `variablesReference` to the + // `variables` request as long as execution remains suspended. See 'Lifetime + // of Object References' in the Overview section for details. + optional variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetExpressionResponse); + +// Evaluates the given `value` expression and assigns it to the `expression` +// which must be a modifiable l-value. The expressions have access to any +// variables and arguments that are in scope of the specified frame. Clients +// should only call this request if the corresponding capability +// `supportsSetExpression` is true. If a debug adapter implements both +// `setExpression` and `setVariable`, a client uses `setExpression` if the +// variable has an `evaluateName` property. +struct SetExpressionRequest : public Request { + using Response = SetExpressionResponse; + // The l-value expression to assign to. + string expression; + // Specifies how the resulting value should be formatted. + optional format; + // Evaluate the expressions in the scope of this stack frame. If not + // specified, the expressions are evaluated in the global scope. + optional frameId; + // The value expression to assign to the l-value expression. + string value; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetExpressionRequest); + +// Response to `setFunctionBreakpoints` request. +// Returned is information about each breakpoint created by this request. +struct SetFunctionBreakpointsResponse : public Response { + // Information about the breakpoints. The array elements correspond to the + // elements of the `breakpoints` array. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetFunctionBreakpointsResponse); + +// Properties of a breakpoint passed to the `setFunctionBreakpoints` request. +struct FunctionBreakpoint { + // An expression for conditional breakpoints. + // It is only honored by a debug adapter if the corresponding capability + // `supportsConditionalBreakpoints` is true. + optional condition; + // An expression that controls how many hits of the breakpoint are ignored. + // The debug adapter is expected to interpret the expression as needed. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsHitConditionalBreakpoints` is true. + optional hitCondition; + // The name of the function. + string name; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(FunctionBreakpoint); + +// Replaces all existing function breakpoints with new function breakpoints. +// To clear all function breakpoints, specify an empty array. +// When a function breakpoint is hit, a `stopped` event (with reason `function +// breakpoint`) is generated. Clients should only call this request if the +// corresponding capability `supportsFunctionBreakpoints` is true. +struct SetFunctionBreakpointsRequest : public Request { + using Response = SetFunctionBreakpointsResponse; + // The function names of the breakpoints. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetFunctionBreakpointsRequest); + +// Response to `setInstructionBreakpoints` request +struct SetInstructionBreakpointsResponse : public Response { + // Information about the breakpoints. The array elements correspond to the + // elements of the `breakpoints` array. + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetInstructionBreakpointsResponse); + +// Properties of a breakpoint passed to the `setInstructionBreakpoints` request +struct InstructionBreakpoint { + // An expression for conditional breakpoints. + // It is only honored by a debug adapter if the corresponding capability + // `supportsConditionalBreakpoints` is true. + optional condition; + // An expression that controls how many hits of the breakpoint are ignored. + // The debug adapter is expected to interpret the expression as needed. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsHitConditionalBreakpoints` is true. + optional hitCondition; + // The instruction reference of the breakpoint. + // This should be a memory or instruction pointer reference from an + // `EvaluateResponse`, `Variable`, `StackFrame`, `GotoTarget`, or + // `Breakpoint`. + string instructionReference; + // The mode of this breakpoint. If defined, this must be one of the + // `breakpointModes` the debug adapter advertised in its `Capabilities`. + optional mode; + // The offset from the instruction reference in bytes. + // This can be negative. + optional offset; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(InstructionBreakpoint); + +// Replaces all existing instruction breakpoints. Typically, instruction +// breakpoints would be set from a disassembly window. To clear all instruction +// breakpoints, specify an empty array. When an instruction breakpoint is hit, a +// `stopped` event (with reason `instruction breakpoint`) is generated. Clients +// should only call this request if the corresponding capability +// `supportsInstructionBreakpoints` is true. +struct SetInstructionBreakpointsRequest : public Request { + using Response = SetInstructionBreakpointsResponse; + // The instruction references of the breakpoints + array breakpoints; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetInstructionBreakpointsRequest); + +// Response to `setVariable` request. +struct SetVariableResponse : public Response { + // The number of indexed child variables. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. The value should be less than or equal to + // 2147483647 (2^31-1). + optional indexedVariables; + // A memory reference to a location appropriate for this result. + // For pointer type eval results, this is generally a reference to the memory + // address contained in the pointer. This attribute may be returned by a debug + // adapter if corresponding capability `supportsMemoryReferences` is true. + optional memoryReference; + // The number of named child variables. + // The client can use this information to present the variables in a paged UI + // and fetch them in chunks. The value should be less than or equal to + // 2147483647 (2^31-1). + optional namedVariables; + // The type of the new value. Typically shown in the UI when hovering over the + // value. + optional type; + // The new value of the variable. + string value; + // A reference that allows the client to request the location where the new + // value is declared. For example, if the new value is function pointer, the + // adapter may be able to look up the function's location. This should be + // present only if the adapter is likely to be able to resolve the location. + // + // This reference shares the same lifetime as the `variablesReference`. See + // 'Lifetime of Object References' in the Overview section for details. + optional valueLocationReference; + // If `variablesReference` is > 0, the new value is structured and its + // children can be retrieved by passing `variablesReference` to the + // `variables` request as long as execution remains suspended. See 'Lifetime + // of Object References' in the Overview section for details. + // + // If this property is included in the response, any `variablesReference` + // previously associated with the updated variable, and those of its children, + // are no longer valid. + optional variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetVariableResponse); + +// Set the variable with the given name in the variable container to a new +// value. Clients should only call this request if the corresponding capability +// `supportsSetVariable` is true. If a debug adapter implements both +// `setVariable` and `setExpression`, a client will only use `setExpression` if +// the variable has an `evaluateName` property. +struct SetVariableRequest : public Request { + using Response = SetVariableResponse; + // Specifies details on how to format the response value. + optional format; + // The name of the variable in the container. + string name; + // The value of the variable. + string value; + // The reference of the variable container. The `variablesReference` must have + // been obtained in the current suspended state. See 'Lifetime of Object + // References' in the Overview section for details. + integer variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SetVariableRequest); + +// Response to `source` request. +struct SourceResponse : public Response { + // Content of the source reference. + string content; + // Content type (MIME type) of the source. + optional mimeType; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SourceResponse); + +// The request retrieves the source code for a given source reference. +struct SourceRequest : public Request { + using Response = SourceResponse; + // Specifies the source content to load. Either `source.path` or + // `source.sourceReference` must be specified. + optional source; + // The reference to the source. This is the same as `source.sourceReference`. + // This is provided for backward compatibility since old clients do not + // understand the `source` attribute. + integer sourceReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(SourceRequest); + +// A Stackframe contains the source location. +struct StackFrame { + // Indicates whether this frame can be restarted with the `restartFrame` + // request. Clients should only use this if the debug adapter supports the + // `restart` request and the corresponding capability `supportsRestartFrame` + // is true. If a debug adapter has this capability, then `canRestart` defaults + // to `true` if the property is absent. + optional canRestart; + // Start position of the range covered by the stack frame. It is measured in + // UTF-16 code units and the client capability `columnsStartAt1` determines + // whether it is 0- or 1-based. If attribute `source` is missing or doesn't + // exist, `column` is 0 and should be ignored by the client. + integer column; + // End position of the range covered by the stack frame. It is measured in + // UTF-16 code units and the client capability `columnsStartAt1` determines + // whether it is 0- or 1-based. + optional endColumn; + // The end line of the range covered by the stack frame. + optional endLine; + // An identifier for the stack frame. It must be unique across all threads. + // This id can be used to retrieve the scopes of the frame with the `scopes` + // request or to restart the execution of a stack frame. + integer id; + // A memory reference for the current instruction pointer in this frame. + optional instructionPointerReference; + // The line within the source of the frame. If the source attribute is missing + // or doesn't exist, `line` is 0 and should be ignored by the client. + integer line; + // The module associated with this frame, if any. + optional> moduleId; + // The name of the stack frame, typically a method name. + string name; + // A hint for how to present this frame in the UI. + // A value of `label` can be used to indicate that the frame is an artificial + // frame that is used as a visual label or separator. A value of `subtle` can + // be used to change the appearance of a frame in a 'subtle' way. + // + // Must be one of the following enumeration values: + // 'normal', 'label', 'subtle' + optional presentationHint; + // The source of the frame. + optional source; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StackFrame); + +// Response to `stackTrace` request. +struct StackTraceResponse : public Response { + // The frames of the stack frame. If the array has length zero, there are no + // stack frames available. This means that there is no location information + // available. + array stackFrames; + // The total number of frames available in the stack. If omitted or if + // `totalFrames` is larger than the available frames, a client is expected to + // request frames until a request returns less frames than requested (which + // indicates the end of the stack). Returning monotonically increasing + // `totalFrames` values for subsequent requests can be used to enforce paging + // in the client. + optional totalFrames; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StackTraceResponse); + +// Provides formatting information for a stack frame. +struct StackFrameFormat : public ValueFormat { + // Includes all stack frames, including those the debug adapter might + // otherwise hide. + optional includeAll; + // Displays the line number of the stack frame. + optional line; + // Displays the module of the stack frame. + optional module; + // Displays the names of parameters for the stack frame. + optional parameterNames; + // Displays the types of parameters for the stack frame. + optional parameterTypes; + // Displays the values of parameters for the stack frame. + optional parameterValues; + // Displays parameters for the stack frame. + optional parameters; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StackFrameFormat); + +// The request returns a stacktrace from the current execution state of a given +// thread. A client can request all stack frames by omitting the startFrame and +// levels arguments. For performance-conscious clients and if the corresponding +// capability `supportsDelayedStackTraceLoading` is true, stack frames can be +// retrieved in a piecemeal way with the `startFrame` and `levels` arguments. +// The response of the `stackTrace` request may contain a `totalFrames` property +// that hints at the total number of frames in the stack. If a client needs this +// total number upfront, it can issue a request for a single (first) frame and +// depending on the value of `totalFrames` decide how to proceed. In any case a +// client should be prepared to receive fewer frames than requested, which is an +// indication that the end of the stack has been reached. +struct StackTraceRequest : public Request { + using Response = StackTraceResponse; + // Specifies details on how to format the stack frames. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsValueFormattingOptions` is true. + optional format; + // The maximum number of frames to return. If levels is not specified or 0, + // all frames are returned. + optional levels; + // The index of the first frame to return; if omitted frames start at 0. + optional startFrame; + // Retrieve the stacktrace for this thread. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StackTraceRequest); + +// Response to `startDebugging` request. This is just an acknowledgement, so no +// body field is required. +struct StartDebuggingResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(StartDebuggingResponse); + +// This request is sent from the debug adapter to the client to start a new +// debug session of the same type as the caller. This request should only be +// sent if the corresponding client capability `supportsStartDebuggingRequest` +// is true. A client implementation of `startDebugging` should start a new debug +// session (of the same type as the caller) in the same way that the caller's +// session was started. If the client supports hierarchical debug sessions, the +// newly created session can be treated as a child of the caller session. +struct StartDebuggingRequest : public Request { + using Response = StartDebuggingResponse; + // Arguments passed to the new debug session. The arguments must only contain + // properties understood by the `launch` or `attach` requests of the debug + // adapter and they must not contain any client-specific properties (e.g. + // `type`) or client-specific features (e.g. substitutable 'variables'). + object configuration; + // Indicates whether the new debug session should be started with a `launch` + // or `attach` request. + // + // Must be one of the following enumeration values: + // 'launch', 'attach' + string request = "launch"; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StartDebuggingRequest); + +// Response to `stepBack` request. This is just an acknowledgement, so no body +// field is required. +struct StepBackResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepBackResponse); + +// The request executes one backward step (in the given granularity) for the +// specified thread and allows all other threads to run backward freely by +// resuming them. If the debug adapter supports single thread execution (see +// capability `supportsSingleThreadExecutionRequests`), setting the +// `singleThread` argument to true prevents other suspended threads from +// resuming. The debug adapter first sends the response and then a `stopped` +// event (with reason `step`) after the step has completed. Clients should only +// call this request if the corresponding capability `supportsStepBack` is true. +struct StepBackRequest : public Request { + using Response = StepBackResponse; + // Stepping granularity to step. If no granularity is specified, a granularity + // of `statement` is assumed. + optional granularity; + // If this flag is true, all other suspended threads are not resumed. + optional singleThread; + // Specifies the thread for which to resume execution for one step backwards + // (of the given granularity). + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepBackRequest); + +// Response to `stepIn` request. This is just an acknowledgement, so no body +// field is required. +struct StepInResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepInResponse); + +// The request resumes the given thread to step into a function/method and +// allows all other threads to run freely by resuming them. If the debug adapter +// supports single thread execution (see capability +// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument +// to true prevents other suspended threads from resuming. If the request cannot +// step into a target, `stepIn` behaves like the `next` request. The debug +// adapter first sends the response and then a `stopped` event (with reason +// `step`) after the step has completed. If there are multiple function/method +// calls (or other targets) on the source line, the argument `targetId` can be +// used to control into which target the `stepIn` should occur. The list of +// possible targets for a given source line can be retrieved via the +// `stepInTargets` request. +struct StepInRequest : public Request { + using Response = StepInResponse; + // Stepping granularity. If no granularity is specified, a granularity of + // `statement` is assumed. + optional granularity; + // If this flag is true, all other suspended threads are not resumed. + optional singleThread; + // Id of the target to step into. + optional targetId; + // Specifies the thread for which to resume execution for one step-into (of + // the given granularity). + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepInRequest); + +// A `StepInTarget` can be used in the `stepIn` request and determines into +// which single target the `stepIn` request should step. +struct StepInTarget { + // Start position of the range covered by the step in target. It is measured + // in UTF-16 code units and the client capability `columnsStartAt1` determines + // whether it is 0- or 1-based. + optional column; + // End position of the range covered by the step in target. It is measured in + // UTF-16 code units and the client capability `columnsStartAt1` determines + // whether it is 0- or 1-based. + optional endColumn; + // The end line of the range covered by the step-in target. + optional endLine; + // Unique identifier for a step-in target. + integer id; + // The name of the step-in target (shown in the UI). + string label; + // The line of the step-in target. + optional line; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepInTarget); + +// Response to `stepInTargets` request. +struct StepInTargetsResponse : public Response { + // The possible step-in targets of the specified source location. + array targets; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepInTargetsResponse); + +// This request retrieves the possible step-in targets for the specified stack +// frame. These targets can be used in the `stepIn` request. Clients should only +// call this request if the corresponding capability +// `supportsStepInTargetsRequest` is true. +struct StepInTargetsRequest : public Request { + using Response = StepInTargetsResponse; + // The stack frame for which to retrieve the possible step-in targets. + integer frameId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepInTargetsRequest); + +// Response to `stepOut` request. This is just an acknowledgement, so no body +// field is required. +struct StepOutResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepOutResponse); + +// The request resumes the given thread to step out (return) from a +// function/method and allows all other threads to run freely by resuming them. +// If the debug adapter supports single thread execution (see capability +// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument +// to true prevents other suspended threads from resuming. The debug adapter +// first sends the response and then a `stopped` event (with reason `step`) +// after the step has completed. +struct StepOutRequest : public Request { + using Response = StepOutResponse; + // Stepping granularity. If no granularity is specified, a granularity of + // `statement` is assumed. + optional granularity; + // If this flag is true, all other suspended threads are not resumed. + optional singleThread; + // Specifies the thread for which to resume execution for one step-out (of the + // given granularity). + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StepOutRequest); + +// The event indicates that the execution of the debuggee has stopped due to +// some condition. This can be caused by a breakpoint previously set, a stepping +// request has completed, by executing a debugger statement etc. +struct StoppedEvent : public Event { + // If `allThreadsStopped` is true, a debug adapter can announce that all + // threads have stopped. + // - The client should use this information to enable that all threads can be + // expanded to access their stacktraces. + // - If the attribute is missing or false, only the thread with the given + // `threadId` can be expanded. + optional allThreadsStopped; + // The full reason for the event, e.g. 'Paused on exception'. This string is + // shown in the UI as is and can be translated. + optional description; + // Ids of the breakpoints that triggered the event. In most cases there is + // only a single breakpoint but here are some examples for multiple + // breakpoints: + // - Different types of breakpoints map to the same location. + // - Multiple source breakpoints get collapsed to the same instruction by the + // compiler/runtime. + // - Multiple function breakpoints with different function names map to the + // same location. + optional> hitBreakpointIds; + // A value of true hints to the client that this event should not change the + // focus. + optional preserveFocusHint; + // The reason for the event. + // For backward compatibility this string is shown in the UI if the + // `description` attribute is missing (but it must not be translated). + // + // May be one of the following enumeration values: + // 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', 'function + // breakpoint', 'data breakpoint', 'instruction breakpoint' + string reason; + // Additional information. E.g. if reason is `exception`, text contains the + // exception name. This string is shown in the UI. + optional text; + // The thread which was stopped. + optional threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(StoppedEvent); + +// Response to `terminate` request. This is just an acknowledgement, so no body +// field is required. +struct TerminateResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(TerminateResponse); + +// The `terminate` request is sent from the client to the debug adapter in order +// to shut down the debuggee gracefully. Clients should only call this request +// if the capability `supportsTerminateRequest` is true. Typically a debug +// adapter implements `terminate` by sending a software signal which the +// debuggee intercepts in order to clean things up properly before terminating +// itself. Please note that this request does not directly affect the state of +// the debug session: if the debuggee decides to veto the graceful shutdown for +// any reason by not terminating itself, then the debug session just continues. +// Clients can surface the `terminate` request as an explicit command or they +// can integrate it into a two stage Stop command that first sends `terminate` +// to request a graceful shutdown, and if that fails uses `disconnect` for a +// forceful shutdown. +struct TerminateRequest : public Request { + using Response = TerminateResponse; + // A value of true indicates that this `terminate` request is part of a + // restart sequence. + optional restart; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(TerminateRequest); + +// Response to `terminateThreads` request. This is just an acknowledgement, no +// body field is required. +struct TerminateThreadsResponse : public Response {}; + +DAP_DECLARE_STRUCT_TYPEINFO(TerminateThreadsResponse); + +// The request terminates the threads with the given ids. +// Clients should only call this request if the corresponding capability +// `supportsTerminateThreadsRequest` is true. +struct TerminateThreadsRequest : public Request { + using Response = TerminateThreadsResponse; + // Ids of threads to be terminated. + optional> threadIds; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(TerminateThreadsRequest); + +// The event indicates that debugging of the debuggee has terminated. This does +// **not** mean that the debuggee itself has exited. +struct TerminatedEvent : public Event { + // A debug adapter may set `restart` to true (or to an arbitrary object) to + // request that the client restarts the session. The value is not interpreted + // by the client and passed unmodified as an attribute `__restart` to the + // `launch` and `attach` requests. + optional, boolean, integer, null, number, object, string>> + restart; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(TerminatedEvent); + +// The event indicates that a thread has started or exited. +struct ThreadEvent : public Event { + // The reason for the event. + // + // May be one of the following enumeration values: + // 'started', 'exited' + string reason; + // The identifier of the thread. + integer threadId; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ThreadEvent); + +// A Thread +struct Thread { + // Unique identifier for the thread. + integer id; + // The name of the thread. + string name; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Thread); + +// Response to `threads` request. +struct ThreadsResponse : public Response { + // All threads. + array threads; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ThreadsResponse); + +// The request retrieves a list of all threads. +struct ThreadsRequest : public Request { + using Response = ThreadsResponse; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(ThreadsRequest); + +// A Variable is a name/value pair. +// The `type` attribute is shown if space permits or when hovering over the +// variable's name. The `kind` attribute is used to render additional properties +// of the variable, e.g. different icons can be used to indicate that a variable +// is public or private. If the value is structured (has children), a handle is +// provided to retrieve the children with the `variables` request. If the number +// of named or indexed children is large, the numbers should be returned via the +// `namedVariables` and `indexedVariables` attributes. The client can use this +// information to present the children in a paged UI and fetch them in chunks. +struct Variable { + // A reference that allows the client to request the location where the + // variable is declared. This should be present only if the adapter is likely + // to be able to resolve the location. + // + // This reference shares the same lifetime as the `variablesReference`. See + // 'Lifetime of Object References' in the Overview section for details. + optional declarationLocationReference; + // The evaluatable name of this variable which can be passed to the `evaluate` + // request to fetch the variable's value. + optional evaluateName; + // The number of indexed child variables. + // The client can use this information to present the children in a paged UI + // and fetch them in chunks. + optional indexedVariables; + // A memory reference associated with this variable. + // For pointer type variables, this is generally a reference to the memory + // address contained in the pointer. For executable data, this reference may + // later be used in a `disassemble` request. This attribute may be returned by + // a debug adapter if corresponding capability `supportsMemoryReferences` is + // true. + optional memoryReference; + // The variable's name. + string name; + // The number of named child variables. + // The client can use this information to present the children in a paged UI + // and fetch them in chunks. + optional namedVariables; + // Properties of a variable that can be used to determine how to render the + // variable in the UI. + optional presentationHint; + // The type of the variable's value. Typically shown in the UI when hovering + // over the value. This attribute should only be returned by a debug adapter + // if the corresponding capability `supportsVariableType` is true. + optional type; + // The variable's value. + // This can be a multi-line text, e.g. for a function the body of a function. + // For structured variables (which do not have a simple value), it is + // recommended to provide a one-line representation of the structured object. + // This helps to identify the structured object in the collapsed state when + // its children are not yet visible. An empty string can be used if no value + // should be shown in the UI. + string value; + // A reference that allows the client to request the location where the + // variable's value is declared. For example, if the variable contains a + // function pointer, the adapter may be able to look up the function's + // location. This should be present only if the adapter is likely to be able + // to resolve the location. + // + // This reference shares the same lifetime as the `variablesReference`. See + // 'Lifetime of Object References' in the Overview section for details. + optional valueLocationReference; + // If `variablesReference` is > 0, the variable is structured and its children + // can be retrieved by passing `variablesReference` to the `variables` request + // as long as execution remains suspended. See 'Lifetime of Object References' + // in the Overview section for details. + integer variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(Variable); + +// Response to `variables` request. +struct VariablesResponse : public Response { + // All (or a range) of variables for the given variable reference. + array variables; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(VariablesResponse); + +// Retrieves all child variables for the given variable reference. +// A filter can be used to limit the fetched children to either named or indexed +// children. +struct VariablesRequest : public Request { + using Response = VariablesResponse; + // The number of variables to return. If count is missing or 0, all variables + // are returned. The attribute is only honored by a debug adapter if the + // corresponding capability `supportsVariablePaging` is true. + optional count; + // Filter to limit the child variables to either named or indexed. If omitted, + // both types are fetched. + // + // Must be one of the following enumeration values: + // 'indexed', 'named' + optional filter; + // Specifies details on how to format the Variable values. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsValueFormattingOptions` is true. + optional format; + // The index of the first variable to return; if omitted children start at 0. + // The attribute is only honored by a debug adapter if the corresponding + // capability `supportsVariablePaging` is true. + optional start; + // The variable for which to retrieve its children. The `variablesReference` + // must have been obtained in the current suspended state. See 'Lifetime of + // Object References' in the Overview section for details. + integer variablesReference; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(VariablesRequest); + +// Response to `writeMemory` request. +struct WriteMemoryResponse : public Response { + // Property that should be returned when `allowPartial` is true to indicate + // the number of bytes starting from address that were successfully written. + optional bytesWritten; + // Property that should be returned when `allowPartial` is true to indicate + // the offset of the first byte of data successfully written. Can be negative. + optional offset; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(WriteMemoryResponse); + +// Writes bytes to memory at the provided location. +// Clients should only call this request if the corresponding capability +// `supportsWriteMemoryRequest` is true. +struct WriteMemoryRequest : public Request { + using Response = WriteMemoryResponse; + // Property to control partial writes. If true, the debug adapter should + // attempt to write memory even if the entire memory region is not writable. + // In such a case the debug adapter should stop after hitting the first byte + // of memory that cannot be written and return the number of bytes written in + // the response via the `offset` and `bytesWritten` properties. If false or + // missing, a debug adapter should attempt to verify the region is writable + // before writing, and fail the response if it is not. + optional allowPartial; + // Bytes to write, encoded using base64. + string data; + // Memory reference to the base location to which data should be written. + string memoryReference; + // Offset (in bytes) to be applied to the reference location before writing + // data. Can be negative. + optional offset; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(WriteMemoryRequest); + +} // namespace dap + +#endif // dap_protocol_h diff --git a/libraries/cppdap/include/dap/serialization.h b/libraries/cppdap/include/dap/serialization.h new file mode 100644 index 000000000..c7d4c5e07 --- /dev/null +++ b/libraries/cppdap/include/dap/serialization.h @@ -0,0 +1,253 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_serialization_h +#define dap_serialization_h + +#include "typeof.h" +#include "types.h" + +#include // ptrdiff_t +#include + +namespace dap { + +// Field describes a single field of a struct. +struct Field { + std::string name; // name of the field + ptrdiff_t offset; // offset of the field to the base of the struct + const TypeInfo* type; // type of the field +}; + +//////////////////////////////////////////////////////////////////////////////// +// Deserializer +//////////////////////////////////////////////////////////////////////////////// + +// Deserializer is the interface used to decode data from structured storage. +// Methods that return a bool use this to indicate success. +class Deserializer { + public: + virtual ~Deserializer() = default; + + // deserialization methods for simple data types. + // If the stored object is not of the correct type, then these function will + // return false. + virtual bool deserialize(boolean*) const = 0; + virtual bool deserialize(integer*) const = 0; + virtual bool deserialize(number*) const = 0; + virtual bool deserialize(string*) const = 0; + virtual bool deserialize(object*) const = 0; + virtual bool deserialize(any*) const = 0; + + // count() returns the number of elements in the array object referenced by + // this Deserializer. + virtual size_t count() const = 0; + + // array() calls the provided std::function for deserializing each array + // element in the array object referenced by this Deserializer. + virtual bool array(const std::function&) const = 0; + + // field() calls the provided std::function for deserializing the field with + // the given name from the struct object referenced by this Deserializer. + virtual bool field(const std::string& name, + const std::function&) const = 0; + + // deserialize() delegates to TypeOf::type()->deserialize(). + template ::has_custom_serialization>> + inline bool deserialize(T*) const; + + // deserialize() decodes an array. + template + inline bool deserialize(dap::array*) const; + + // deserialize() decodes an optional. + template + inline bool deserialize(dap::optional*) const; + + // deserialize() decodes an variant. + template + inline bool deserialize(dap::variant*) const; + + // deserialize() decodes the struct field f with the given name. + template + inline bool field(const std::string& name, T* f) const; +}; + +template +bool Deserializer::deserialize(T* ptr) const { + return TypeOf::type()->deserialize(this, ptr); +} + +template +bool Deserializer::deserialize(dap::array* vec) const { + auto n = count(); + vec->resize(n); + size_t i = 0; + if (!array([&](Deserializer* d) { return d->deserialize(&(*vec)[i++]); })) { + return false; + } + return true; +} + +template +bool Deserializer::deserialize(dap::optional* opt) const { + T v; + if (deserialize(&v)) { + *opt = v; + } + return true; +} + +template +bool Deserializer::deserialize(dap::variant* var) const { + return deserialize(&var->value); +} + +template +bool Deserializer::field(const std::string& name, T* v) const { + return this->field(name, + [&](const Deserializer* d) { return d->deserialize(v); }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Serializer +//////////////////////////////////////////////////////////////////////////////// +class FieldSerializer; + +// Serializer is the interface used to encode data to structured storage. +// A Serializer is associated with a single storage object, whos type and value +// is assigned by a call to serialize(). +// If serialize() is called multiple times on the same Serializer instance, +// the last type and value is stored. +// Methods that return a bool use this to indicate success. +class Serializer { + public: + virtual ~Serializer() = default; + + // serialization methods for simple data types. + virtual bool serialize(boolean) = 0; + virtual bool serialize(integer) = 0; + virtual bool serialize(number) = 0; + virtual bool serialize(const string&) = 0; + virtual bool serialize(const dap::object&) = 0; + virtual bool serialize(const any&) = 0; + + // array() encodes count array elements to the array object referenced by this + // Serializer. The std::function will be called count times, each time with a + // Serializer that should be used to encode the n'th array element's data. + virtual bool array(size_t count, const std::function&) = 0; + + // object() begins encoding the object referenced by this Serializer. + // The std::function will be called with a FieldSerializer to serialize the + // object's fields. + virtual bool object(const std::function&) = 0; + + // remove() deletes the object referenced by this Serializer. + // remove() can be used to serialize optionals with no value assigned. + virtual void remove() = 0; + + // serialize() delegates to TypeOf::type()->serialize(). + template ::has_custom_serialization>> + inline bool serialize(const T&); + + // serialize() encodes the given array. + template + inline bool serialize(const dap::array&); + + // serialize() encodes the given optional. + template + inline bool serialize(const dap::optional& v); + + // serialize() encodes the given variant. + template + inline bool serialize(const dap::variant&); + + // deserialize() encodes the given string. + inline bool serialize(const char* v); + protected: + static inline const TypeInfo* get_any_type(const any&); + static inline const void* get_any_val(const any&); +}; + +inline const TypeInfo* Serializer::get_any_type(const any& a){ + return a.type; +} +const void* Serializer::get_any_val(const any& a) { + return a.value; +} + +template +bool Serializer::serialize(const T& object) { + return TypeOf::type()->serialize(this, &object); +} + +template +bool Serializer::serialize(const dap::array& vec) { + auto it = vec.begin(); + return array(vec.size(), [&](Serializer* s) { return s->serialize(*it++); }); +} + +template +bool Serializer::serialize(const dap::optional& opt) { + if (!opt.has_value()) { + remove(); + return true; + } + return serialize(opt.value()); +} + +template +bool Serializer::serialize(const dap::variant& var) { + return serialize(var.value); +} + +bool Serializer::serialize(const char* v) { + return serialize(std::string(v)); +} + +//////////////////////////////////////////////////////////////////////////////// +// FieldSerializer +//////////////////////////////////////////////////////////////////////////////// + +// FieldSerializer is the interface used to serialize fields of an object. +class FieldSerializer { + public: + using SerializeFunc = std::function; + template + using IsSerializeFunc = std::is_convertible; + + virtual ~FieldSerializer() = default; + + // field() encodes a field to the struct object referenced by this Serializer. + // The SerializeFunc will be called with a Serializer used to encode the + // field's data. + virtual bool field(const std::string& name, const SerializeFunc&) = 0; + + // field() encodes the field with the given name and value. + template < + typename T, + typename = typename std::enable_if::value>::type> + inline bool field(const std::string& name, const T& v); +}; + +template +bool FieldSerializer::field(const std::string& name, const T& v) { + return this->field(name, [&](Serializer* s) { return s->serialize(v); }); +} + +} // namespace dap + +#endif // dap_serialization_h diff --git a/libraries/cppdap/include/dap/session.h b/libraries/cppdap/include/dap/session.h new file mode 100644 index 000000000..96db04b7d --- /dev/null +++ b/libraries/cppdap/include/dap/session.h @@ -0,0 +1,460 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_session_h +#define dap_session_h + +#include "future.h" +#include "io.h" +#include "traits.h" +#include "typeinfo.h" +#include "typeof.h" + +#include + +namespace dap { + +// Forward declarations +struct Request; +struct Response; +struct Event; + +//////////////////////////////////////////////////////////////////////////////// +// Error +//////////////////////////////////////////////////////////////////////////////// + +// Error represents an error message in response to a DAP request. +struct Error { + Error() = default; + Error(const std::string& error); + Error(const char* msg, ...); + + // operator bool() returns true if there is an error. + inline operator bool() const { return message.size() > 0; } + + std::string message; // empty represents success. +}; + +//////////////////////////////////////////////////////////////////////////////// +// ResponseOrError +//////////////////////////////////////////////////////////////////////////////// + +// ResponseOrError holds either the response to a DAP request or an error +// message. +template +struct ResponseOrError { + using Request = T; + + inline ResponseOrError() = default; + inline ResponseOrError(const T& response); + inline ResponseOrError(T&& response); + inline ResponseOrError(const Error& error); + inline ResponseOrError(Error&& error); + inline ResponseOrError(const ResponseOrError& other); + inline ResponseOrError(ResponseOrError&& other); + + inline ResponseOrError& operator=(const ResponseOrError& other); + inline ResponseOrError& operator=(ResponseOrError&& other); + + T response; + Error error; // empty represents success. +}; + +template +ResponseOrError::ResponseOrError(const T& resp) : response(resp) {} +template +ResponseOrError::ResponseOrError(T&& resp) : response(std::move(resp)) {} +template +ResponseOrError::ResponseOrError(const Error& err) : error(err) {} +template +ResponseOrError::ResponseOrError(Error&& err) : error(std::move(err)) {} +template +ResponseOrError::ResponseOrError(const ResponseOrError& other) + : response(other.response), error(other.error) {} +template +ResponseOrError::ResponseOrError(ResponseOrError&& other) + : response(std::move(other.response)), error(std::move(other.error)) {} +template +ResponseOrError& ResponseOrError::operator=( + const ResponseOrError& other) { + response = other.response; + error = other.error; + return *this; +} +template +ResponseOrError& ResponseOrError::operator=(ResponseOrError&& other) { + response = std::move(other.response); + error = std::move(other.error); + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +// Session +//////////////////////////////////////////////////////////////////////////////// + +// An enum flag that controls how the Session handles invalid data. +enum OnInvalidData { + // Ignore invalid data. + kIgnore, + // Close the underlying reader when invalid data is received. + kClose, +}; + +// Session implements a DAP client or server endpoint. +// The general usage is as follows: +// (1) Create a session with Session::create(). +// (2) Register request and event handlers with registerHandler(). +// (3) Optionally register a protocol error handler with onError(). +// (3) Bind the session to the remote endpoint with bind(). +// (4) Send requests or events with send(). +class Session { + template + using ParamType = traits::ParameterType; + + template + using IsRequest = traits::EnableIfIsType; + + template + using IsEvent = traits::EnableIfIsType; + + template + using IsRequestHandlerWithoutCallback = traits::EnableIf< + traits::CompatibleWith>::value>; + + template + using IsRequestHandlerWithCallback = traits::EnableIf)>>:: + value>; + + public: + virtual ~Session(); + + // ErrorHandler is the type of callback function used for reporting protocol + // errors. + using ErrorHandler = std::function; + + // ClosedHandler is the type of callback function used to signal that a + // connected endpoint has closed. + using ClosedHandler = std::function; + + // create() constructs and returns a new Session. + static std::unique_ptr create(); + + // Sets how the Session handles invalid data. + virtual void setOnInvalidData(OnInvalidData) = 0; + + // onError() registers a error handler that will be called whenever a protocol + // error is encountered. + // Only one error handler can be bound at any given time, and later calls + // will replace the existing error handler. + virtual void onError(const ErrorHandler&) = 0; + + // registerHandler() registers a request handler for a specific request type. + // The function F must have one of the following signatures: + // ResponseOrError(const RequestType&) + // ResponseType(const RequestType&) + // Error(const RequestType&) + template > + inline IsRequestHandlerWithoutCallback registerHandler(F&& handler); + + // registerHandler() registers a request handler for a specific request type. + // The handler has a response callback function for the second argument of the + // handler function. This callback may be called after the handler has + // returned. + // The function F must have the following signature: + // void(const RequestType& request, + // std::function response) + template , + typename ResponseType = typename RequestType::Response> + inline IsRequestHandlerWithCallback registerHandler( + F&& handler); + + // registerHandler() registers a request handler for a specific request type. + // The handler has a response callback function for the second argument of the + // handler function. This callback may be called after the handler has + // returned. + // The function F must have the following signature: + // void(const RequestType& request, + // std::function)> response) + template , + typename ResponseType = typename RequestType::Response> + inline IsRequestHandlerWithCallback> + registerHandler(F&& handler); + + // registerHandler() registers a event handler for a specific event type. + // The function F must have the following signature: + // void(const EventType&) + template > + inline IsEvent registerHandler(F&& handler); + + // registerSentHandler() registers the function F to be called when a response + // of the specific type has been sent. + // The function F must have the following signature: + // void(const ResponseOrError&) + template ::Request> + inline void registerSentHandler(F&& handler); + + // send() sends the request to the connected endpoint and returns a + // future that is assigned the request response or error. + template > + future> send(const T& request); + + // send() sends the event to the connected endpoint. + template > + void send(const T& event); + + // bind() connects this Session to an endpoint using connect(), and then + // starts processing incoming messages with startProcessingMessages(). + // onClose is the optional callback which will be called when the session + // endpoint has been closed. + inline void bind(const std::shared_ptr& reader, + const std::shared_ptr& writer, + const ClosedHandler& onClose); + inline void bind(const std::shared_ptr& readerWriter, + const ClosedHandler& onClose); + + ////////////////////////////////////////////////////////////////////////////// + // Note: + // Methods and members below this point are for advanced usage, and are more + // likely to change signature than the methods above. + // The methods above this point should be sufficient for most use cases. + ////////////////////////////////////////////////////////////////////////////// + + // connect() connects this Session to an endpoint. + // connect() can only be called once. Repeated calls will raise an error, but + // otherwise will do nothing. + // Note: This method is used for explicit control over message handling. + // Most users will use bind() instead of calling this method directly. + virtual void connect(const std::shared_ptr&, + const std::shared_ptr&) = 0; + inline void connect(const std::shared_ptr&); + + // startProcessingMessages() starts a new thread to receive and dispatch + // incoming messages. + // onClose is the optional callback which will be called when the session + // endpoint has been closed. + // Note: This method is used for explicit control over message handling. + // Most users will use bind() instead of calling this method directly. + virtual void startProcessingMessages(const ClosedHandler& onClose = {}) = 0; + + // getPayload() blocks until the next incoming message is received, returning + // the payload or an empty function if the connection was lost. The returned + // payload is function that can be called on any thread to dispatch the + // message to the Session handler. + // Note: This method is used for explicit control over message handling. + // Most users will use bind() instead of calling this method directly. + virtual std::function getPayload() = 0; + + // The callback function type called when a request handler is invoked, and + // the request returns a successful result. + // 'responseTypeInfo' is the type information of the response data structure. + // 'responseData' is a pointer to response payload data. + using RequestHandlerSuccessCallback = + std::function; + + // The callback function type used to notify when a DAP request fails. + // 'responseTypeInfo' is the type information of the response data structure. + // 'message' is the error message + using RequestHandlerErrorCallback = + std::function; + + // The callback function type used to invoke a request handler. + // 'request' is a pointer to the request data structure + // 'onSuccess' is the function to call if the request completed succesfully. + // 'onError' is the function to call if the request failed. + // For each call of the request handler, 'onSuccess' or 'onError' must be + // called exactly once. + using GenericRequestHandler = + std::function; + + // The callback function type used to handle a response to a request. + // 'response' is a pointer to the response data structure. May be nullptr. + // 'error' is a pointer to the reponse error message. May be nullptr. + // One of 'data' or 'error' will be nullptr. + using GenericResponseHandler = + std::function; + + // The callback function type used to handle an event. + // 'event' is a pointer to the event data structure. + using GenericEventHandler = std::function; + + // The callback function type used to notify when a response has been sent + // from this session endpoint. + // 'response' is a pointer to the response data structure. + // 'error' is a pointer to the reponse error message. May be nullptr. + using GenericResponseSentHandler = + std::function; + + // registerHandler() registers 'handler' as the request handler callback for + // requests of the type 'typeinfo'. + virtual void registerHandler(const TypeInfo* typeinfo, + const GenericRequestHandler& handler) = 0; + + // registerHandler() registers 'handler' as the event handler callback for + // events of the type 'typeinfo'. + virtual void registerHandler(const TypeInfo* typeinfo, + const GenericEventHandler& handler) = 0; + + // registerHandler() registers 'handler' as the response-sent handler function + // which is called whenever a response of the type 'typeinfo' is sent from + // this session endpoint. + virtual void registerHandler(const TypeInfo* typeinfo, + const GenericResponseSentHandler& handler) = 0; + + // send() sends a request to the remote endpoint. + // 'requestTypeInfo' is the type info of the request data structure. + // 'requestTypeInfo' is the type info of the response data structure. + // 'request' is a pointer to the request data structure. + // 'responseHandler' is the handler function for the response. + virtual bool send(const dap::TypeInfo* requestTypeInfo, + const dap::TypeInfo* responseTypeInfo, + const void* request, + const GenericResponseHandler& responseHandler) = 0; + + // send() sends an event to the remote endpoint. + // 'eventTypeInfo' is the type info for the event data structure. + // 'event' is a pointer to the event data structure. + virtual bool send(const TypeInfo* eventTypeInfo, const void* event) = 0; +}; + +template +Session::IsRequestHandlerWithoutCallback Session::registerHandler( + F&& handler) { + using ResponseType = typename RequestType::Response; + const TypeInfo* typeinfo = TypeOf::type(); + registerHandler(typeinfo, + [handler](const void* args, + const RequestHandlerSuccessCallback& onSuccess, + const RequestHandlerErrorCallback& onError) { + ResponseOrError res = + handler(*reinterpret_cast(args)); + if (res.error) { + onError(TypeOf::type(), res.error); + } else { + onSuccess(TypeOf::type(), &res.response); + } + }); +} + +template +Session::IsRequestHandlerWithCallback Session::registerHandler( + F&& handler) { + using CallbackType = ParamType; + registerHandler( + TypeOf::type(), + [handler](const void* args, + const RequestHandlerSuccessCallback& onSuccess, + const RequestHandlerErrorCallback&) { + CallbackType responseCallback = [onSuccess](const ResponseType& res) { + onSuccess(TypeOf::type(), &res); + }; + handler(*reinterpret_cast(args), responseCallback); + }); +} + +template +Session::IsRequestHandlerWithCallback> +Session::registerHandler(F&& handler) { + using CallbackType = ParamType; + registerHandler( + TypeOf::type(), + [handler](const void* args, + const RequestHandlerSuccessCallback& onSuccess, + const RequestHandlerErrorCallback& onError) { + CallbackType responseCallback = + [onError, onSuccess](const ResponseOrError& res) { + if (res.error) { + onError(TypeOf::type(), res.error); + } else { + onSuccess(TypeOf::type(), &res.response); + } + }; + handler(*reinterpret_cast(args), responseCallback); + }); +} + +template +Session::IsEvent Session::registerHandler(F&& handler) { + auto cb = [handler](const void* args) { + handler(*reinterpret_cast(args)); + }; + const TypeInfo* typeinfo = TypeOf::type(); + registerHandler(typeinfo, cb); +} + +template +void Session::registerSentHandler(F&& handler) { + auto cb = [handler](const void* response, const Error* error) { + if (error != nullptr) { + handler(ResponseOrError(*error)); + } else { + handler(ResponseOrError(*reinterpret_cast(response))); + } + }; + const TypeInfo* typeinfo = TypeOf::type(); + registerHandler(typeinfo, cb); +} + +template +future> Session::send(const T& request) { + using Response = typename T::Response; + promise> promise; + auto sent = send(TypeOf::type(), TypeOf::type(), &request, + [=](const void* result, const Error* error) { + if (error != nullptr) { + promise.set_value(ResponseOrError(*error)); + } else { + promise.set_value(ResponseOrError( + *reinterpret_cast(result))); + } + }); + if (!sent) { + promise.set_value(Error("Failed to send request")); + } + return promise.get_future(); +} + +template +void Session::send(const T& event) { + const TypeInfo* typeinfo = TypeOf::type(); + send(typeinfo, &event); +} + +void Session::connect(const std::shared_ptr& rw) { + connect(rw, rw); +} + +void Session::bind(const std::shared_ptr& r, + const std::shared_ptr& w, + const ClosedHandler& onClose = {}) { + connect(r, w); + startProcessingMessages(onClose); +} + +void Session::bind(const std::shared_ptr& rw, + const ClosedHandler& onClose = {}) { + bind(rw, rw, onClose); +} + +} // namespace dap + +#endif // dap_session_h diff --git a/libraries/cppdap/include/dap/traits.h b/libraries/cppdap/include/dap/traits.h new file mode 100644 index 000000000..6a0c20d07 --- /dev/null +++ b/libraries/cppdap/include/dap/traits.h @@ -0,0 +1,159 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_traits_h +#define dap_traits_h + +#include +#include + +namespace dap { +namespace traits { + +// NthTypeOf returns the `N`th type in `Types` +template +using NthTypeOf = typename std::tuple_element>::type; + +// `IsTypeOrDerived::value` is true iff `T` is of type `BASE`, or +// derives from `BASE`. +template +using IsTypeOrDerived = std::integral_constant< + bool, + std::is_base_of::type>::value || + std::is_same::type>::value>; + +// `EachIsTypeOrDerived::value` is true iff all of the types in +// the std::tuple `TYPES` is of, or derives from the corresponding indexed type +// in the std::tuple `BASES`. +// `N` must be equal to the number of types in both the std::tuple `BASES` and +// `TYPES`. +template +struct EachIsTypeOrDerived { + using base = typename std::tuple_element::type; + using type = typename std::tuple_element::type; + using last_matches = IsTypeOrDerived; + using others_match = EachIsTypeOrDerived; + static constexpr bool value = last_matches::value && others_match::value; +}; + +// EachIsTypeOrDerived specialization for N = 1 +template +struct EachIsTypeOrDerived<1, BASES, TYPES> { + using base = typename std::tuple_element<0, BASES>::type; + using type = typename std::tuple_element<0, TYPES>::type; + static constexpr bool value = IsTypeOrDerived::value; +}; + +// EachIsTypeOrDerived specialization for N = 0 +template +struct EachIsTypeOrDerived<0, BASES, TYPES> { + static constexpr bool value = true; +}; + +// Signature describes the signature of a function. +template +struct Signature { + // The return type of the function signature + using ret = RETURN; + // The parameters of the function signature held in a std::tuple + using parameters = std::tuple; + // The type of the Nth parameter of function signature + template + using parameter = NthTypeOf; + // The total number of parameters + static constexpr std::size_t parameter_count = sizeof...(PARAMETERS); +}; + +// SignatureOf is a traits helper that infers the signature of the function, +// method, static method, lambda, or function-like object `F`. +template +struct SignatureOf { + // The signature of the function-like object `F` + using type = typename SignatureOf::type; +}; + +// SignatureOf specialization for a regular function or static method. +template +struct SignatureOf { + // The signature of the function-like object `F` + using type = Signature::type, + typename std::decay::type...>; +}; + +// SignatureOf specialization for a non-static method. +template +struct SignatureOf { + // The signature of the function-like object `F` + using type = Signature::type, + typename std::decay::type...>; +}; + +// SignatureOf specialization for a non-static, const method. +template +struct SignatureOf { + // The signature of the function-like object `F` + using type = Signature::type, + typename std::decay::type...>; +}; + +// SignatureOfT is an alias to `typename SignatureOf::type`. +template +using SignatureOfT = typename SignatureOf::type; + +// ParameterType is an alias to `typename SignatureOf::type::parameter`. +template +using ParameterType = typename SignatureOfT::template parameter; + +// `HasSignature::value` is true iff the function-like `F` has a matching +// signature to the function-like `S`. +template +using HasSignature = std::integral_constant< + bool, + std::is_same, SignatureOfT>::value>; + +// `Min::value` resolves to the smaller value of A and B. +template +using Min = std::integral_constant; + +// `CompatibleWith::value` is true iff the function-like `F` +// can be called with the argument types of the function-like `S`. Return type +// of the two functions are not considered. +template +using CompatibleWith = std::integral_constant< + bool, + (SignatureOfT::parameter_count == SignatureOfT::parameter_count) && + EachIsTypeOrDerived::parameter_count, + SignatureOfT::parameter_count>::value, + typename SignatureOfT::parameters, + typename SignatureOfT::parameters>::value>; + +// If `CONDITION` is true then EnableIf resolves to type T, otherwise an +// invalid type. +template +using EnableIf = typename std::enable_if::type; + +// If `BASE` is a base of `T` then EnableIfIsType resolves to type `TRUE_TY`, +// otherwise an invalid type. +template +using EnableIfIsType = EnableIf::value, TRUE_TY>; + +// If the function-like `F` has a matching signature to the function-like `S` +// then EnableIfHasSignature resolves to type `TRUE_TY`, otherwise an invalid type. +template +using EnableIfHasSignature = EnableIf::value, TRUE_TY>; + +} // namespace traits +} // namespace dap + +#endif // dap_traits_h diff --git a/libraries/cppdap/include/dap/typeinfo.h b/libraries/cppdap/include/dap/typeinfo.h new file mode 100644 index 000000000..d99f2777e --- /dev/null +++ b/libraries/cppdap/include/dap/typeinfo.h @@ -0,0 +1,59 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_typeinfo_h +#define dap_typeinfo_h + +#include +#include + +namespace dap { + +class any; +class Deserializer; +class Serializer; + +// The TypeInfo interface provides basic runtime type information about DAP +// types. TypeInfo is used by the serialization system to encode and decode DAP +// requests, responses, events and structs. +struct TypeInfo { + virtual ~TypeInfo(); + virtual std::string name() const = 0; + virtual size_t size() const = 0; + virtual size_t alignment() const = 0; + virtual void construct(void*) const = 0; + virtual void copyConstruct(void* dst, const void* src) const = 0; + virtual void destruct(void*) const = 0; + virtual bool deserialize(const Deserializer*, void*) const = 0; + virtual bool serialize(Serializer*, const void*) const = 0; + + // create() allocates and constructs the TypeInfo of type T, registers the + // pointer for deletion on cppdap library termination, and returns the pointer + // to T. + template + static T* create(ARGS&&... args) { + auto typeinfo = new T(std::forward(args)...); + deleteOnExit(typeinfo); + return typeinfo; + } + + private: + // deleteOnExit() ensures that the TypeInfo is destructed and deleted on + // library termination. + static void deleteOnExit(TypeInfo*); +}; + +} // namespace dap + +#endif // dap_typeinfo_h diff --git a/libraries/cppdap/include/dap/typeof.h b/libraries/cppdap/include/dap/typeof.h new file mode 100644 index 000000000..43c9289c9 --- /dev/null +++ b/libraries/cppdap/include/dap/typeof.h @@ -0,0 +1,266 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_typeof_h +#define dap_typeof_h + +#include "typeinfo.h" +#include "types.h" + +#include "serialization.h" + +namespace dap { + +// BasicTypeInfo is an implementation of the TypeInfo interface for the simple +// template type T. +template +struct BasicTypeInfo : public TypeInfo { + constexpr BasicTypeInfo(std::string&& name) : name_(std::move(name)) {} + + // TypeInfo compliance + inline std::string name() const override { return name_; } + inline size_t size() const override { return sizeof(T); } + inline size_t alignment() const override { return alignof(T); } + inline void construct(void* ptr) const override { new (ptr) T(); } + inline void copyConstruct(void* dst, const void* src) const override { + new (dst) T(*reinterpret_cast(src)); + } + inline void destruct(void* ptr) const override { + reinterpret_cast(ptr)->~T(); + } + inline bool deserialize(const Deserializer* d, void* ptr) const override { + return d->deserialize(reinterpret_cast(ptr)); + } + inline bool serialize(Serializer* s, const void* ptr) const override { + return s->serialize(*reinterpret_cast(ptr)); + } + + private: + std::string name_; +}; + +// TypeOf has a template specialization for each DAP type, each declaring a +// const TypeInfo* type() static member function that describes type T. +template +struct TypeOf {}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template <> +struct TypeOf { + static const TypeInfo* type(); +}; + +template +struct TypeOf> { + static inline const TypeInfo* type() { + static auto typeinfo = TypeInfo::create>>( + "array<" + TypeOf::type()->name() + ">"); + return typeinfo; + } +}; + +template +struct TypeOf> { + static inline const TypeInfo* type() { + static auto typeinfo = + TypeInfo::create>>("variant"); + return typeinfo; + } +}; + +template +struct TypeOf> { + static inline const TypeInfo* type() { + static auto typeinfo = TypeInfo::create>>( + "optional<" + TypeOf::type()->name() + ">"); + return typeinfo; + } +}; + +// DAP_OFFSETOF() macro is a generalization of the offsetof() macro defined in +// . It evaluates to the offset of the given field, with fewer +// restrictions than offsetof(). We cast the address '32' and subtract it again, +// because null-dereference is undefined behavior. +#define DAP_OFFSETOF(s, m) \ + ((int)(size_t) & reinterpret_cast((((s*)32)->m)) - 32) + +// internal functionality +namespace detail { +template +M member_type(M T::*); +} // namespace detail + +// DAP_TYPEOF() returns the type of the struct (s) member (m). +#define DAP_TYPEOF(s, m) decltype(detail::member_type(&s::m)) + +// DAP_FIELD() declares a structure field for the DAP_IMPLEMENT_STRUCT_TYPEINFO +// macro. +// FIELD is the name of the struct field. +// NAME is the serialized name of the field, as described by the DAP +// specification. +#define DAP_FIELD(FIELD, NAME) \ + ::dap::Field { \ + NAME, DAP_OFFSETOF(StructTy, FIELD), \ + TypeOf::type(), \ + } + +// DAP_DECLARE_STRUCT_TYPEINFO() declares a TypeOf<> specialization for STRUCT. +// Must be used within the 'dap' namespace. +#define DAP_DECLARE_STRUCT_TYPEINFO(STRUCT) \ + template <> \ + struct TypeOf { \ + static constexpr bool has_custom_serialization = true; \ + static const TypeInfo* type(); \ + static bool deserializeFields(const Deserializer*, void* obj); \ + static bool serializeFields(FieldSerializer*, const void* obj); \ + } + +// DAP_IMPLEMENT_STRUCT_FIELD_SERIALIZATION() implements the deserializeFields() +// and serializeFields() static methods of a TypeOf<> specialization. Used +// internally by DAP_IMPLEMENT_STRUCT_TYPEINFO() and +// DAP_IMPLEMENT_STRUCT_TYPEINFO_EXT(). +// You probably do not want to use this directly. +#define DAP_IMPLEMENT_STRUCT_FIELD_SERIALIZATION(STRUCT, NAME, ...) \ + bool TypeOf::deserializeFields(const Deserializer* fd, void* obj) { \ + using StructTy = STRUCT; \ + (void)sizeof(StructTy); /* avoid unused 'using' warning */ \ + for (auto& field : std::initializer_list{__VA_ARGS__}) { \ + if (!fd->field(field.name, [&](Deserializer* d) { \ + auto ptr = reinterpret_cast(obj) + field.offset; \ + return field.type->deserialize(d, ptr); \ + })) { \ + return false; \ + } \ + } \ + return true; \ + } \ + bool TypeOf::serializeFields(FieldSerializer* fs, const void* obj) {\ + using StructTy = STRUCT; \ + (void)sizeof(StructTy); /* avoid unused 'using' warning */ \ + for (auto& field : std::initializer_list{__VA_ARGS__}) { \ + if (!fs->field(field.name, [&](Serializer* s) { \ + auto ptr = reinterpret_cast(obj) + field.offset; \ + return field.type->serialize(s, ptr); \ + })) { \ + return false; \ + } \ + } \ + return true; \ + } + +// DAP_IMPLEMENT_STRUCT_TYPEINFO() implements the type() member function for the +// TypeOf<> specialization for STRUCT. +// STRUCT is the structure typename. +// NAME is the serialized name of the structure, as described by the DAP +// specification. The variadic (...) parameters should be a repeated list of +// DAP_FIELD()s, one for each field of the struct. +// Must be used within the 'dap' namespace. +#define DAP_IMPLEMENT_STRUCT_TYPEINFO(STRUCT, NAME, ...) \ + DAP_IMPLEMENT_STRUCT_FIELD_SERIALIZATION(STRUCT, NAME, __VA_ARGS__) \ + const ::dap::TypeInfo* TypeOf::type() { \ + struct TI : BasicTypeInfo { \ + TI() : BasicTypeInfo(NAME) {} \ + bool deserialize(const Deserializer* d, void* obj) const override { \ + return deserializeFields(d, obj); \ + } \ + bool serialize(Serializer* s, const void* obj) const override { \ + return s->object( \ + [&](FieldSerializer* fs) { return serializeFields(fs, obj); }); \ + } \ + }; \ + static TI typeinfo; \ + return &typeinfo; \ + } + +// DAP_STRUCT_TYPEINFO() is a helper for declaring and implementing a TypeOf<> +// specialization for STRUCT in a single statement. +// Must be used within the 'dap' namespace. +#define DAP_STRUCT_TYPEINFO(STRUCT, NAME, ...) \ + DAP_DECLARE_STRUCT_TYPEINFO(STRUCT); \ + DAP_IMPLEMENT_STRUCT_TYPEINFO(STRUCT, NAME, __VA_ARGS__) + +// DAP_IMPLEMENT_STRUCT_TYPEINFO_EXT() implements the type() member function for +// the TypeOf<> specialization for STRUCT that derives from BASE. +// STRUCT is the structure typename. +// BASE is the base structure typename. +// NAME is the serialized name of the structure, as described by the DAP +// specification. The variadic (...) parameters should be a repeated list of +// DAP_FIELD()s, one for each field of the struct. +// Must be used within the 'dap' namespace. +#define DAP_IMPLEMENT_STRUCT_TYPEINFO_EXT(STRUCT, BASE, NAME, ...) \ + static_assert(std::is_base_of::value, \ + #STRUCT " does not derive from " #BASE); \ + DAP_IMPLEMENT_STRUCT_FIELD_SERIALIZATION(STRUCT, NAME, __VA_ARGS__) \ + const ::dap::TypeInfo* TypeOf::type() { \ + struct TI : BasicTypeInfo { \ + TI() : BasicTypeInfo(NAME) {} \ + bool deserialize(const Deserializer* d, void* obj) const override { \ + auto derived = static_cast(obj); \ + auto base = static_cast(obj); \ + return TypeOf::deserializeFields(d, base) && \ + deserializeFields(d, derived); \ + } \ + bool serialize(Serializer* s, const void* obj) const override { \ + return s->object([&](FieldSerializer* fs) { \ + auto derived = static_cast(obj); \ + auto base = static_cast(obj); \ + return TypeOf::serializeFields(fs, base) && \ + serializeFields(fs, derived); \ + }); \ + } \ + }; \ + static TI typeinfo; \ + return &typeinfo; \ + } + +// DAP_STRUCT_TYPEINFO_EXT() is a helper for declaring and implementing a +// TypeOf<> specialization for STRUCT that derives from BASE in a single +// statement. +// Must be used within the 'dap' namespace. +#define DAP_STRUCT_TYPEINFO_EXT(STRUCT, BASE, NAME, ...) \ + DAP_DECLARE_STRUCT_TYPEINFO(STRUCT); \ + DAP_IMPLEMENT_STRUCT_TYPEINFO_EXT(STRUCT, BASE, NAME, __VA_ARGS__) + +} // namespace dap + +#endif // dap_typeof_h diff --git a/libraries/cppdap/include/dap/types.h b/libraries/cppdap/include/dap/types.h new file mode 100644 index 000000000..7954e871c --- /dev/null +++ b/libraries/cppdap/include/dap/types.h @@ -0,0 +1,104 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file holds the basic serializable types used by the debug adapter +// protocol. + +#ifndef dap_types_h +#define dap_types_h + +#include "any.h" +#include "optional.h" +#include "variant.h" + +#include +#include + +#include + +namespace dap { + +// string is a sequence of characters. +// string defaults to an empty string. +using string = std::string; + +// boolean holds a true or false value. +// boolean defaults to false. +class boolean { + public: + inline boolean() : val(false) {} + inline boolean(bool i) : val(i) {} + inline operator bool() const { return val; } + inline boolean& operator=(bool i) { + val = i; + return *this; + } + + private: + bool val; +}; + +// integer holds a whole signed number. +// integer defaults to 0. +class integer { + public: + inline integer() : val(0) {} + inline integer(int64_t i) : val(i) {} + inline operator int64_t() const { return val; } + inline integer& operator=(int64_t i) { + val = i; + return *this; + } + inline integer operator++(int) { + auto copy = *this; + val++; + return copy; + } + + private: + int64_t val; +}; + +// number holds a 64-bit floating point number. +// number defaults to 0. +class number { + public: + inline number() : val(0.0) {} + inline number(double i) : val(i) {} + inline operator double() const { return val; } + inline number& operator=(double i) { + val = i; + return *this; + } + + private: + double val; +}; + +// array is a list of items of type T. +// array defaults to an empty list. +template +using array = std::vector; + +// object is a map of string to any. +// object defaults to an empty map. +using object = std::unordered_map; + +// null represents no value. +// null is used by any to check for no-value. +using null = std::nullptr_t; + +} // namespace dap + +#endif // dap_types_h diff --git a/libraries/cppdap/include/dap/variant.h b/libraries/cppdap/include/dap/variant.h new file mode 100644 index 000000000..96e57c230 --- /dev/null +++ b/libraries/cppdap/include/dap/variant.h @@ -0,0 +1,108 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_variant_h +#define dap_variant_h + +#include "any.h" + +namespace dap { + +// internal functionality +namespace detail { +template +struct TypeIsIn { + static constexpr bool value = false; +}; + +template +struct TypeIsIn { + static constexpr bool value = + std::is_same::value || TypeIsIn::value; +}; +} // namespace detail + +// variant represents a type-safe union of DAP types. +// variant can hold a value of any of the template argument types. +// variant defaults to a default-constructed T0. +template +class variant { + public: + // constructors + inline variant(); + template + inline variant(const T& val); + + // assignment + template + inline variant& operator=(const T& val); + + // get() returns the contained value of the type T. + // If the any does not contain a value of type T, then get() will assert. + template + inline T& get() const; + + // is() returns true iff the contained value is of type T. + template + inline bool is() const; + + // accepts() returns true iff the variant accepts values of type T. + template + static constexpr bool accepts(); + + private: + friend class Serializer; + friend class Deserializer; + any value; +}; + +template +variant::variant() : value(T0()) {} + +template +template +variant::variant(const T& v) : value(v) { + static_assert(accepts(), "variant does not accept template type T"); +} + +template +template +variant& variant::operator=(const T& v) { + static_assert(accepts(), "variant does not accept template type T"); + value = v; + return *this; +} + +template +template +T& variant::get() const { + static_assert(accepts(), "variant does not accept template type T"); + return value.get(); +} + +template +template +bool variant::is() const { + return value.is(); +} + +template +template +constexpr bool variant::accepts() { + return detail::TypeIsIn::value; +} + +} // namespace dap + +#endif // dap_variant_h diff --git a/libraries/cppdap/kokoro/license-check/build.sh b/libraries/cppdap/kokoro/license-check/build.sh new file mode 100755 index 000000000..8b109e56b --- /dev/null +++ b/libraries/cppdap/kokoro/license-check/build.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # Fail on any error. + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )" +ROOT_DIR="$( cd "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd )" + +docker run --rm -i \ + --volume "${ROOT_DIR}:${ROOT_DIR}:ro" \ + --workdir "${ROOT_DIR}" \ + us-east4-docker.pkg.dev/shaderc-build/radial-docker/ubuntu-24.04-amd64/license-checker diff --git a/libraries/cppdap/kokoro/license-check/presubmit.cfg b/libraries/cppdap/kokoro/license-check/presubmit.cfg new file mode 100644 index 000000000..271ad2f6c --- /dev/null +++ b/libraries/cppdap/kokoro/license-check/presubmit.cfg @@ -0,0 +1,4 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +build_file: "cppdap/kokoro/license-check/build.sh" + diff --git a/libraries/cppdap/kokoro/macos/clang-x64/cmake/presubmit.cfg b/libraries/cppdap/kokoro/macos/clang-x64/cmake/presubmit.cfg new file mode 100644 index 000000000..89357a7a3 --- /dev/null +++ b/libraries/cppdap/kokoro/macos/clang-x64/cmake/presubmit.cfg @@ -0,0 +1,14 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Location of the continuous bash script in Git. +build_file: "cppdap/kokoro/macos/presubmit.sh" + +env_vars { + key: "BUILD_SYSTEM" + value: "cmake" +} + +env_vars { + key: "BUILD_TARGET_ARCH" + value: "x64" +} diff --git a/libraries/cppdap/kokoro/macos/presubmit.sh b/libraries/cppdap/kokoro/macos/presubmit.sh new file mode 100755 index 000000000..d74e48595 --- /dev/null +++ b/libraries/cppdap/kokoro/macos/presubmit.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # Fail on any error. +set -x # Display commands being run. + +BUILD_ROOT=$PWD + +cd github/cppdap + +git submodule update --init + +if [ "$BUILD_SYSTEM" == "cmake" ]; then + mkdir build + cd build + + cmake .. -DCPPDAP_BUILD_EXAMPLES=1 -DCPPDAP_BUILD_TESTS=1 -DCPPDAP_WARNINGS_AS_ERRORS=1 + make -j$(sysctl -n hw.logicalcpu) + + ./cppdap-unittests +else + echo "Unknown build system: $BUILD_SYSTEM" + exit 1 +fi diff --git a/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/asan/presubmit.cfg b/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/asan/presubmit.cfg new file mode 100644 index 000000000..f4657cc0b --- /dev/null +++ b/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/asan/presubmit.cfg @@ -0,0 +1,19 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Location of the continuous bash script in Git. +build_file: "cppdap/kokoro/ubuntu/presubmit.sh" + +env_vars { + key: "BUILD_SYSTEM" + value: "cmake" +} + +env_vars { + key: "BUILD_TARGET_ARCH" + value: "x64" +} + +env_vars { + key: "BUILD_SANITIZER" + value: "asan" +} diff --git a/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/presubmit.cfg b/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/presubmit.cfg new file mode 100644 index 000000000..55afbc2a3 --- /dev/null +++ b/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/presubmit.cfg @@ -0,0 +1,14 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Location of the continuous bash script in Git. +build_file: "cppdap/kokoro/ubuntu/presubmit.sh" + +env_vars { + key: "BUILD_SYSTEM" + value: "cmake" +} + +env_vars { + key: "BUILD_TARGET_ARCH" + value: "x64" +} diff --git a/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/tsan/presubmit.cfg b/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/tsan/presubmit.cfg new file mode 100644 index 000000000..3899026ee --- /dev/null +++ b/libraries/cppdap/kokoro/ubuntu/gcc-x64/cmake/tsan/presubmit.cfg @@ -0,0 +1,19 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Location of the continuous bash script in Git. +build_file: "cppdap/kokoro/ubuntu/presubmit.sh" + +env_vars { + key: "BUILD_SYSTEM" + value: "cmake" +} + +env_vars { + key: "BUILD_TARGET_ARCH" + value: "x64" +} + +env_vars { + key: "BUILD_SANITIZER" + value: "tsan" +} diff --git a/libraries/cppdap/kokoro/ubuntu/presubmit-docker.sh b/libraries/cppdap/kokoro/ubuntu/presubmit-docker.sh new file mode 100755 index 000000000..b779160ce --- /dev/null +++ b/libraries/cppdap/kokoro/ubuntu/presubmit-docker.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # Fail on any error. + +. /bin/using.sh # Declare the bash `using` function for configuring toolchains. + +set -x # Display commands being run. + +cd github/cppdap + +# Silence "fatal: unsafe repository" errors +git config --global --add safe.directory '*' + +git submodule update --init + +if [ "$BUILD_SYSTEM" == "cmake" ]; then + using cmake-3.31.2 + using gcc-13 + + mkdir build + cd build + + build_and_run() { + cmake .. -DCPPDAP_BUILD_EXAMPLES=1 -DCPPDAP_BUILD_TESTS=1 -DCPPDAP_WARNINGS_AS_ERRORS=1 $1 + make --jobs=$(nproc) + + ./cppdap-unittests + } + + if [ "$BUILD_SANITIZER" == "asan" ]; then + build_and_run "-DCPPDAP_ASAN=1" + elif [ "$BUILD_SANITIZER" == "msan" ]; then + build_and_run "-DCPPDAP_MSAN=1" + elif [ "$BUILD_SANITIZER" == "tsan" ]; then + build_and_run "-DCPPDAP_TSAN=1" + else + build_and_run + fi +else + echo "Unknown build system: $BUILD_SYSTEM" + exit 1 +fi diff --git a/libraries/cppdap/kokoro/ubuntu/presubmit.sh b/libraries/cppdap/kokoro/ubuntu/presubmit.sh new file mode 100755 index 000000000..b2c5a4395 --- /dev/null +++ b/libraries/cppdap/kokoro/ubuntu/presubmit.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # Fail on any error. + +ROOT_DIR=`pwd` +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )" + +# --privileged is required for some sanitizer builds, as they seem to require PTRACE privileges +docker run --rm -i \ + --privileged \ + --volume "${ROOT_DIR}:${ROOT_DIR}" \ + --volume "${KOKORO_ARTIFACTS_DIR}:/mnt/artifacts" \ + --workdir "${ROOT_DIR}" \ + --env BUILD_SYSTEM=$BUILD_SYSTEM \ + --env BUILD_TARGET_ARCH=$BUILD_TARGET_ARCH \ + --env BUILD_SANITIZER=$BUILD_SANITIZER \ + --entrypoint "${SCRIPT_DIR}/presubmit-docker.sh" \ + us-east4-docker.pkg.dev/shaderc-build/radial-docker/ubuntu-24.04-amd64/cpp-builder diff --git a/libraries/cppdap/kokoro/windows/presubmit.bat b/libraries/cppdap/kokoro/windows/presubmit.bat new file mode 100644 index 000000000..6e6f9bfaa --- /dev/null +++ b/libraries/cppdap/kokoro/windows/presubmit.bat @@ -0,0 +1,45 @@ +REM Copyright 2020 Google LLC +REM +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM https://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. + +@echo on + +SETLOCAL ENABLEDELAYEDEXPANSION + +SET BUILD_ROOT=%cd% +SET PATH=C:\python312;C:\cmake-3.31.2\bin;%PATH% +SET SRC=%cd%\github\cppdap + +cd %SRC% +if !ERRORLEVEL! neq 0 exit !ERRORLEVEL! + +git submodule update --init +if !ERRORLEVEL! neq 0 exit !ERRORLEVEL! + +SET CONFIG=Release + +mkdir %SRC%\build +cd %SRC%\build +if !ERRORLEVEL! neq 0 exit !ERRORLEVEL! + +IF /I "%BUILD_SYSTEM%"=="cmake" ( + cmake .. -G "%BUILD_GENERATOR%" -A %BUILD_TARGET_ARCH% "-DCPPDAP_BUILD_TESTS=1" "-DCPPDAP_BUILD_EXAMPLES=1" "-DCPPDAP_WARNINGS_AS_ERRORS=1" + if !ERRORLEVEL! neq 0 exit !ERRORLEVEL! + cmake --build . --config %CONFIG% + if !ERRORLEVEL! neq 0 exit !ERRORLEVEL! + %CONFIG%\cppdap-unittests.exe + if !ERRORLEVEL! neq 0 exit !ERRORLEVEL! +) ELSE ( + echo "Unknown build system: %BUILD_SYSTEM%" + exit /b 1 +) diff --git a/libraries/cppdap/kokoro/windows/vs2022-amd64/cmake/presubmit.cfg b/libraries/cppdap/kokoro/windows/vs2022-amd64/cmake/presubmit.cfg new file mode 100644 index 000000000..5800334c2 --- /dev/null +++ b/libraries/cppdap/kokoro/windows/vs2022-amd64/cmake/presubmit.cfg @@ -0,0 +1,19 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Location of the continuous bash script in Git. +build_file: "cppdap/kokoro/windows/presubmit.bat" + +env_vars { + key: "BUILD_SYSTEM" + value: "cmake" +} + +env_vars { + key: "BUILD_GENERATOR" + value: "Visual Studio 17 2022" +} + +env_vars { + key: "BUILD_TARGET_ARCH" + value: "x64" +} diff --git a/libraries/cppdap/kokoro/windows/vs2022-x86/cmake/presubmit.cfg b/libraries/cppdap/kokoro/windows/vs2022-x86/cmake/presubmit.cfg new file mode 100644 index 000000000..bf3d29f14 --- /dev/null +++ b/libraries/cppdap/kokoro/windows/vs2022-x86/cmake/presubmit.cfg @@ -0,0 +1,19 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Location of the continuous bash script in Git. +build_file: "cppdap/kokoro/windows/presubmit.bat" + +env_vars { + key: "BUILD_SYSTEM" + value: "cmake" +} + +env_vars { + key: "BUILD_GENERATOR" + value: "Visual Studio 17 2022" +} + +env_vars { + key: "BUILD_TARGET_ARCH" + value: "Win32" +} diff --git a/libraries/cppdap/license-checker.cfg b/libraries/cppdap/license-checker.cfg new file mode 100644 index 000000000..163aec746 --- /dev/null +++ b/libraries/cppdap/license-checker.cfg @@ -0,0 +1,28 @@ +{ + "licenses": [ + "Apache-2.0", + "Apache-2.0-Header" + ], + "paths": [ + { + "exclude": [ + ".clang-format", + ".gitattributes", + ".github/workflows/main.yml", + ".gitignore", + ".gitmodules", + ".vscode/*.json", + "**.md", + "CONTRIBUTING", + "LICENSE", + "build/**", + "examples/vscode/package.json", + "fuzz/**", + "kokoro/**.cfg", + "third_party/googletest/**", + "third_party/json/**", + "third_party/rapidjson/**" + ] + } + ] +} diff --git a/libraries/cppdap/src/any_test.cpp b/libraries/cppdap/src/any_test.cpp new file mode 100644 index 000000000..7dfb73ce8 --- /dev/null +++ b/libraries/cppdap/src/any_test.cpp @@ -0,0 +1,262 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/any.h" +#include "dap/typeof.h" +#include "dap/types.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace dap { + +struct AnyTestObject { + dap::integer i; + dap::number n; +}; + +DAP_STRUCT_TYPEINFO(AnyTestObject, + "AnyTestObject", + DAP_FIELD(i, "i"), + DAP_FIELD(n, "n")); + +inline bool operator==(const AnyTestObject& a, const AnyTestObject& b) { + return a.i == b.i && a.n == b.n; +} + +} // namespace dap + +namespace { + +template +struct TestValue {}; + +template <> +struct TestValue { + static const dap::null value; +}; +template <> +struct TestValue { + static const dap::integer value; +}; +template <> +struct TestValue { + static const dap::boolean value; +}; +template <> +struct TestValue { + static const dap::number value; +}; +template <> +struct TestValue { + static const dap::string value; +}; +template <> +struct TestValue> { + static const dap::array value; +}; +template <> +struct TestValue { + static const dap::AnyTestObject value; +}; + +const dap::null TestValue::value = nullptr; +const dap::integer TestValue::value = 20; +const dap::boolean TestValue::value = true; +const dap::number TestValue::value = 123.45; +const dap::string TestValue::value = "hello world"; +const dap::array TestValue>::value = { + "one", "two", "three"}; +const dap::AnyTestObject TestValue::value = {10, 20.30}; + +} // namespace + +TEST(Any, EmptyConstruct) { + dap::any any; + ASSERT_TRUE(any.is()); + ASSERT_FALSE(any.is()); + ASSERT_FALSE(any.is()); + ASSERT_FALSE(any.is()); + ASSERT_FALSE(any.is()); + ASSERT_FALSE(any.is()); + ASSERT_FALSE(any.is>()); + ASSERT_FALSE(any.is()); +} + +TEST(Any, Boolean) { + dap::any any(dap::boolean(true)); + ASSERT_TRUE(any.is()); + ASSERT_EQ(any.get(), dap::boolean(true)); +} + +TEST(Any, Integer) { + dap::any any(dap::integer(10)); + ASSERT_TRUE(any.is()); + ASSERT_EQ(any.get(), dap::integer(10)); +} + +TEST(Any, Number) { + dap::any any(dap::number(123.0f)); + ASSERT_TRUE(any.is()); + ASSERT_EQ(any.get(), dap::number(123.0f)); +} + +TEST(Any, String) { + dap::any any(dap::string("hello world")); + ASSERT_TRUE(any.is()); + ASSERT_EQ(any.get(), dap::string("hello world")); +} + +TEST(Any, Array) { + using array = dap::array; + dap::any any(array({10, 20, 30})); + ASSERT_TRUE(any.is()); + ASSERT_EQ(any.get(), array({10, 20, 30})); +} + +TEST(Any, Object) { + dap::object o; + o["one"] = dap::integer(1); + o["two"] = dap::integer(2); + o["three"] = dap::integer(3); + dap::any any(o); + ASSERT_TRUE(any.is()); + if (any.is()) { + auto got = any.get(); + ASSERT_EQ(got.size(), 3U); + ASSERT_EQ(got.count("one"), 1U); + ASSERT_EQ(got.count("two"), 1U); + ASSERT_EQ(got.count("three"), 1U); + ASSERT_TRUE(got["one"].is()); + ASSERT_TRUE(got["two"].is()); + ASSERT_TRUE(got["three"].is()); + ASSERT_EQ(got["one"].get(), dap::integer(1)); + ASSERT_EQ(got["two"].get(), dap::integer(2)); + ASSERT_EQ(got["three"].get(), dap::integer(3)); + } +} + +TEST(Any, TestObject) { + dap::any any(dap::AnyTestObject{5, 3.0}); + ASSERT_TRUE(any.is()); + ASSERT_EQ(any.get().i, 5); + ASSERT_EQ(any.get().n, 3.0); +} + +template +class AnyT : public ::testing::Test { + protected: + template ::value && + !std::is_same::value>> + void check_val(const dap::any& any, const T0& expect) { + ASSERT_EQ(any.is(), any.is()); + ASSERT_EQ(any.get(), expect); + } + + // Special case for Null assignment, as we can assign nullptr_t to any but + // can't `get()` it + template + void check_val(const dap::any& any, const dap::null& expect) { + ASSERT_EQ(nullptr, expect); + ASSERT_TRUE(any.is()); + } + + void check_type(const dap::any& any) { + ASSERT_EQ(any.is(), (std::is_same::value)); + ASSERT_EQ(any.is(), (std::is_same::value)); + ASSERT_EQ(any.is(), (std::is_same::value)); + ASSERT_EQ(any.is(), (std::is_same::value)); + ASSERT_EQ(any.is(), (std::is_same::value)); + ASSERT_EQ(any.is>(), + (std::is_same>::value)); + ASSERT_EQ(any.is(), + (std::is_same::value)); + } +}; +TYPED_TEST_SUITE_P(AnyT); + +TYPED_TEST_P(AnyT, CopyConstruct) { + auto val = TestValue::value; + dap::any any(val); + this->check_type(any); + this->check_val(any, val); +} + +TYPED_TEST_P(AnyT, MoveConstruct) { + auto val = TestValue::value; + dap::any any(std::move(val)); + this->check_type(any); + this->check_val(any, val); +} + +TYPED_TEST_P(AnyT, Assign) { + auto val = TestValue::value; + dap::any any; + any = val; + this->check_type(any); + this->check_val(any, val); +} + +TYPED_TEST_P(AnyT, MoveAssign) { + auto val = TestValue::value; + dap::any any; + any = std::move(val); + this->check_type(any); + this->check_val(any, val); +} + +TYPED_TEST_P(AnyT, RepeatedAssign) { + dap::string str = "hello world"; + auto val = TestValue::value; + dap::any any; + any = str; + any = val; + this->check_type(any); + this->check_val(any, val); +} + +TYPED_TEST_P(AnyT, RepeatedMoveAssign) { + dap::string str = "hello world"; + auto val = TestValue::value; + dap::any any; + any = std::move(str); + any = std::move(val); + this->check_type(any); + this->check_val(any, val); +} + +REGISTER_TYPED_TEST_SUITE_P(AnyT, + CopyConstruct, + MoveConstruct, + Assign, + MoveAssign, + RepeatedAssign, + RepeatedMoveAssign); + +using AnyTypes = ::testing::Types, + dap::AnyTestObject>; +INSTANTIATE_TYPED_TEST_SUITE_P(T, AnyT, AnyTypes); + +TEST(Any, Reset) { + dap::any any(dap::integer(10)); + ASSERT_TRUE(any.is()); + any.reset(); + ASSERT_FALSE(any.is()); +} diff --git a/libraries/cppdap/src/chan.h b/libraries/cppdap/src/chan.h new file mode 100644 index 000000000..f2345e968 --- /dev/null +++ b/libraries/cppdap/src/chan.h @@ -0,0 +1,90 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_chan_h +#define dap_chan_h + +#include "dap/optional.h" + +#include +#include +#include + +namespace dap { + +template +struct Chan { + public: + void reset(); + void close(); + optional take(); + void put(T&& in); + void put(const T& in); + + private: + bool closed = false; + std::queue queue; + std::condition_variable cv; + std::mutex mutex; +}; + +template +void Chan::reset() { + std::unique_lock lock(mutex); + queue = {}; + closed = false; +} + +template +void Chan::close() { + std::unique_lock lock(mutex); + closed = true; + cv.notify_all(); +} + +template +optional Chan::take() { + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return queue.size() > 0 || closed; }); + if (queue.size() == 0) { + return optional(); + } + auto out = std::move(queue.front()); + queue.pop(); + return optional(std::move(out)); +} + +template +void Chan::put(T&& in) { + std::unique_lock lock(mutex); + auto notify = queue.size() == 0 && !closed; + queue.push(std::move(in)); + if (notify) { + cv.notify_all(); + } +} + +template +void Chan::put(const T& in) { + std::unique_lock lock(mutex); + auto notify = queue.size() == 0 && !closed; + queue.push(in); + if (notify) { + cv.notify_all(); + } +} + +} // namespace dap + +#endif // dap_chan_h diff --git a/libraries/cppdap/src/chan_test.cpp b/libraries/cppdap/src/chan_test.cpp new file mode 100644 index 000000000..4d7e0a43a --- /dev/null +++ b/libraries/cppdap/src/chan_test.cpp @@ -0,0 +1,35 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "chan.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include + +TEST(ChanTest, PutTakeClose) { + dap::Chan chan; + auto thread = std::thread([&] { + chan.put(10); + chan.put(20); + chan.put(30); + chan.close(); + }); + EXPECT_EQ(chan.take(), dap::optional(10)); + EXPECT_EQ(chan.take(), dap::optional(20)); + EXPECT_EQ(chan.take(), dap::optional(30)); + EXPECT_EQ(chan.take(), dap::optional()); + thread.join(); +} diff --git a/libraries/cppdap/src/content_stream.cpp b/libraries/cppdap/src/content_stream.cpp new file mode 100644 index 000000000..c5264aad1 --- /dev/null +++ b/libraries/cppdap/src/content_stream.cpp @@ -0,0 +1,198 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "content_stream.h" + +#include "dap/io.h" + +#include // strlen +#include // std::min + +namespace dap { + +//////////////////////////////////////////////////////////////////////////////// +// ContentReader +//////////////////////////////////////////////////////////////////////////////// +ContentReader::ContentReader( + const std::shared_ptr& reader, + OnInvalidData on_invalid_data /* = OnInvalidData::kIgnore */) + : reader(reader), on_invalid_data(on_invalid_data) {} + +ContentReader& ContentReader::operator=(ContentReader&& rhs) noexcept { + buf = std::move(rhs.buf); + reader = std::move(rhs.reader); + on_invalid_data = std::move(rhs.on_invalid_data); + return *this; +} + +bool ContentReader::isOpen() { + return reader ? reader->isOpen() : false; +} + +void ContentReader::close() { + if (reader) { + reader->close(); + } +} + +std::string ContentReader::read() { + // Find Content-Length header prefix + if (on_invalid_data == kClose) { + if (!match("Content-Length:")) { + return badHeader(); + } + } else { + if (!scan("Content-Length:")) { + return ""; + } + } + // Skip whitespace and tabs + while (matchAny(" \t")) { + } + // Parse length + size_t len = 0; + while (true) { + auto c = matchAny("0123456789"); + if (c == 0) { + break; + } + len *= 10; + len += size_t(c) - size_t('0'); + } + if (len == 0) { + return ""; + } + + // Expect \r\n\r\n + if (!match("\r\n\r\n")) { + return badHeader(); + } + + // Read message + if (!buffer(len)) { + return ""; + } + std::string out; + out.reserve(len); + for (size_t i = 0; i < len; i++) { + out.push_back(static_cast(buf.front())); + buf.pop_front(); + } + return out; +} + +bool ContentReader::scan(const uint8_t* seq, size_t len) { + while (buffer(len)) { + if (match(seq, len)) { + return true; + } + buf.pop_front(); + } + return false; +} + +bool ContentReader::scan(const char* str) { + auto len = strlen(str); + return scan(reinterpret_cast(str), len); +} + +bool ContentReader::match(const uint8_t* seq, size_t len) { + if (!buffer(len)) { + return false; + } + auto it = buf.begin(); + for (size_t i = 0; i < len; i++, it++) { + if (*it != seq[i]) { + return false; + } + } + for (size_t i = 0; i < len; i++) { + buf.pop_front(); + } + return true; +} + +bool ContentReader::match(const char* str) { + auto len = strlen(str); + return match(reinterpret_cast(str), len); +} + +char ContentReader::matchAny(const char* chars) { + if (!buffer(1)) { + return false; + } + int c = buf.front(); + if (auto p = strchr(chars, c)) { + buf.pop_front(); + return *p; + } + return 0; +} + +bool ContentReader::buffer(size_t bytes) { + if (bytes < buf.size()) { + return true; + } + bytes -= buf.size(); + while (bytes > 0) { + uint8_t chunk[256]; + auto numWant = std::min(sizeof(chunk), bytes); + auto numGot = reader->read(chunk, numWant); + if (numGot == 0) { + return false; + } + for (size_t i = 0; i < numGot; i++) { + buf.push_back(chunk[i]); + } + bytes -= numGot; + } + return true; +} + +std::string ContentReader::badHeader() { + if (on_invalid_data == kClose) { + close(); + } + return ""; +} + +//////////////////////////////////////////////////////////////////////////////// +// ContentWriter +//////////////////////////////////////////////////////////////////////////////// +ContentWriter::ContentWriter(const std::shared_ptr& rhs) + : writer(rhs) {} + +ContentWriter& ContentWriter::operator=(ContentWriter&& rhs) noexcept { + writer = std::move(rhs.writer); + return *this; +} + +bool ContentWriter::isOpen() { + return writer ? writer->isOpen() : false; +} + +void ContentWriter::close() { + if (writer) { + writer->close(); + } +} + +bool ContentWriter::write(const std::string& msg) const { + auto header = + std::string("Content-Length: ") + std::to_string(msg.size()) + "\r\n\r\n"; + return writer->write(header.data(), header.size()) && + writer->write(msg.data(), msg.size()); +} + +} // namespace dap \ No newline at end of file diff --git a/libraries/cppdap/src/content_stream.h b/libraries/cppdap/src/content_stream.h new file mode 100644 index 000000000..eee998ed9 --- /dev/null +++ b/libraries/cppdap/src/content_stream.h @@ -0,0 +1,73 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_content_stream_h +#define dap_content_stream_h + +#include +#include +#include + +#include + +#include "dap/session.h" + +namespace dap { + +// Forward declarations +class Reader; +class Writer; + +class ContentReader { + public: + ContentReader() = default; + ContentReader(const std::shared_ptr&, + const OnInvalidData on_invalid_data = kIgnore); + ContentReader& operator=(ContentReader&&) noexcept; + + bool isOpen(); + void close(); + std::string read(); + + private: + bool scan(const uint8_t* seq, size_t len); + bool scan(const char* str); + bool match(const uint8_t* seq, size_t len); + bool match(const char* str); + char matchAny(const char* chars); + bool buffer(size_t bytes); + std::string badHeader(); + + std::shared_ptr reader; + std::deque buf; + OnInvalidData on_invalid_data; +}; + +class ContentWriter { + public: + ContentWriter() = default; + ContentWriter(const std::shared_ptr&); + ContentWriter& operator=(ContentWriter&&) noexcept; + + bool isOpen(); + void close(); + bool write(const std::string&) const; + + private: + std::shared_ptr writer; +}; + +} // namespace dap + +#endif // dap_content_stream_h diff --git a/libraries/cppdap/src/content_stream_test.cpp b/libraries/cppdap/src/content_stream_test.cpp new file mode 100644 index 000000000..3f0047230 --- /dev/null +++ b/libraries/cppdap/src/content_stream_test.cpp @@ -0,0 +1,126 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "content_stream.h" + +#include "string_buffer.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include + +namespace { + +// SingleByteReader wraps a dap::Reader to only provide a single byte for each +// read() call, regardless of the number of bytes actually requested. +class SingleByteReader : public dap::Reader { + public: + SingleByteReader(std::unique_ptr&& inner) + : inner(std::move(inner)) {} + + bool isOpen() override { return inner->isOpen(); } + void close() override { return inner->close(); } + size_t read(void* buffer, size_t) override { return inner->read(buffer, 1); }; + + private: + std::unique_ptr inner; +}; + +} // namespace + +TEST(ContentStreamTest, Write) { + auto sb = dap::StringBuffer::create(); + auto ptr = sb.get(); + dap::ContentWriter cw(std::move(sb)); + cw.write("Content payload number one"); + cw.write("Content payload number two"); + cw.write("Content payload number three"); + ASSERT_EQ(ptr->string(), + "Content-Length: 26\r\n\r\nContent payload number one" + "Content-Length: 26\r\n\r\nContent payload number two" + "Content-Length: 28\r\n\r\nContent payload number three"); +} + +TEST(ContentStreamTest, Read) { + auto sb = dap::StringBuffer::create(); + sb->write("Content-Length: 26\r\n\r\nContent payload number one"); + sb->write("some unrecognised garbage"); + sb->write("Content-Length: 26\r\n\r\nContent payload number two"); + sb->write("some more unrecognised garbage"); + sb->write("Content-Length: 28\r\n\r\nContent payload number three"); + dap::ContentReader cs(std::move(sb)); + ASSERT_EQ(cs.read(), "Content payload number one"); + ASSERT_EQ(cs.read(), "Content payload number two"); + ASSERT_EQ(cs.read(), "Content payload number three"); + ASSERT_EQ(cs.read(), ""); +} + +TEST(ContentStreamTest, ShortRead) { + auto sb = dap::StringBuffer::create(); + sb->write("Content-Length: 26\r\n\r\nContent payload number one"); + sb->write("some unrecognised garbage"); + sb->write("Content-Length: 26\r\n\r\nContent payload number two"); + sb->write("some more unrecognised garbage"); + sb->write("Content-Length: 28\r\n\r\nContent payload number three"); + dap::ContentReader cs( + std::unique_ptr(new SingleByteReader(std::move(sb)))); + ASSERT_EQ(cs.read(), "Content payload number one"); + ASSERT_EQ(cs.read(), "Content payload number two"); + ASSERT_EQ(cs.read(), "Content payload number three"); + ASSERT_EQ(cs.read(), ""); +} + +TEST(ContentStreamTest, PartialReadAndParse) { + auto sb = std::make_shared(); + sb->write("Content"); + sb->write("-Length: "); + sb->write("26"); + sb->write("\r\n\r\n"); + sb->write("Content payload number one"); + + dap::ContentReader cs(sb); + ASSERT_EQ(cs.read(), "Content payload number one"); + ASSERT_EQ(cs.read(), ""); +} + +TEST(ContentStreamTest, HttpRequest) { + const char* const part1 = + "POST / HTTP/1.1\r\n" + "Host: localhost:8001\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 99\r\n"; + const char* const part2 = + "Pragma: no-cache\r\n" + "Cache-Control: no-cache\r\n" + "Content-Type: text/plain;charset=UTF-8\r\n" + "Accept: */*\r\n" + "Origin: null\r\n" + "Sec-Fetch-Site: cross-site\r\n" + "Sec-Fetch-Mode: cors\r\n" + "Sec-Fetch-Dest: empty\r\n" + "Accept-Encoding: gzip, deflate, br\r\n" + "Accept-Language: en-US,en;q=0.9\r\n" + "\r\n" + "{\"type\":\"request\",\"command\":\"launch\",\"arguments\":{\"cmd\":\"/" + "bin/sh -c 'echo remote code execution'\"}}"; + + auto sb = dap::StringBuffer::create(); + sb->write(part1); + sb->write(part2); + + dap::ContentReader cr(std::move(sb), dap::kClose); + ASSERT_EQ(cr.read(), ""); + ASSERT_FALSE(cr.isOpen()); +} diff --git a/libraries/cppdap/src/dap_test.cpp b/libraries/cppdap/src/dap_test.cpp new file mode 100644 index 000000000..f31be464e --- /dev/null +++ b/libraries/cppdap/src/dap_test.cpp @@ -0,0 +1,72 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/dap.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +TEST(DAP, PairedInitializeTerminate) { + dap::initialize(); + dap::terminate(); +} + +TEST(DAP, NestedInitializeTerminate) { + dap::initialize(); + dap::initialize(); + dap::initialize(); + dap::terminate(); + dap::terminate(); + dap::terminate(); +} + +TEST(DAP, MultiThreadedInitializeTerminate) { + const size_t numThreads = 64; + + std::mutex mutex; + std::condition_variable cv; + size_t numInits = 0; + + std::vector threads; + threads.reserve(numThreads); + for (size_t i = 0; i < numThreads; i++) { + threads.emplace_back([&] { + dap::initialize(); + { + std::unique_lock lock(mutex); + numInits++; + if (numInits == numThreads) { + cv.notify_all(); + } else { + cv.wait(lock, [&] { return numInits == numThreads; }); + } + } + dap::terminate(); + }); + } + + for (auto& thread : threads) { + thread.join(); + } +} diff --git a/libraries/cppdap/src/io.cpp b/libraries/cppdap/src/io.cpp new file mode 100644 index 000000000..b4133e50b --- /dev/null +++ b/libraries/cppdap/src/io.cpp @@ -0,0 +1,258 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/io.h" + +#include +#include +#include +#include +#include // strlen +#include +#include +#include + +namespace { + +class Pipe : public dap::ReaderWriter { + public: + // dap::ReaderWriter compliance + bool isOpen() override { + std::unique_lock lock(mutex); + return !closed; + } + + void close() override { + std::unique_lock lock(mutex); + closed = true; + cv.notify_all(); + } + + size_t read(void* buffer, size_t bytes) override { + std::unique_lock lock(mutex); + auto out = reinterpret_cast(buffer); + size_t n = 0; + while (true) { + cv.wait(lock, [&] { return closed || data.size() > 0; }); + if (closed) { + return n; + } + for (; n < bytes && data.size() > 0; n++) { + out[n] = data.front(); + data.pop_front(); + } + if (n == bytes) { + return n; + } + } + } + + bool write(const void* buffer, size_t bytes) override { + std::unique_lock lock(mutex); + if (closed) { + return false; + } + if (bytes == 0) { + return true; + } + auto notify = data.size() == 0; + auto src = reinterpret_cast(buffer); + for (size_t i = 0; i < bytes; i++) { + data.emplace_back(src[i]); + } + if (notify) { + cv.notify_all(); + } + return true; + } + + private: + std::mutex mutex; + std::condition_variable cv; + std::deque data; + bool closed = false; +}; + +class RW : public dap::ReaderWriter { + public: + RW(const std::shared_ptr& r, const std::shared_ptr& w) + : r(r), w(w) {} + + // dap::ReaderWriter compliance + bool isOpen() override { return r->isOpen() && w->isOpen(); } + void close() override { + r->close(); + w->close(); + } + size_t read(void* buffer, size_t n) override { return r->read(buffer, n); } + bool write(const void* buffer, size_t n) override { + return w->write(buffer, n); + } + + private: + const std::shared_ptr r; + const std::shared_ptr w; +}; + +class File : public dap::ReaderWriter { + public: + File(FILE* f, bool closable) : f(f), closable(closable) {} + + ~File() { close(); } + + // dap::ReaderWriter compliance + bool isOpen() override { return !closed; } + void close() override { + if (closable) { + if (!closed.exchange(true)) { + fclose(f); + } + } + } + size_t read(void* buffer, size_t n) override { + std::unique_lock lock(readMutex); + auto out = reinterpret_cast(buffer); + for (size_t i = 0; i < n; i++) { + int c = fgetc(f); + if (c == EOF) { + return i; + } + out[i] = char(c); + } + return n; + } + bool write(const void* buffer, size_t n) override { + std::unique_lock lock(writeMutex); + if (fwrite(buffer, 1, n, f) == n) { + fflush(f); + return true; + } + return false; + } + + private: + FILE* const f; + const bool closable; + std::mutex readMutex; + std::mutex writeMutex; + std::atomic closed = {false}; +}; + +class ReaderSpy : public dap::Reader { + public: + ReaderSpy(const std::shared_ptr& r, + const std::shared_ptr& s, + const std::string& prefix) + : r(r), s(s), prefix(prefix) {} + + // dap::Reader compliance + bool isOpen() override { return r->isOpen(); } + void close() override { r->close(); } + size_t read(void* buffer, size_t n) override { + auto c = r->read(buffer, n); + if (c > 0) { + auto chars = reinterpret_cast(buffer); + std::string buf = prefix; + buf.append(chars, chars + c); + s->write(buf.data(), buf.size()); + } + return c; + } + + private: + const std::shared_ptr r; + const std::shared_ptr s; + const std::string prefix; +}; + +class WriterSpy : public dap::Writer { + public: + WriterSpy(const std::shared_ptr& w, + const std::shared_ptr& s, + const std::string& prefix) + : w(w), s(s), prefix(prefix) {} + + // dap::Writer compliance + bool isOpen() override { return w->isOpen(); } + void close() override { w->close(); } + bool write(const void* buffer, size_t n) override { + if (!w->write(buffer, n)) { + return false; + } + auto chars = reinterpret_cast(buffer); + std::string buf = prefix; + buf.append(chars, chars + n); + s->write(buf.data(), buf.size()); + return true; + } + + private: + const std::shared_ptr w; + const std::shared_ptr s; + const std::string prefix; +}; + +} // anonymous namespace + +namespace dap { + +std::shared_ptr ReaderWriter::create( + const std::shared_ptr& r, + const std::shared_ptr& w) { + return std::make_shared(r, w); +} + +std::shared_ptr pipe() { + return std::make_shared(); +} + +std::shared_ptr file(FILE* f, bool closable /* = true */) { + return std::make_shared(f, closable); +} + +std::shared_ptr file(const char* path) { + if (auto f = fopen(path, "wb")) { + return std::make_shared(f, true); + } + return nullptr; +} + +// spy() returns a Reader that copies all reads from the Reader r to the Writer +// s, using the given optional prefix. +std::shared_ptr spy(const std::shared_ptr& r, + const std::shared_ptr& s, + const char* prefix /* = "\n<-" */) { + return std::make_shared(r, s, prefix); +} + +// spy() returns a Writer that copies all writes to the Writer w to the Writer +// s, using the given optional prefix. +std::shared_ptr spy(const std::shared_ptr& w, + const std::shared_ptr& s, + const char* prefix /* = "\n->" */) { + return std::make_shared(w, s, prefix); +} + +bool writef(const std::shared_ptr& w, const char* msg, ...) { + char buf[2048]; + + va_list vararg; + va_start(vararg, msg); + vsnprintf(buf, sizeof(buf), msg, vararg); + va_end(vararg); + + return w->write(buf, strlen(buf)); +} + +} // namespace dap diff --git a/libraries/cppdap/src/json_serializer.h b/libraries/cppdap/src/json_serializer.h new file mode 100644 index 000000000..32a7ce430 --- /dev/null +++ b/libraries/cppdap/src/json_serializer.h @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_json_serializer_h +#define dap_json_serializer_h + +#if defined(CPPDAP_JSON_NLOHMANN) +#include "nlohmann_json_serializer.h" +#elif defined(CPPDAP_JSON_RAPID) +#include "rapid_json_serializer.h" +#elif defined(CPPDAP_JSON_JSONCPP) +#include "jsoncpp_json_serializer.h" +#else +#error "Unrecognised cppdap JSON library" +#endif + +namespace dap { +namespace json { + +#if defined(CPPDAP_JSON_NLOHMANN) +using Deserializer = NlohmannDeserializer; +using Serializer = NlohmannSerializer; +#elif defined(CPPDAP_JSON_RAPID) +using Deserializer = RapidDeserializer; +using Serializer = RapidSerializer; +#elif defined(CPPDAP_JSON_JSONCPP) +using Deserializer = JsonCppDeserializer; +using Serializer = JsonCppSerializer; +#else +#error "Unrecognised cppdap JSON library" +#endif + +} // namespace json +} // namespace dap + +#endif // dap_json_serializer_h diff --git a/libraries/cppdap/src/json_serializer_test.cpp b/libraries/cppdap/src/json_serializer_test.cpp new file mode 100644 index 000000000..3416cd9e1 --- /dev/null +++ b/libraries/cppdap/src/json_serializer_test.cpp @@ -0,0 +1,266 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "json_serializer.h" + +#include "dap/typeinfo.h" +#include "dap/typeof.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace dap { + +struct JSONInnerTestObject { + integer i; +}; + +DAP_STRUCT_TYPEINFO(JSONInnerTestObject, + "json-inner-test-object", + DAP_FIELD(i, "i")); + +struct JSONTestObject { + boolean b; + integer i; + number n; + array a; + object o; + string s; + optional o1; + optional o2; + JSONInnerTestObject inner; +}; + +DAP_STRUCT_TYPEINFO(JSONTestObject, + "json-test-object", + DAP_FIELD(b, "b"), + DAP_FIELD(i, "i"), + DAP_FIELD(n, "n"), + DAP_FIELD(a, "a"), + DAP_FIELD(o, "o"), + DAP_FIELD(s, "s"), + DAP_FIELD(o1, "o1"), + DAP_FIELD(o2, "o2"), + DAP_FIELD(inner, "inner")); + +struct JSONObjectNoFields {}; + +DAP_STRUCT_TYPEINFO(JSONObjectNoFields, "json-object-no-fields"); + +struct SimpleJSONTestObject { + boolean b; + integer i; +}; +DAP_STRUCT_TYPEINFO(SimpleJSONTestObject, + "simple-json-test-object", + DAP_FIELD(b, "b"), + DAP_FIELD(i, "i")); + +} // namespace dap + +class JSONSerializer : public testing::Test { + protected: + static dap::object GetSimpleObject() { + return dap::object({{"one", dap::integer(1)}, + {"two", dap::number(2)}, + {"three", dap::string("three")}, + {"four", dap::boolean(true)}}); + } + void TEST_SIMPLE_OBJECT(const dap::object& obj) { + NESTED_TEST_FAILED = true; + auto ref_obj = GetSimpleObject(); + ASSERT_EQ(obj.size(), ref_obj.size()); + ASSERT_TRUE(obj.at("one").is()); + ASSERT_TRUE(obj.at("two").is()); + ASSERT_TRUE(obj.at("three").is()); + ASSERT_TRUE(obj.at("four").is()); + + ASSERT_EQ(ref_obj.at("one").get(), + obj.at("one").get()); + ASSERT_EQ(ref_obj.at("two").get(), + obj.at("two").get()); + ASSERT_EQ(ref_obj.at("three").get(), + obj.at("three").get()); + ASSERT_EQ(ref_obj.at("four").get(), + obj.at("four").get()); + NESTED_TEST_FAILED = false; + } + template + void TEST_SERIALIZING_DESERIALIZING(const T& encoded, T& decoded) { + NESTED_TEST_FAILED = true; + dap::json::Serializer s; + ASSERT_TRUE(s.serialize(encoded)); + dap::json::Deserializer d(s.dump()); + ASSERT_TRUE(d.deserialize(&decoded)); + NESTED_TEST_FAILED = false; + } + bool NESTED_TEST_FAILED = false; +#define _ASSERT_PASS(NESTED_TEST) \ + NESTED_TEST; \ + ASSERT_FALSE(NESTED_TEST_FAILED); +}; + +TEST_F(JSONSerializer, SerializeDeserialize) { + dap::JSONTestObject encoded; + encoded.b = true; + encoded.i = 32; + encoded.n = 123.456; + encoded.a = {2, 4, 6, 8, 0x100000000, -2, -4, -6, -8, -0x100000000}; + encoded.o["one"] = dap::integer(1); + encoded.o["two"] = dap::number(2); + encoded.s = "hello world"; + encoded.o2 = 42; + encoded.inner.i = 70; + + dap::json::Serializer s; + ASSERT_TRUE(s.serialize(encoded)); + + dap::JSONTestObject decoded; + dap::json::Deserializer d(s.dump()); + ASSERT_TRUE(d.deserialize(&decoded)); + + ASSERT_EQ(encoded.b, decoded.b); + ASSERT_EQ(encoded.i, decoded.i); + ASSERT_EQ(encoded.n, decoded.n); + ASSERT_EQ(encoded.a, decoded.a); + ASSERT_EQ(encoded.o["one"].get(), + decoded.o["one"].get()); + ASSERT_EQ(encoded.o["two"].get(), + decoded.o["two"].get()); + ASSERT_EQ(encoded.s, decoded.s); + ASSERT_EQ(encoded.o2, decoded.o2); + ASSERT_EQ(encoded.inner.i, decoded.inner.i); +} + +TEST_F(JSONSerializer, SerializeObjectNoFields) { + dap::JSONObjectNoFields obj; + dap::json::Serializer s; + ASSERT_TRUE(s.serialize(obj)); + ASSERT_EQ(s.dump(), "{}"); +} + +TEST_F(JSONSerializer, SerializeDeserializeObject) { + dap::object encoded = GetSimpleObject(); + dap::object decoded; + _ASSERT_PASS(TEST_SERIALIZING_DESERIALIZING(encoded, decoded)); + _ASSERT_PASS(TEST_SIMPLE_OBJECT(decoded)); +} + +TEST_F(JSONSerializer, SerializeDeserializeEmbeddedObject) { + dap::object encoded; + dap::object decoded; + // object nested inside object + dap::object encoded_embed_obj = GetSimpleObject(); + dap::object decoded_embed_obj; + encoded["embed_obj"] = encoded_embed_obj; + _ASSERT_PASS(TEST_SERIALIZING_DESERIALIZING(encoded, decoded)); + ASSERT_TRUE(decoded["embed_obj"].is()); + decoded_embed_obj = decoded["embed_obj"].get(); + _ASSERT_PASS(TEST_SIMPLE_OBJECT(decoded_embed_obj)); +} + +TEST_F(JSONSerializer, SerializeDeserializeEmbeddedStruct) { + dap::object encoded; + dap::object decoded; + // object nested inside object + dap::SimpleJSONTestObject encoded_embed_struct; + encoded_embed_struct.b = true; + encoded_embed_struct.i = 50; + encoded["embed_struct"] = encoded_embed_struct; + + dap::object decoded_embed_obj; + _ASSERT_PASS(TEST_SERIALIZING_DESERIALIZING(encoded, decoded)); + ASSERT_TRUE(decoded["embed_struct"].is()); + decoded_embed_obj = decoded["embed_struct"].get(); + ASSERT_TRUE(decoded_embed_obj.at("b").is()); + ASSERT_TRUE(decoded_embed_obj.at("i").is()); + + ASSERT_EQ(encoded_embed_struct.b, decoded_embed_obj["b"].get()); + ASSERT_EQ(encoded_embed_struct.i, decoded_embed_obj["i"].get()); +} + +TEST_F(JSONSerializer, SerializeDeserializeEmbeddedIntArray) { + dap::object encoded; + dap::object decoded; + // array nested inside object + dap::array encoded_embed_arr = {1, 2, 3, 4}; + dap::array decoded_embed_arr; + + encoded["embed_arr"] = encoded_embed_arr; + + _ASSERT_PASS(TEST_SERIALIZING_DESERIALIZING(encoded, decoded)); + // TODO: Deserializing array should infer basic member types + ASSERT_TRUE(decoded["embed_arr"].is>()); + decoded_embed_arr = decoded["embed_arr"].get>(); + ASSERT_EQ(encoded_embed_arr.size(), decoded_embed_arr.size()); + for (std::size_t i = 0; i < decoded_embed_arr.size(); i++) { + ASSERT_TRUE(decoded_embed_arr[i].is()); + ASSERT_EQ(encoded_embed_arr[i], decoded_embed_arr[i].get()); + } +} + +TEST_F(JSONSerializer, SerializeDeserializeEmbeddedObjectArray) { + dap::object encoded; + dap::object decoded; + + dap::array encoded_embed_arr = {GetSimpleObject(), + GetSimpleObject()}; + dap::array decoded_embed_arr; + + encoded["embed_arr"] = encoded_embed_arr; + + _ASSERT_PASS(TEST_SERIALIZING_DESERIALIZING(encoded, decoded)); + // TODO: Deserializing array should infer basic member types + ASSERT_TRUE(decoded["embed_arr"].is>()); + decoded_embed_arr = decoded["embed_arr"].get>(); + ASSERT_EQ(encoded_embed_arr.size(), decoded_embed_arr.size()); + for (std::size_t i = 0; i < decoded_embed_arr.size(); i++) { + ASSERT_TRUE(decoded_embed_arr[i].is()); + _ASSERT_PASS(TEST_SIMPLE_OBJECT(decoded_embed_arr[i].get())); + } +} + +TEST_F(JSONSerializer, DeserializeSerializeEmptyObject) { + auto empty_obj = "{}"; + dap::object decoded; + dap::json::Deserializer d(empty_obj); + ASSERT_TRUE(d.deserialize(&decoded)); + dap::json::Serializer s; + ASSERT_TRUE(s.serialize(decoded)); + ASSERT_EQ(s.dump(), empty_obj); +} + +TEST_F(JSONSerializer, SerializeDeserializeEmbeddedEmptyObject) { + dap::object encoded_empty_obj; + dap::object encoded = {{"empty_obj", encoded_empty_obj}}; + dap::object decoded; + + _ASSERT_PASS(TEST_SERIALIZING_DESERIALIZING(encoded, decoded)); + ASSERT_TRUE(decoded["empty_obj"].is()); + dap::object decoded_empty_obj = decoded["empty_obj"].get(); + ASSERT_EQ(encoded_empty_obj.size(), decoded_empty_obj.size()); +} + +TEST_F(JSONSerializer, SerializeDeserializeObjectWithNulledField) { + auto thing = dap::any(dap::null()); + dap::object encoded; + encoded["nulled_field"] = dap::null(); + dap::json::Serializer s; + ASSERT_TRUE(s.serialize(encoded)); + dap::object decoded; + auto dump = s.dump(); + dap::json::Deserializer d(dump); + ASSERT_TRUE(d.deserialize(&decoded)); + ASSERT_TRUE(encoded["nulled_field"].is()); +} diff --git a/libraries/cppdap/src/jsoncpp_json_serializer.cpp b/libraries/cppdap/src/jsoncpp_json_serializer.cpp new file mode 100644 index 000000000..954b0e5a6 --- /dev/null +++ b/libraries/cppdap/src/jsoncpp_json_serializer.cpp @@ -0,0 +1,272 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jsoncpp_json_serializer.h" + +#include "null_json_serializer.h" + +#include +#include +#include + +namespace dap { +namespace json { + +JsonCppDeserializer::JsonCppDeserializer(const std::string& str) + : json(new Json::Value(JsonCppDeserializer::parse(str))), ownsJson(true) {} + +JsonCppDeserializer::JsonCppDeserializer(const Json::Value* json) + : json(json), ownsJson(false) {} + +JsonCppDeserializer::~JsonCppDeserializer() { + if (ownsJson) { + delete json; + } +} + +bool JsonCppDeserializer::deserialize(dap::boolean* v) const { + if (!json->isBool()) { + return false; + } + *v = json->asBool(); + return true; +} + +bool JsonCppDeserializer::deserialize(dap::integer* v) const { + if (!json->isInt64()) { + return false; + } + *v = json->asInt64(); + return true; +} + +bool JsonCppDeserializer::deserialize(dap::number* v) const { + if (!json->isNumeric()) { + return false; + } + *v = json->asDouble(); + return true; +} + +bool JsonCppDeserializer::deserialize(dap::string* v) const { + if (!json->isString()) { + return false; + } + *v = json->asString(); + return true; +} + +bool JsonCppDeserializer::deserialize(dap::object* v) const { + v->reserve(json->size()); + for (auto i = json->begin(); i != json->end(); i++) { + JsonCppDeserializer d(&*i); + dap::any val; + if (!d.deserialize(&val)) { + return false; + } + (*v)[i.name()] = val; + } + return true; +} + +bool JsonCppDeserializer::deserialize(dap::any* v) const { + if (json->isBool()) { + *v = dap::boolean(json->asBool()); + } else if (json->type() == Json::ValueType::realValue) { + // json->isDouble() returns true for integers as well, so we need to + // explicitly look for the realValue type. + *v = dap::number(json->asDouble()); + } else if (json->isInt64()) { + *v = dap::integer(json->asInt64()); + } else if (json->isString()) { + *v = json->asString(); + } else if (json->isObject()) { + dap::object obj; + if (!deserialize(&obj)) { + return false; + } + *v = obj; + } else if (json->isArray()) { + dap::array arr; + if (!deserialize(&arr)) { + return false; + } + *v = arr; + } else if (json->isNull()) { + *v = null(); + } else { + return false; + } + return true; +} + +size_t JsonCppDeserializer::count() const { + return json->size(); +} + +bool JsonCppDeserializer::array( + const std::function& cb) const { + if (!json->isArray()) { + return false; + } + for (const auto& value : *json) { + JsonCppDeserializer d(&value); + if (!cb(&d)) { + return false; + } + } + return true; +} + +bool JsonCppDeserializer::field( + const std::string& name, + const std::function& cb) const { + if (!json->isObject()) { + return false; + } + auto value = json->find(name.data(), name.data() + name.size()); + if (value == nullptr) { + return cb(&NullDeserializer::instance); + } + JsonCppDeserializer d(value); + return cb(&d); +} + +Json::Value JsonCppDeserializer::parse(const std::string& text) { + Json::CharReaderBuilder builder; + auto jsonReader = std::unique_ptr(builder.newCharReader()); + Json::Value json; + std::string error; + if (!jsonReader->parse(text.data(), text.data() + text.size(), &json, + &error)) { + // cppdap expects that the JSON layer does not throw exceptions. + std::abort(); + } + return json; +} + +JsonCppSerializer::JsonCppSerializer() + : json(new Json::Value()), ownsJson(true) {} + +JsonCppSerializer::JsonCppSerializer(Json::Value* json) + : json(json), ownsJson(false) {} + +JsonCppSerializer::~JsonCppSerializer() { + if (ownsJson) { + delete json; + } +} + +std::string JsonCppSerializer::dump() const { + Json::StreamWriterBuilder writer; + return Json::writeString(writer, *json); +} + +bool JsonCppSerializer::serialize(dap::boolean v) { + *json = (bool)v; + return true; +} + +bool JsonCppSerializer::serialize(dap::integer v) { + *json = (Json::LargestInt)v; + return true; +} + +bool JsonCppSerializer::serialize(dap::number v) { + *json = (double)v; + return true; +} + +bool JsonCppSerializer::serialize(const dap::string& v) { + *json = v; + return true; +} + +bool JsonCppSerializer::serialize(const dap::object& v) { + if (!json->isObject()) { + *json = Json::Value(Json::objectValue); + } + for (auto& it : v) { + JsonCppSerializer s(&(*json)[it.first]); + if (!s.serialize(it.second)) { + return false; + } + } + return true; +} + +bool JsonCppSerializer::serialize(const dap::any& v) { + if (v.is()) { + *json = (bool)v.get(); + } else if (v.is()) { + *json = (Json::LargestInt)v.get(); + } else if (v.is()) { + *json = (double)v.get(); + } else if (v.is()) { + *json = v.get(); + } else if (v.is()) { + // reachable if dap::object nested is inside other dap::object + return serialize(v.get()); + } else if (v.is()) { + } else { + // reachable if array or custom serialized type is nested inside other + auto type = get_any_type(v); + auto value = get_any_val(v); + if (type && value) { + return type->serialize(this, value); + } + return false; + } + return true; +} + +bool JsonCppSerializer::array(size_t count, + const std::function& cb) { + *json = Json::Value(Json::arrayValue); + for (size_t i = 0; i < count; i++) { + JsonCppSerializer s(&(*json)[Json::Value::ArrayIndex(i)]); + if (!cb(&s)) { + return false; + } + } + return true; +} + +bool JsonCppSerializer::object( + const std::function& cb) { + struct FS : public FieldSerializer { + Json::Value* const json; + + FS(Json::Value* json) : json(json) {} + bool field(const std::string& name, const SerializeFunc& cb) override { + JsonCppSerializer s(&(*json)[name]); + auto res = cb(&s); + if (s.removed) { + json->removeMember(name); + } + return res; + } + }; + + *json = Json::Value(Json::objectValue); + FS fs{json}; + return cb(&fs); +} + +void JsonCppSerializer::remove() { + removed = true; +} + +} // namespace json +} // namespace dap diff --git a/libraries/cppdap/src/jsoncpp_json_serializer.h b/libraries/cppdap/src/jsoncpp_json_serializer.h new file mode 100644 index 000000000..6bdf6a451 --- /dev/null +++ b/libraries/cppdap/src/jsoncpp_json_serializer.h @@ -0,0 +1,134 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_jsoncpp_json_serializer_h +#define dap_jsoncpp_json_serializer_h + +#include "dap/protocol.h" +#include "dap/serialization.h" +#include "dap/types.h" + +#include + +namespace dap { +namespace json { + +struct JsonCppDeserializer : public dap::Deserializer { + explicit JsonCppDeserializer(const std::string&); + ~JsonCppDeserializer(); + + // dap::Deserializer compliance + bool deserialize(boolean* v) const override; + bool deserialize(integer* v) const override; + bool deserialize(number* v) const override; + bool deserialize(string* v) const override; + bool deserialize(object* v) const override; + bool deserialize(any* v) const override; + size_t count() const override; + bool array(const std::function&) const override; + bool field(const std::string& name, + const std::function&) const override; + + // Unhide base overloads + template + inline bool field(const std::string& name, T* v) { + return dap::Deserializer::field(name, v); + } + + template ::has_custom_serialization>> + inline bool deserialize(T* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::array* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::optional* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::variant* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool field(const std::string& name, T* v) const { + return dap::Deserializer::deserialize(name, v); + } + + private: + JsonCppDeserializer(const Json::Value*); + static Json::Value parse(const std::string& text); + const Json::Value* const json; + const bool ownsJson; +}; + +struct JsonCppSerializer : public dap::Serializer { + JsonCppSerializer(); + ~JsonCppSerializer(); + + std::string dump() const; + + // dap::Serializer compliance + bool serialize(boolean v) override; + bool serialize(integer v) override; + bool serialize(number v) override; + bool serialize(const string& v) override; + bool serialize(const dap::object& v) override; + bool serialize(const any& v) override; + bool array(size_t count, + const std::function&) override; + bool object(const std::function&) override; + void remove() override; + + // Unhide base overloads + template ::has_custom_serialization>> + inline bool serialize(const T& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::array& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::optional& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::variant& v) { + return dap::Serializer::serialize(v); + } + + inline bool serialize(const char* v) { return dap::Serializer::serialize(v); } + + private: + JsonCppSerializer(Json::Value*); + Json::Value* const json; + const bool ownsJson; + bool removed = false; +}; + +} // namespace json +} // namespace dap + +#endif // dap_jsoncpp_json_serializer_h diff --git a/libraries/cppdap/src/network.cpp b/libraries/cppdap/src/network.cpp new file mode 100644 index 000000000..7d1da7a1c --- /dev/null +++ b/libraries/cppdap/src/network.cpp @@ -0,0 +1,107 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/network.h" + +#include "socket.h" + +#include +#include +#include +#include + +namespace { + +class Impl : public dap::net::Server { + public: + Impl() : stopped{true} {} + + ~Impl() { stop(); } + + bool start(int port, + const OnConnect& onConnect, + const OnError& onError) override { + return start("localhost", port, onConnect, onError); + } + + bool start(const char* address, + int port, + const OnConnect& onConnect, + const OnError& onError) override { + std::unique_lock lock(mutex); + stopWithLock(); + socket = std::unique_ptr( + new dap::Socket(address, std::to_string(port).c_str())); + + if (!socket->isOpen()) { + onError("Failed to open socket"); + return false; + } + + stopped = false; + thread = std::thread([=] { + while (true) { + if (auto rw = socket->accept()) { + onConnect(rw); + continue; + } + if (!stopped) { + onError("Failed to accept connection"); + } + break; + }; + }); + + return true; + } + + void stop() override { + std::unique_lock lock(mutex); + stopWithLock(); + } + + private: + bool isRunning() { return !stopped; } + + void stopWithLock() { + if (!stopped.exchange(true)) { + socket->close(); + thread.join(); + } + } + + std::mutex mutex; + std::thread thread; + std::unique_ptr socket; + std::atomic stopped; + OnError errorHandler; +}; + +} // anonymous namespace + +namespace dap { +namespace net { + +std::unique_ptr Server::create() { + return std::unique_ptr(new Impl()); +} + +std::shared_ptr connect(const char* addr, + int port, + uint32_t timeoutMillis) { + return Socket::connect(addr, std::to_string(port).c_str(), timeoutMillis); +} + +} // namespace net +} // namespace dap diff --git a/libraries/cppdap/src/network_test.cpp b/libraries/cppdap/src/network_test.cpp new file mode 100644 index 000000000..57bb0a903 --- /dev/null +++ b/libraries/cppdap/src/network_test.cpp @@ -0,0 +1,110 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/network.h" +#include "dap/io.h" + +#include "chan.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include + +namespace { + +constexpr int port = 19021; + +bool write(const std::shared_ptr& w, const std::string& s) { + return w->write(s.data(), s.size()) && w->write("\0", 1); +} + +std::string read(const std::shared_ptr& r) { + char c; + std::string s; + while (r->read(&c, sizeof(c)) > 0) { + if (c == '\0') { + return s; + } + s += c; + } + return r->isOpen() ? "" : ""; +} + +} // anonymous namespace + +TEST(Network, ClientServer) { + dap::Chan done; + auto server = dap::net::Server::create(); + if (!server->start( + port, + [&](const std::shared_ptr& rw) { + ASSERT_EQ(read(rw), "client to server"); + ASSERT_TRUE(write(rw, "server to client")); + done.put(true); + }, + [&](const char* err) { FAIL() << "Server error: " << err; })) { + FAIL() << "Couldn't start server"; + return; + } + + for (int i = 0; i < 5; i++) { + auto client = dap::net::connect("localhost", port); + ASSERT_NE(client, nullptr) << "Failed to connect client " << i; + ASSERT_TRUE(write(client, "client to server")); + ASSERT_EQ(read(client), "server to client"); + done.take(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + server.reset(); +} + +TEST(Network, ServerRepeatStopAndRestart) { + dap::Chan done; + auto onConnect = [&](const std::shared_ptr& rw) { + ASSERT_EQ(read(rw), "client to server"); + ASSERT_TRUE(write(rw, "server to client")); + done.put(true); + }; + auto onError = [&](const char* err) { FAIL() << "Server error: " << err; }; + + auto server = dap::net::Server::create(); + if (!server->start(port, onConnect, onError)) { + FAIL() << "Couldn't start server"; + return; + } + + server->stop(); + server->stop(); + server->stop(); + + if (!server->start(port, onConnect, onError)) { + FAIL() << "Couldn't restart server"; + return; + } + + auto client = dap::net::connect("localhost", port); + ASSERT_NE(client, nullptr) << "Failed to connect"; + ASSERT_TRUE(write(client, "client to server")); + ASSERT_EQ(read(client), "server to client"); + done.take(); + + server->stop(); + server->stop(); + server->stop(); + + server.reset(); +} diff --git a/libraries/cppdap/src/nlohmann_json_serializer.cpp b/libraries/cppdap/src/nlohmann_json_serializer.cpp new file mode 100644 index 000000000..783423032 --- /dev/null +++ b/libraries/cppdap/src/nlohmann_json_serializer.cpp @@ -0,0 +1,260 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "nlohmann_json_serializer.h" + +#include "null_json_serializer.h" + +// Disable JSON exceptions. We should be guarding against any exceptions being +// fired in this file. +#define JSON_NOEXCEPTION 1 +#include + +namespace dap { +namespace json { + +NlohmannDeserializer::NlohmannDeserializer(const std::string& str) + : json(new nlohmann::json(nlohmann::json::parse(str, nullptr, false))), + ownsJson(true) {} + +NlohmannDeserializer::NlohmannDeserializer(const nlohmann::json* json) + : json(json), ownsJson(false) {} + +NlohmannDeserializer::~NlohmannDeserializer() { + if (ownsJson) { + delete json; + } +} + +bool NlohmannDeserializer::deserialize(dap::boolean* v) const { + if (!json->is_boolean()) { + return false; + } + *v = json->get(); + return true; +} + +bool NlohmannDeserializer::deserialize(dap::integer* v) const { + if (!json->is_number_integer()) { + return false; + } + *v = json->get(); + return true; +} + +bool NlohmannDeserializer::deserialize(dap::number* v) const { + if (!json->is_number()) { + return false; + } + *v = json->get(); + return true; +} + +bool NlohmannDeserializer::deserialize(dap::string* v) const { + if (!json->is_string()) { + return false; + } + *v = json->get(); + return true; +} + +bool NlohmannDeserializer::deserialize(dap::object* v) const { + v->reserve(json->size()); + for (auto& el : json->items()) { + NlohmannDeserializer d(&el.value()); + dap::any val; + if (!d.deserialize(&val)) { + return false; + } + (*v)[el.key()] = val; + } + return true; +} + +bool NlohmannDeserializer::deserialize(dap::any* v) const { + if (json->is_boolean()) { + *v = dap::boolean(json->get()); + } else if (json->is_number_float()) { + *v = dap::number(json->get()); + } else if (json->is_number_integer()) { + *v = dap::integer(json->get()); + } else if (json->is_string()) { + *v = json->get(); + } else if (json->is_object()) { + dap::object obj; + if (!deserialize(&obj)) { + return false; + } + *v = obj; + } else if (json->is_array()) { + dap::array arr; + if (!deserialize(&arr)) { + return false; + } + *v = arr; + } else if (json->is_null()) { + *v = null(); + } else { + return false; + } + return true; +} + +size_t NlohmannDeserializer::count() const { + return json->size(); +} + +bool NlohmannDeserializer::array( + const std::function& cb) const { + if (!json->is_array()) { + return false; + } + for (size_t i = 0; i < json->size(); i++) { + NlohmannDeserializer d(&(*json)[i]); + if (!cb(&d)) { + return false; + } + } + return true; +} + +bool NlohmannDeserializer::field( + const std::string& name, + const std::function& cb) const { + if (!json->is_structured()) { + return false; + } + auto it = json->find(name); + if (it == json->end()) { + return cb(&NullDeserializer::instance); + } + auto obj = *it; + NlohmannDeserializer d(&obj); + return cb(&d); +} + +NlohmannSerializer::NlohmannSerializer() + : json(new nlohmann::json()), ownsJson(true) {} + +NlohmannSerializer::NlohmannSerializer(nlohmann::json* json) + : json(json), ownsJson(false) {} + +NlohmannSerializer::~NlohmannSerializer() { + if (ownsJson) { + delete json; + } +} + +std::string NlohmannSerializer::dump() const { + return json->dump(); +} + +bool NlohmannSerializer::serialize(dap::boolean v) { + *json = (bool)v; + return true; +} + +bool NlohmannSerializer::serialize(dap::integer v) { + *json = (int64_t)v; + return true; +} + +bool NlohmannSerializer::serialize(dap::number v) { + *json = (double)v; + return true; +} + +bool NlohmannSerializer::serialize(const dap::string& v) { + *json = v; + return true; +} + +bool NlohmannSerializer::serialize(const dap::object& v) { + if (!json->is_object()) { + *json = nlohmann::json::object(); + } + for (auto& it : v) { + NlohmannSerializer s(&(*json)[it.first]); + if (!s.serialize(it.second)) { + return false; + } + } + return true; +} + +bool NlohmannSerializer::serialize(const dap::any& v) { + if (v.is()) { + *json = (bool)v.get(); + } else if (v.is()) { + *json = (int64_t)v.get(); + } else if (v.is()) { + *json = (double)v.get(); + } else if (v.is()) { + *json = v.get(); + } else if (v.is()) { + // reachable if dap::object nested is inside other dap::object + return serialize(v.get()); + } else if (v.is()) { + } else { + // reachable if array or custom serialized type is nested inside other + auto type = get_any_type(v); + auto value = get_any_val(v); + if (type && value) { + return type->serialize(this, value); + } + return false; + } + return true; +} + +bool NlohmannSerializer::array( + size_t count, + const std::function& cb) { + *json = std::vector(); + for (size_t i = 0; i < count; i++) { + NlohmannSerializer s(&(*json)[i]); + if (!cb(&s)) { + return false; + } + } + return true; +} + +bool NlohmannSerializer::object( + const std::function& cb) { + struct FS : public FieldSerializer { + nlohmann::json* const json; + + FS(nlohmann::json* json) : json(json) {} + bool field(const std::string& name, const SerializeFunc& cb) override { + NlohmannSerializer s(&(*json)[name]); + auto res = cb(&s); + if (s.removed) { + json->erase(name); + } + return res; + } + }; + + *json = nlohmann::json({}, false, nlohmann::json::value_t::object); + FS fs{json}; + return cb(&fs); +} + +void NlohmannSerializer::remove() { + removed = true; +} + +} // namespace json +} // namespace dap diff --git a/libraries/cppdap/src/nlohmann_json_serializer.h b/libraries/cppdap/src/nlohmann_json_serializer.h new file mode 100644 index 000000000..f75fe78bd --- /dev/null +++ b/libraries/cppdap/src/nlohmann_json_serializer.h @@ -0,0 +1,133 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_nlohmann_json_serializer_h +#define dap_nlohmann_json_serializer_h + +#include "dap/protocol.h" +#include "dap/serialization.h" +#include "dap/types.h" + +#include + +namespace dap { +namespace json { + +struct NlohmannDeserializer : public dap::Deserializer { + explicit NlohmannDeserializer(const std::string&); + ~NlohmannDeserializer(); + + // dap::Deserializer compliance + bool deserialize(boolean* v) const override; + bool deserialize(integer* v) const override; + bool deserialize(number* v) const override; + bool deserialize(string* v) const override; + bool deserialize(object* v) const override; + bool deserialize(any* v) const override; + size_t count() const override; + bool array(const std::function&) const override; + bool field(const std::string& name, + const std::function&) const override; + + // Unhide base overloads + template + inline bool field(const std::string& name, T* v) { + return dap::Deserializer::field(name, v); + } + + template ::has_custom_serialization>> + inline bool deserialize(T* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::array* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::optional* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::variant* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool field(const std::string& name, T* v) const { + return dap::Deserializer::deserialize(name, v); + } + + private: + NlohmannDeserializer(const nlohmann::json*); + const nlohmann::json* const json; + const bool ownsJson; +}; + +struct NlohmannSerializer : public dap::Serializer { + NlohmannSerializer(); + ~NlohmannSerializer(); + + std::string dump() const; + + // dap::Serializer compliance + bool serialize(boolean v) override; + bool serialize(integer v) override; + bool serialize(number v) override; + bool serialize(const string& v) override; + bool serialize(const dap::object& v) override; + bool serialize(const any& v) override; + bool array(size_t count, + const std::function&) override; + bool object(const std::function&) override; + void remove() override; + + // Unhide base overloads + template ::has_custom_serialization>> + inline bool serialize(const T& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::array& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::optional& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::variant& v) { + return dap::Serializer::serialize(v); + } + + inline bool serialize(const char* v) { return dap::Serializer::serialize(v); } + + private: + NlohmannSerializer(nlohmann::json*); + nlohmann::json* const json; + const bool ownsJson; + bool removed = false; +}; + +} // namespace json +} // namespace dap + +#endif // dap_nlohmann_json_serializer_h \ No newline at end of file diff --git a/libraries/cppdap/src/null_json_serializer.cpp b/libraries/cppdap/src/null_json_serializer.cpp new file mode 100644 index 000000000..5aa5a03aa --- /dev/null +++ b/libraries/cppdap/src/null_json_serializer.cpp @@ -0,0 +1,23 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "null_json_serializer.h" + +namespace dap { +namespace json { + +NullDeserializer NullDeserializer::instance; + +} // namespace json +} // namespace dap diff --git a/libraries/cppdap/src/null_json_serializer.h b/libraries/cppdap/src/null_json_serializer.h new file mode 100644 index 000000000..c92b99a38 --- /dev/null +++ b/libraries/cppdap/src/null_json_serializer.h @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_null_json_serializer_h +#define dap_null_json_serializer_h + +#include "dap/protocol.h" +#include "dap/serialization.h" +#include "dap/types.h" + +namespace dap { +namespace json { + +struct NullDeserializer : public dap::Deserializer { + static NullDeserializer instance; + + bool deserialize(dap::boolean*) const override { return false; } + bool deserialize(dap::integer*) const override { return false; } + bool deserialize(dap::number*) const override { return false; } + bool deserialize(dap::string*) const override { return false; } + bool deserialize(dap::object*) const override { return false; } + bool deserialize(dap::any*) const override { return false; } + size_t count() const override { return 0; } + bool array(const std::function&) const override { + return false; + } + bool field(const std::string&, + const std::function&) const override { + return false; + } +}; + +} // namespace json +} // namespace dap + +#endif // dap_null_json_serializer_h diff --git a/libraries/cppdap/src/optional_test.cpp b/libraries/cppdap/src/optional_test.cpp new file mode 100644 index 000000000..b2590fc7a --- /dev/null +++ b/libraries/cppdap/src/optional_test.cpp @@ -0,0 +1,169 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/optional.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include + +TEST(Optional, EmptyConstruct) { + dap::optional opt; + ASSERT_FALSE(opt); + ASSERT_FALSE(opt.has_value()); +} + +TEST(Optional, ValueConstruct) { + dap::optional opt(10); + ASSERT_TRUE(opt); + ASSERT_TRUE(opt.has_value()); + ASSERT_EQ(opt.value(), 10); +} + +TEST(Optional, CopyConstruct) { + dap::optional a("meow"); + dap::optional b(a); + ASSERT_EQ(a, b); + ASSERT_EQ(a.value(), "meow"); + ASSERT_EQ(b.value(), "meow"); +} + +TEST(Optional, CopyCastConstruct) { + dap::optional a(10); + dap::optional b(a); + ASSERT_EQ(a, b); + ASSERT_EQ(b.value(), (uint16_t)10); +} + +TEST(Optional, MoveConstruct) { + dap::optional a("meow"); + dap::optional b(std::move(a)); + ASSERT_EQ(b.value(), "meow"); +} + +TEST(Optional, MoveCastConstruct) { + dap::optional a(10); + dap::optional b(std::move(a)); + ASSERT_EQ(b.value(), (uint16_t)10); +} + +TEST(Optional, AssignValue) { + dap::optional a; + std::string b = "meow"; + a = b; + ASSERT_EQ(a.value(), "meow"); + ASSERT_EQ(b, "meow"); +} + +TEST(Optional, AssignOptional) { + dap::optional a; + dap::optional b("meow"); + a = b; + ASSERT_EQ(a.value(), "meow"); + ASSERT_EQ(b.value(), "meow"); +} + +TEST(Optional, MoveAssignOptional) { + dap::optional a; + dap::optional b("meow"); + a = std::move(b); + ASSERT_EQ(a.value(), "meow"); +} + +TEST(Optional, StarDeref) { + dap::optional a("meow"); + ASSERT_EQ(*a, "meow"); +} + +TEST(Optional, StarDerefConst) { + const dap::optional a("meow"); + ASSERT_EQ(*a, "meow"); +} + +TEST(Optional, ArrowDeref) { + struct S { + int i; + }; + dap::optional a(S{10}); + ASSERT_EQ(a->i, 10); +} + +TEST(Optional, ArrowDerefConst) { + struct S { + int i; + }; + const dap::optional a(S{10}); + ASSERT_EQ(a->i, 10); +} + +TEST(Optional, Value) { + const dap::optional a("meow"); + ASSERT_EQ(a.value(), "meow"); +} + +TEST(Optional, ValueDefault) { + const dap::optional a; + const dap::optional b("woof"); + ASSERT_EQ(a.value("meow"), "meow"); + ASSERT_EQ(b.value("meow"), "woof"); +} + +TEST(Optional, CompareLT) { + ASSERT_FALSE(dap::optional(5) < dap::optional(3)); + ASSERT_FALSE(dap::optional(5) < dap::optional(5)); + ASSERT_TRUE(dap::optional(5) < dap::optional(10)); + ASSERT_TRUE(dap::optional() < dap::optional(10)); + ASSERT_FALSE(dap::optional() < dap::optional()); +} + +TEST(Optional, CompareLE) { + ASSERT_FALSE(dap::optional(5) <= dap::optional(3)); + ASSERT_TRUE(dap::optional(5) <= dap::optional(5)); + ASSERT_TRUE(dap::optional(5) <= dap::optional(10)); + ASSERT_TRUE(dap::optional() <= dap::optional(10)); + ASSERT_TRUE(dap::optional() <= dap::optional()); +} + +TEST(Optional, CompareGT) { + ASSERT_TRUE(dap::optional(5) > dap::optional(3)); + ASSERT_FALSE(dap::optional(5) > dap::optional(5)); + ASSERT_FALSE(dap::optional(5) > dap::optional(10)); + ASSERT_FALSE(dap::optional() > dap::optional(10)); + ASSERT_FALSE(dap::optional() > dap::optional()); +} + +TEST(Optional, CompareGE) { + ASSERT_TRUE(dap::optional(5) >= dap::optional(3)); + ASSERT_TRUE(dap::optional(5) >= dap::optional(5)); + ASSERT_FALSE(dap::optional(5) >= dap::optional(10)); + ASSERT_FALSE(dap::optional() >= dap::optional(10)); + ASSERT_TRUE(dap::optional() >= dap::optional()); +} + +TEST(Optional, CompareEQ) { + ASSERT_FALSE(dap::optional(5) == dap::optional(3)); + ASSERT_TRUE(dap::optional(5) == dap::optional(5)); + ASSERT_FALSE(dap::optional(5) == dap::optional(10)); + ASSERT_FALSE(dap::optional() == dap::optional(10)); + ASSERT_TRUE(dap::optional() == dap::optional()); +} + +TEST(Optional, CompareNEQ) { + ASSERT_TRUE(dap::optional(5) != dap::optional(3)); + ASSERT_FALSE(dap::optional(5) != dap::optional(5)); + ASSERT_TRUE(dap::optional(5) != dap::optional(10)); + ASSERT_TRUE(dap::optional() != dap::optional(10)); + ASSERT_FALSE(dap::optional() != dap::optional()); +} diff --git a/libraries/cppdap/src/protocol_events.cpp b/libraries/cppdap/src/protocol_events.cpp new file mode 100644 index 000000000..f97324c1a --- /dev/null +++ b/libraries/cppdap/src/protocol_events.cpp @@ -0,0 +1,127 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated with protocol_gen.go -- do not edit this file. +// go run scripts/protocol_gen/protocol_gen.go +// +// DAP version 1.68.0 + +#include "dap/protocol.h" + +namespace dap { + +DAP_IMPLEMENT_STRUCT_TYPEINFO(BreakpointEvent, + "breakpoint", + DAP_FIELD(breakpoint, "breakpoint"), + DAP_FIELD(reason, "reason")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(CapabilitiesEvent, + "capabilities", + DAP_FIELD(capabilities, "capabilities")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ContinuedEvent, + "continued", + DAP_FIELD(allThreadsContinued, + "allThreadsContinued"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExitedEvent, + "exited", + DAP_FIELD(exitCode, "exitCode")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(InitializedEvent, "initialized"); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(InvalidatedEvent, + "invalidated", + DAP_FIELD(areas, "areas"), + DAP_FIELD(stackFrameId, "stackFrameId"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LoadedSourceEvent, + "loadedSource", + DAP_FIELD(reason, "reason"), + DAP_FIELD(source, "source")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(MemoryEvent, + "memory", + DAP_FIELD(count, "count"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(offset, "offset")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ModuleEvent, + "module", + DAP_FIELD(module, "module"), + DAP_FIELD(reason, "reason")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(OutputEvent, + "output", + DAP_FIELD(category, "category"), + DAP_FIELD(column, "column"), + DAP_FIELD(data, "data"), + DAP_FIELD(group, "group"), + DAP_FIELD(line, "line"), + DAP_FIELD(locationReference, "locationReference"), + DAP_FIELD(output, "output"), + DAP_FIELD(source, "source"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ProcessEvent, + "process", + DAP_FIELD(isLocalProcess, "isLocalProcess"), + DAP_FIELD(name, "name"), + DAP_FIELD(pointerSize, "pointerSize"), + DAP_FIELD(startMethod, "startMethod"), + DAP_FIELD(systemProcessId, "systemProcessId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ProgressEndEvent, + "progressEnd", + DAP_FIELD(message, "message"), + DAP_FIELD(progressId, "progressId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ProgressStartEvent, + "progressStart", + DAP_FIELD(cancellable, "cancellable"), + DAP_FIELD(message, "message"), + DAP_FIELD(percentage, "percentage"), + DAP_FIELD(progressId, "progressId"), + DAP_FIELD(requestId, "requestId"), + DAP_FIELD(title, "title")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ProgressUpdateEvent, + "progressUpdate", + DAP_FIELD(message, "message"), + DAP_FIELD(percentage, "percentage"), + DAP_FIELD(progressId, "progressId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StoppedEvent, + "stopped", + DAP_FIELD(allThreadsStopped, "allThreadsStopped"), + DAP_FIELD(description, "description"), + DAP_FIELD(hitBreakpointIds, "hitBreakpointIds"), + DAP_FIELD(preserveFocusHint, "preserveFocusHint"), + DAP_FIELD(reason, "reason"), + DAP_FIELD(text, "text"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(TerminatedEvent, + "terminated", + DAP_FIELD(restart, "restart")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ThreadEvent, + "thread", + DAP_FIELD(reason, "reason"), + DAP_FIELD(threadId, "threadId")); + +} // namespace dap diff --git a/libraries/cppdap/src/protocol_requests.cpp b/libraries/cppdap/src/protocol_requests.cpp new file mode 100644 index 000000000..250ee0193 --- /dev/null +++ b/libraries/cppdap/src/protocol_requests.cpp @@ -0,0 +1,293 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated with protocol_gen.go -- do not edit this file. +// go run scripts/protocol_gen/protocol_gen.go +// +// DAP version 1.68.0 + +#include "dap/protocol.h" + +namespace dap { + +DAP_IMPLEMENT_STRUCT_TYPEINFO(AttachRequest, + "attach", + DAP_FIELD(restart, "__restart")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(BreakpointLocationsRequest, + "breakpointLocations", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(line, "line"), + DAP_FIELD(source, "source")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(CancelRequest, + "cancel", + DAP_FIELD(progressId, "progressId"), + DAP_FIELD(requestId, "requestId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(CompletionsRequest, + "completions", + DAP_FIELD(column, "column"), + DAP_FIELD(frameId, "frameId"), + DAP_FIELD(line, "line"), + DAP_FIELD(text, "text")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ConfigurationDoneRequest, "configurationDone"); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ContinueRequest, + "continue", + DAP_FIELD(singleThread, "singleThread"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DataBreakpointInfoRequest, + "dataBreakpointInfo", + DAP_FIELD(asAddress, "asAddress"), + DAP_FIELD(bytes, "bytes"), + DAP_FIELD(frameId, "frameId"), + DAP_FIELD(mode, "mode"), + DAP_FIELD(name, "name"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DisassembleRequest, + "disassemble", + DAP_FIELD(instructionCount, "instructionCount"), + DAP_FIELD(instructionOffset, "instructionOffset"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(offset, "offset"), + DAP_FIELD(resolveSymbols, "resolveSymbols")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DisconnectRequest, + "disconnect", + DAP_FIELD(restart, "restart"), + DAP_FIELD(suspendDebuggee, "suspendDebuggee"), + DAP_FIELD(terminateDebuggee, + "terminateDebuggee")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(EvaluateRequest, + "evaluate", + DAP_FIELD(column, "column"), + DAP_FIELD(context, "context"), + DAP_FIELD(expression, "expression"), + DAP_FIELD(format, "format"), + DAP_FIELD(frameId, "frameId"), + DAP_FIELD(line, "line"), + DAP_FIELD(source, "source")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionInfoRequest, + "exceptionInfo", + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(GotoRequest, + "goto", + DAP_FIELD(targetId, "targetId"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(GotoTargetsRequest, + "gotoTargets", + DAP_FIELD(column, "column"), + DAP_FIELD(line, "line"), + DAP_FIELD(source, "source")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO( + InitializeRequest, + "initialize", + DAP_FIELD(adapterID, "adapterID"), + DAP_FIELD(clientID, "clientID"), + DAP_FIELD(clientName, "clientName"), + DAP_FIELD(columnsStartAt1, "columnsStartAt1"), + DAP_FIELD(linesStartAt1, "linesStartAt1"), + DAP_FIELD(locale, "locale"), + DAP_FIELD(pathFormat, "pathFormat"), + DAP_FIELD(supportsANSIStyling, "supportsANSIStyling"), + DAP_FIELD(supportsArgsCanBeInterpretedByShell, + "supportsArgsCanBeInterpretedByShell"), + DAP_FIELD(supportsInvalidatedEvent, "supportsInvalidatedEvent"), + DAP_FIELD(supportsMemoryEvent, "supportsMemoryEvent"), + DAP_FIELD(supportsMemoryReferences, "supportsMemoryReferences"), + DAP_FIELD(supportsProgressReporting, "supportsProgressReporting"), + DAP_FIELD(supportsRunInTerminalRequest, "supportsRunInTerminalRequest"), + DAP_FIELD(supportsStartDebuggingRequest, "supportsStartDebuggingRequest"), + DAP_FIELD(supportsVariablePaging, "supportsVariablePaging"), + DAP_FIELD(supportsVariableType, "supportsVariableType")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LaunchRequest, + "launch", + DAP_FIELD(restart, "__restart"), + DAP_FIELD(noDebug, "noDebug")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LoadedSourcesRequest, "loadedSources"); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LocationsRequest, + "locations", + DAP_FIELD(locationReference, + "locationReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ModulesRequest, + "modules", + DAP_FIELD(moduleCount, "moduleCount"), + DAP_FIELD(startModule, "startModule")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(NextRequest, + "next", + DAP_FIELD(granularity, "granularity"), + DAP_FIELD(singleThread, "singleThread"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(PauseRequest, + "pause", + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ReadMemoryRequest, + "readMemory", + DAP_FIELD(count, "count"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(offset, "offset")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(RestartFrameRequest, + "restartFrame", + DAP_FIELD(frameId, "frameId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(RestartRequest, + "restart", + DAP_FIELD(arguments, "arguments")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ReverseContinueRequest, + "reverseContinue", + DAP_FIELD(singleThread, "singleThread"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(RunInTerminalRequest, + "runInTerminal", + DAP_FIELD(args, "args"), + DAP_FIELD(argsCanBeInterpretedByShell, + "argsCanBeInterpretedByShell"), + DAP_FIELD(cwd, "cwd"), + DAP_FIELD(env, "env"), + DAP_FIELD(kind, "kind"), + DAP_FIELD(title, "title")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ScopesRequest, + "scopes", + DAP_FIELD(frameId, "frameId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetBreakpointsRequest, + "setBreakpoints", + DAP_FIELD(breakpoints, "breakpoints"), + DAP_FIELD(lines, "lines"), + DAP_FIELD(source, "source"), + DAP_FIELD(sourceModified, "sourceModified")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetDataBreakpointsRequest, + "setDataBreakpoints", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetExceptionBreakpointsRequest, + "setExceptionBreakpoints", + DAP_FIELD(exceptionOptions, "exceptionOptions"), + DAP_FIELD(filterOptions, "filterOptions"), + DAP_FIELD(filters, "filters")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetExpressionRequest, + "setExpression", + DAP_FIELD(expression, "expression"), + DAP_FIELD(format, "format"), + DAP_FIELD(frameId, "frameId"), + DAP_FIELD(value, "value")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetFunctionBreakpointsRequest, + "setFunctionBreakpoints", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetInstructionBreakpointsRequest, + "setInstructionBreakpoints", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetVariableRequest, + "setVariable", + DAP_FIELD(format, "format"), + DAP_FIELD(name, "name"), + DAP_FIELD(value, "value"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SourceRequest, + "source", + DAP_FIELD(source, "source"), + DAP_FIELD(sourceReference, "sourceReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StackTraceRequest, + "stackTrace", + DAP_FIELD(format, "format"), + DAP_FIELD(levels, "levels"), + DAP_FIELD(startFrame, "startFrame"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StartDebuggingRequest, + "startDebugging", + DAP_FIELD(configuration, "configuration"), + DAP_FIELD(request, "request")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepBackRequest, + "stepBack", + DAP_FIELD(granularity, "granularity"), + DAP_FIELD(singleThread, "singleThread"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepInRequest, + "stepIn", + DAP_FIELD(granularity, "granularity"), + DAP_FIELD(singleThread, "singleThread"), + DAP_FIELD(targetId, "targetId"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepInTargetsRequest, + "stepInTargets", + DAP_FIELD(frameId, "frameId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepOutRequest, + "stepOut", + DAP_FIELD(granularity, "granularity"), + DAP_FIELD(singleThread, "singleThread"), + DAP_FIELD(threadId, "threadId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(TerminateRequest, + "terminate", + DAP_FIELD(restart, "restart")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(TerminateThreadsRequest, + "terminateThreads", + DAP_FIELD(threadIds, "threadIds")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ThreadsRequest, "threads"); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(VariablesRequest, + "variables", + DAP_FIELD(count, "count"), + DAP_FIELD(filter, "filter"), + DAP_FIELD(format, "format"), + DAP_FIELD(start, "start"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(WriteMemoryRequest, + "writeMemory", + DAP_FIELD(allowPartial, "allowPartial"), + DAP_FIELD(data, "data"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(offset, "offset")); + +} // namespace dap diff --git a/libraries/cppdap/src/protocol_response.cpp b/libraries/cppdap/src/protocol_response.cpp new file mode 100644 index 000000000..b44cf1b20 --- /dev/null +++ b/libraries/cppdap/src/protocol_response.cpp @@ -0,0 +1,262 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated with protocol_gen.go -- do not edit this file. +// go run scripts/protocol_gen/protocol_gen.go +// +// DAP version 1.68.0 + +#include "dap/protocol.h" + +namespace dap { + +DAP_IMPLEMENT_STRUCT_TYPEINFO(AttachResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(BreakpointLocationsResponse, + "", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(CancelResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(CompletionsResponse, + "", + DAP_FIELD(targets, "targets")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ConfigurationDoneResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ContinueResponse, + "", + DAP_FIELD(allThreadsContinued, + "allThreadsContinued")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DataBreakpointInfoResponse, + "", + DAP_FIELD(accessTypes, "accessTypes"), + DAP_FIELD(canPersist, "canPersist"), + DAP_FIELD(dataId, "dataId"), + DAP_FIELD(description, "description")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DisassembleResponse, + "", + DAP_FIELD(instructions, "instructions")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DisconnectResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ErrorResponse, "", DAP_FIELD(error, "error")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(EvaluateResponse, + "", + DAP_FIELD(indexedVariables, "indexedVariables"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(namedVariables, "namedVariables"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(result, "result"), + DAP_FIELD(type, "type"), + DAP_FIELD(valueLocationReference, + "valueLocationReference"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionInfoResponse, + "", + DAP_FIELD(breakMode, "breakMode"), + DAP_FIELD(description, "description"), + DAP_FIELD(details, "details"), + DAP_FIELD(exceptionId, "exceptionId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(GotoResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(GotoTargetsResponse, + "", + DAP_FIELD(targets, "targets")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO( + InitializeResponse, + "", + DAP_FIELD(additionalModuleColumns, "additionalModuleColumns"), + DAP_FIELD(breakpointModes, "breakpointModes"), + DAP_FIELD(completionTriggerCharacters, "completionTriggerCharacters"), + DAP_FIELD(exceptionBreakpointFilters, "exceptionBreakpointFilters"), + DAP_FIELD(supportSuspendDebuggee, "supportSuspendDebuggee"), + DAP_FIELD(supportTerminateDebuggee, "supportTerminateDebuggee"), + DAP_FIELD(supportedChecksumAlgorithms, "supportedChecksumAlgorithms"), + DAP_FIELD(supportsANSIStyling, "supportsANSIStyling"), + DAP_FIELD(supportsBreakpointLocationsRequest, + "supportsBreakpointLocationsRequest"), + DAP_FIELD(supportsCancelRequest, "supportsCancelRequest"), + DAP_FIELD(supportsClipboardContext, "supportsClipboardContext"), + DAP_FIELD(supportsCompletionsRequest, "supportsCompletionsRequest"), + DAP_FIELD(supportsConditionalBreakpoints, "supportsConditionalBreakpoints"), + DAP_FIELD(supportsConfigurationDoneRequest, + "supportsConfigurationDoneRequest"), + DAP_FIELD(supportsDataBreakpointBytes, "supportsDataBreakpointBytes"), + DAP_FIELD(supportsDataBreakpoints, "supportsDataBreakpoints"), + DAP_FIELD(supportsDelayedStackTraceLoading, + "supportsDelayedStackTraceLoading"), + DAP_FIELD(supportsDisassembleRequest, "supportsDisassembleRequest"), + DAP_FIELD(supportsEvaluateForHovers, "supportsEvaluateForHovers"), + DAP_FIELD(supportsExceptionFilterOptions, "supportsExceptionFilterOptions"), + DAP_FIELD(supportsExceptionInfoRequest, "supportsExceptionInfoRequest"), + DAP_FIELD(supportsExceptionOptions, "supportsExceptionOptions"), + DAP_FIELD(supportsFunctionBreakpoints, "supportsFunctionBreakpoints"), + DAP_FIELD(supportsGotoTargetsRequest, "supportsGotoTargetsRequest"), + DAP_FIELD(supportsHitConditionalBreakpoints, + "supportsHitConditionalBreakpoints"), + DAP_FIELD(supportsInstructionBreakpoints, "supportsInstructionBreakpoints"), + DAP_FIELD(supportsLoadedSourcesRequest, "supportsLoadedSourcesRequest"), + DAP_FIELD(supportsLogPoints, "supportsLogPoints"), + DAP_FIELD(supportsModulesRequest, "supportsModulesRequest"), + DAP_FIELD(supportsReadMemoryRequest, "supportsReadMemoryRequest"), + DAP_FIELD(supportsRestartFrame, "supportsRestartFrame"), + DAP_FIELD(supportsRestartRequest, "supportsRestartRequest"), + DAP_FIELD(supportsSetExpression, "supportsSetExpression"), + DAP_FIELD(supportsSetVariable, "supportsSetVariable"), + DAP_FIELD(supportsSingleThreadExecutionRequests, + "supportsSingleThreadExecutionRequests"), + DAP_FIELD(supportsStepBack, "supportsStepBack"), + DAP_FIELD(supportsStepInTargetsRequest, "supportsStepInTargetsRequest"), + DAP_FIELD(supportsSteppingGranularity, "supportsSteppingGranularity"), + DAP_FIELD(supportsTerminateRequest, "supportsTerminateRequest"), + DAP_FIELD(supportsTerminateThreadsRequest, + "supportsTerminateThreadsRequest"), + DAP_FIELD(supportsValueFormattingOptions, "supportsValueFormattingOptions"), + DAP_FIELD(supportsWriteMemoryRequest, "supportsWriteMemoryRequest")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LaunchResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LoadedSourcesResponse, + "", + DAP_FIELD(sources, "sources")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(LocationsResponse, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(line, "line"), + DAP_FIELD(source, "source")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ModulesResponse, + "", + DAP_FIELD(modules, "modules"), + DAP_FIELD(totalModules, "totalModules")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(NextResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(PauseResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ReadMemoryResponse, + "", + DAP_FIELD(address, "address"), + DAP_FIELD(data, "data"), + DAP_FIELD(unreadableBytes, "unreadableBytes")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(RestartFrameResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(RestartResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ReverseContinueResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(RunInTerminalResponse, + "", + DAP_FIELD(processId, "processId"), + DAP_FIELD(shellProcessId, "shellProcessId")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ScopesResponse, "", DAP_FIELD(scopes, "scopes")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetBreakpointsResponse, + "", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetDataBreakpointsResponse, + "", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetExceptionBreakpointsResponse, + "", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetExpressionResponse, + "", + DAP_FIELD(indexedVariables, "indexedVariables"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(namedVariables, "namedVariables"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(type, "type"), + DAP_FIELD(value, "value"), + DAP_FIELD(valueLocationReference, + "valueLocationReference"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetFunctionBreakpointsResponse, + "", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetInstructionBreakpointsResponse, + "", + DAP_FIELD(breakpoints, "breakpoints")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SetVariableResponse, + "", + DAP_FIELD(indexedVariables, "indexedVariables"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(namedVariables, "namedVariables"), + DAP_FIELD(type, "type"), + DAP_FIELD(value, "value"), + DAP_FIELD(valueLocationReference, + "valueLocationReference"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SourceResponse, + "", + DAP_FIELD(content, "content"), + DAP_FIELD(mimeType, "mimeType")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StackTraceResponse, + "", + DAP_FIELD(stackFrames, "stackFrames"), + DAP_FIELD(totalFrames, "totalFrames")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StartDebuggingResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepBackResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepInResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepInTargetsResponse, + "", + DAP_FIELD(targets, "targets")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepOutResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(TerminateResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(TerminateThreadsResponse, ""); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ThreadsResponse, + "", + DAP_FIELD(threads, "threads")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(VariablesResponse, + "", + DAP_FIELD(variables, "variables")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(WriteMemoryResponse, + "", + DAP_FIELD(bytesWritten, "bytesWritten"), + DAP_FIELD(offset, "offset")); + +} // namespace dap diff --git a/libraries/cppdap/src/protocol_types.cpp b/libraries/cppdap/src/protocol_types.cpp new file mode 100644 index 000000000..68b726a05 --- /dev/null +++ b/libraries/cppdap/src/protocol_types.cpp @@ -0,0 +1,333 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated with protocol_gen.go -- do not edit this file. +// go run scripts/protocol_gen/protocol_gen.go +// +// DAP version 1.68.0 + +#include "dap/protocol.h" + +namespace dap { + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Checksum, + "", + DAP_FIELD(algorithm, "algorithm"), + DAP_FIELD(checksum, "checksum")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Source, + "", + DAP_FIELD(adapterData, "adapterData"), + DAP_FIELD(checksums, "checksums"), + DAP_FIELD(name, "name"), + DAP_FIELD(origin, "origin"), + DAP_FIELD(path, "path"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(sourceReference, "sourceReference"), + DAP_FIELD(sources, "sources")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Breakpoint, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(id, "id"), + DAP_FIELD(instructionReference, + "instructionReference"), + DAP_FIELD(line, "line"), + DAP_FIELD(message, "message"), + DAP_FIELD(offset, "offset"), + DAP_FIELD(reason, "reason"), + DAP_FIELD(source, "source"), + DAP_FIELD(verified, "verified")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(BreakpointLocation, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(line, "line")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ColumnDescriptor, + "", + DAP_FIELD(attributeName, "attributeName"), + DAP_FIELD(format, "format"), + DAP_FIELD(label, "label"), + DAP_FIELD(type, "type"), + DAP_FIELD(width, "width")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(BreakpointMode, + "", + DAP_FIELD(appliesTo, "appliesTo"), + DAP_FIELD(description, "description"), + DAP_FIELD(label, "label"), + DAP_FIELD(mode, "mode")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionBreakpointsFilter, + "", + DAP_FIELD(conditionDescription, + "conditionDescription"), + DAP_FIELD(def, "default"), + DAP_FIELD(description, "description"), + DAP_FIELD(filter, "filter"), + DAP_FIELD(label, "label"), + DAP_FIELD(supportsCondition, + "supportsCondition")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO( + Capabilities, + "", + DAP_FIELD(additionalModuleColumns, "additionalModuleColumns"), + DAP_FIELD(breakpointModes, "breakpointModes"), + DAP_FIELD(completionTriggerCharacters, "completionTriggerCharacters"), + DAP_FIELD(exceptionBreakpointFilters, "exceptionBreakpointFilters"), + DAP_FIELD(supportSuspendDebuggee, "supportSuspendDebuggee"), + DAP_FIELD(supportTerminateDebuggee, "supportTerminateDebuggee"), + DAP_FIELD(supportedChecksumAlgorithms, "supportedChecksumAlgorithms"), + DAP_FIELD(supportsANSIStyling, "supportsANSIStyling"), + DAP_FIELD(supportsBreakpointLocationsRequest, + "supportsBreakpointLocationsRequest"), + DAP_FIELD(supportsCancelRequest, "supportsCancelRequest"), + DAP_FIELD(supportsClipboardContext, "supportsClipboardContext"), + DAP_FIELD(supportsCompletionsRequest, "supportsCompletionsRequest"), + DAP_FIELD(supportsConditionalBreakpoints, "supportsConditionalBreakpoints"), + DAP_FIELD(supportsConfigurationDoneRequest, + "supportsConfigurationDoneRequest"), + DAP_FIELD(supportsDataBreakpointBytes, "supportsDataBreakpointBytes"), + DAP_FIELD(supportsDataBreakpoints, "supportsDataBreakpoints"), + DAP_FIELD(supportsDelayedStackTraceLoading, + "supportsDelayedStackTraceLoading"), + DAP_FIELD(supportsDisassembleRequest, "supportsDisassembleRequest"), + DAP_FIELD(supportsEvaluateForHovers, "supportsEvaluateForHovers"), + DAP_FIELD(supportsExceptionFilterOptions, "supportsExceptionFilterOptions"), + DAP_FIELD(supportsExceptionInfoRequest, "supportsExceptionInfoRequest"), + DAP_FIELD(supportsExceptionOptions, "supportsExceptionOptions"), + DAP_FIELD(supportsFunctionBreakpoints, "supportsFunctionBreakpoints"), + DAP_FIELD(supportsGotoTargetsRequest, "supportsGotoTargetsRequest"), + DAP_FIELD(supportsHitConditionalBreakpoints, + "supportsHitConditionalBreakpoints"), + DAP_FIELD(supportsInstructionBreakpoints, "supportsInstructionBreakpoints"), + DAP_FIELD(supportsLoadedSourcesRequest, "supportsLoadedSourcesRequest"), + DAP_FIELD(supportsLogPoints, "supportsLogPoints"), + DAP_FIELD(supportsModulesRequest, "supportsModulesRequest"), + DAP_FIELD(supportsReadMemoryRequest, "supportsReadMemoryRequest"), + DAP_FIELD(supportsRestartFrame, "supportsRestartFrame"), + DAP_FIELD(supportsRestartRequest, "supportsRestartRequest"), + DAP_FIELD(supportsSetExpression, "supportsSetExpression"), + DAP_FIELD(supportsSetVariable, "supportsSetVariable"), + DAP_FIELD(supportsSingleThreadExecutionRequests, + "supportsSingleThreadExecutionRequests"), + DAP_FIELD(supportsStepBack, "supportsStepBack"), + DAP_FIELD(supportsStepInTargetsRequest, "supportsStepInTargetsRequest"), + DAP_FIELD(supportsSteppingGranularity, "supportsSteppingGranularity"), + DAP_FIELD(supportsTerminateRequest, "supportsTerminateRequest"), + DAP_FIELD(supportsTerminateThreadsRequest, + "supportsTerminateThreadsRequest"), + DAP_FIELD(supportsValueFormattingOptions, "supportsValueFormattingOptions"), + DAP_FIELD(supportsWriteMemoryRequest, "supportsWriteMemoryRequest")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(CompletionItem, + "", + DAP_FIELD(detail, "detail"), + DAP_FIELD(label, "label"), + DAP_FIELD(length, "length"), + DAP_FIELD(selectionLength, "selectionLength"), + DAP_FIELD(selectionStart, "selectionStart"), + DAP_FIELD(sortText, "sortText"), + DAP_FIELD(start, "start"), + DAP_FIELD(text, "text"), + DAP_FIELD(type, "type")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DisassembledInstruction, + "", + DAP_FIELD(address, "address"), + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(instruction, "instruction"), + DAP_FIELD(instructionBytes, "instructionBytes"), + DAP_FIELD(line, "line"), + DAP_FIELD(location, "location"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(symbol, "symbol")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Message, + "", + DAP_FIELD(format, "format"), + DAP_FIELD(id, "id"), + DAP_FIELD(sendTelemetry, "sendTelemetry"), + DAP_FIELD(showUser, "showUser"), + DAP_FIELD(url, "url"), + DAP_FIELD(urlLabel, "urlLabel"), + DAP_FIELD(variables, "variables")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(VariablePresentationHint, + "", + DAP_FIELD(attributes, "attributes"), + DAP_FIELD(kind, "kind"), + DAP_FIELD(lazy, "lazy"), + DAP_FIELD(visibility, "visibility")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ValueFormat, "", DAP_FIELD(hex, "hex")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionDetails, + "", + DAP_FIELD(evaluateName, "evaluateName"), + DAP_FIELD(fullTypeName, "fullTypeName"), + DAP_FIELD(innerException, "innerException"), + DAP_FIELD(message, "message"), + DAP_FIELD(stackTrace, "stackTrace"), + DAP_FIELD(typeName, "typeName")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(GotoTarget, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(id, "id"), + DAP_FIELD(instructionPointerReference, + "instructionPointerReference"), + DAP_FIELD(label, "label"), + DAP_FIELD(line, "line")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Module, + "", + DAP_FIELD(addressRange, "addressRange"), + DAP_FIELD(dateTimeStamp, "dateTimeStamp"), + DAP_FIELD(id, "id"), + DAP_FIELD(isOptimized, "isOptimized"), + DAP_FIELD(isUserCode, "isUserCode"), + DAP_FIELD(name, "name"), + DAP_FIELD(path, "path"), + DAP_FIELD(symbolFilePath, "symbolFilePath"), + DAP_FIELD(symbolStatus, "symbolStatus"), + DAP_FIELD(version, "version")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Scope, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(expensive, "expensive"), + DAP_FIELD(indexedVariables, "indexedVariables"), + DAP_FIELD(line, "line"), + DAP_FIELD(name, "name"), + DAP_FIELD(namedVariables, "namedVariables"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(source, "source"), + DAP_FIELD(variablesReference, + "variablesReference")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(SourceBreakpoint, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(condition, "condition"), + DAP_FIELD(hitCondition, "hitCondition"), + DAP_FIELD(line, "line"), + DAP_FIELD(logMessage, "logMessage"), + DAP_FIELD(mode, "mode")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(DataBreakpoint, + "", + DAP_FIELD(accessType, "accessType"), + DAP_FIELD(condition, "condition"), + DAP_FIELD(dataId, "dataId"), + DAP_FIELD(hitCondition, "hitCondition")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionPathSegment, + "", + DAP_FIELD(names, "names"), + DAP_FIELD(negate, "negate")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionOptions, + "", + DAP_FIELD(breakMode, "breakMode"), + DAP_FIELD(path, "path")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(ExceptionFilterOptions, + "", + DAP_FIELD(condition, "condition"), + DAP_FIELD(filterId, "filterId"), + DAP_FIELD(mode, "mode")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(FunctionBreakpoint, + "", + DAP_FIELD(condition, "condition"), + DAP_FIELD(hitCondition, "hitCondition"), + DAP_FIELD(name, "name")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(InstructionBreakpoint, + "", + DAP_FIELD(condition, "condition"), + DAP_FIELD(hitCondition, "hitCondition"), + DAP_FIELD(instructionReference, + "instructionReference"), + DAP_FIELD(mode, "mode"), + DAP_FIELD(offset, "offset")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StackFrame, + "", + DAP_FIELD(canRestart, "canRestart"), + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(id, "id"), + DAP_FIELD(instructionPointerReference, + "instructionPointerReference"), + DAP_FIELD(line, "line"), + DAP_FIELD(moduleId, "moduleId"), + DAP_FIELD(name, "name"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(source, "source")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StackFrameFormat, + "", + DAP_FIELD(includeAll, "includeAll"), + DAP_FIELD(line, "line"), + DAP_FIELD(module, "module"), + DAP_FIELD(parameterNames, "parameterNames"), + DAP_FIELD(parameterTypes, "parameterTypes"), + DAP_FIELD(parameterValues, "parameterValues"), + DAP_FIELD(parameters, "parameters")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(StepInTarget, + "", + DAP_FIELD(column, "column"), + DAP_FIELD(endColumn, "endColumn"), + DAP_FIELD(endLine, "endLine"), + DAP_FIELD(id, "id"), + DAP_FIELD(label, "label"), + DAP_FIELD(line, "line")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO(Thread, + "", + DAP_FIELD(id, "id"), + DAP_FIELD(name, "name")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO( + Variable, + "", + DAP_FIELD(declarationLocationReference, "declarationLocationReference"), + DAP_FIELD(evaluateName, "evaluateName"), + DAP_FIELD(indexedVariables, "indexedVariables"), + DAP_FIELD(memoryReference, "memoryReference"), + DAP_FIELD(name, "name"), + DAP_FIELD(namedVariables, "namedVariables"), + DAP_FIELD(presentationHint, "presentationHint"), + DAP_FIELD(type, "type"), + DAP_FIELD(value, "value"), + DAP_FIELD(valueLocationReference, "valueLocationReference"), + DAP_FIELD(variablesReference, "variablesReference")); + +} // namespace dap diff --git a/libraries/cppdap/src/rapid_json_serializer.cpp b/libraries/cppdap/src/rapid_json_serializer.cpp new file mode 100644 index 000000000..16ff43f42 --- /dev/null +++ b/libraries/cppdap/src/rapid_json_serializer.cpp @@ -0,0 +1,290 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "rapid_json_serializer.h" + +#include "null_json_serializer.h" + +#include +#include + +namespace dap { +namespace json { + +RapidDeserializer::RapidDeserializer(const std::string& str) + : doc(new rapidjson::Document()) { + doc->Parse(str.c_str()); +} + +RapidDeserializer::RapidDeserializer(rapidjson::Value* json) : val(json) {} + +RapidDeserializer::~RapidDeserializer() { + delete doc; +} + +bool RapidDeserializer::deserialize(dap::boolean* v) const { + if (!json()->IsBool()) { + return false; + } + *v = json()->GetBool(); + return true; +} + +bool RapidDeserializer::deserialize(dap::integer* v) const { + if (json()->IsInt()) { + *v = json()->GetInt(); + return true; + } else if (json()->IsUint()) { + *v = static_cast(json()->GetUint()); + return true; + } else if (json()->IsInt64()) { + *v = json()->GetInt64(); + return true; + } else if (json()->IsUint64()) { + *v = static_cast(json()->GetUint64()); + return true; + } + return false; +} + +bool RapidDeserializer::deserialize(dap::number* v) const { + if (!json()->IsNumber()) { + return false; + } + *v = json()->GetDouble(); + return true; +} + +bool RapidDeserializer::deserialize(dap::string* v) const { + if (!json()->IsString()) { + return false; + } + *v = json()->GetString(); + return true; +} + +bool RapidDeserializer::deserialize(dap::object* v) const { + v->reserve(json()->MemberCount()); + for (auto el = json()->MemberBegin(); el != json()->MemberEnd(); el++) { + dap::any el_val; + RapidDeserializer d(&(el->value)); + if (!d.deserialize(&el_val)) { + return false; + } + (*v)[el->name.GetString()] = el_val; + } + return true; +} + +bool RapidDeserializer::deserialize(dap::any* v) const { + if (json()->IsBool()) { + *v = dap::boolean(json()->GetBool()); + } else if (json()->IsDouble()) { + *v = dap::number(json()->GetDouble()); + } else if (json()->IsInt()) { + *v = dap::integer(json()->GetInt()); + } else if (json()->IsString()) { + *v = dap::string(json()->GetString()); + } else if (json()->IsNull()) { + *v = null(); + } else if (json()->IsObject()) { + dap::object obj; + if (!deserialize(&obj)) { + return false; + } + *v = obj; + } else if (json()->IsArray()) { + dap::array arr; + if (!deserialize(&arr)) { + return false; + } + *v = arr; + } else { + return false; + } + return true; +} + +size_t RapidDeserializer::count() const { + return json()->Size(); +} + +bool RapidDeserializer::array( + const std::function& cb) const { + if (!json()->IsArray()) { + return false; + } + for (uint32_t i = 0; i < json()->Size(); i++) { + RapidDeserializer d(&(*json())[i]); + if (!cb(&d)) { + return false; + } + } + return true; +} + +bool RapidDeserializer::field( + const std::string& name, + const std::function& cb) const { + if (!json()->IsObject()) { + return false; + } + auto it = json()->FindMember(name.c_str()); + if (it == json()->MemberEnd()) { + return cb(&NullDeserializer::instance); + } + RapidDeserializer d(&(it->value)); + return cb(&d); +} + +RapidSerializer::RapidSerializer() + : doc(new rapidjson::Document(rapidjson::kObjectType)), + allocator(doc->GetAllocator()) {} + +RapidSerializer::RapidSerializer(rapidjson::Value* json, + rapidjson::Document::AllocatorType& allocator) + : val(json), allocator(allocator) {} + +RapidSerializer::~RapidSerializer() { + delete doc; +} + +std::string RapidSerializer::dump() const { + rapidjson::StringBuffer sb; + rapidjson::PrettyWriter writer(sb); + json()->Accept(writer); + return sb.GetString(); +} + +bool RapidSerializer::serialize(dap::boolean v) { + json()->SetBool(v); + return true; +} + +bool RapidSerializer::serialize(dap::integer v) { + json()->SetInt64(v); + return true; +} + +bool RapidSerializer::serialize(dap::number v) { + json()->SetDouble(v); + return true; +} + +bool RapidSerializer::serialize(const dap::string& v) { + json()->SetString(v.data(), static_cast(v.length()), allocator); + return true; +} + +bool RapidSerializer::serialize(const dap::object& v) { + if (!json()->IsObject()) { + json()->SetObject(); + } + for (auto& it : v) { + if (!json()->HasMember(it.first.c_str())) { + rapidjson::Value name_value{it.first.c_str(), allocator}; + json()->AddMember(name_value, rapidjson::Value(), allocator); + } + rapidjson::Value& member = (*json())[it.first.c_str()]; + RapidSerializer s(&member, allocator); + if (!s.serialize(it.second)) { + return false; + } + } + return true; +} + +bool RapidSerializer::serialize(const dap::any& v) { + if (v.is()) { + json()->SetBool((bool)v.get()); + } else if (v.is()) { + json()->SetInt64(v.get()); + } else if (v.is()) { + json()->SetDouble((double)v.get()); + } else if (v.is()) { + auto s = v.get(); + json()->SetString(s.data(), static_cast(s.length()), allocator); + } else if (v.is()) { + // reachable if dap::object nested is inside other dap::object + return serialize(v.get()); + } else if (v.is()) { + } else { + // reachable if array or custom serialized type is nested inside other + // dap::object + auto type = get_any_type(v); + auto value = get_any_val(v); + if (type && value) { + return type->serialize(this, value); + } + return false; + } + + return true; +} + +bool RapidSerializer::array(size_t count, + const std::function& cb) { + if (!json()->IsArray()) { + json()->SetArray(); + } + + while (count > json()->Size()) { + json()->PushBack(rapidjson::Value(), allocator); + } + + for (uint32_t i = 0; i < count; i++) { + RapidSerializer s(&(*json())[i], allocator); + if (!cb(&s)) { + return false; + } + } + return true; +} + +bool RapidSerializer::object( + const std::function& cb) { + struct FS : public FieldSerializer { + rapidjson::Value* const json; + rapidjson::Document::AllocatorType& allocator; + + FS(rapidjson::Value* json, rapidjson::Document::AllocatorType& allocator) + : json(json), allocator(allocator) {} + bool field(const std::string& name, const SerializeFunc& cb) override { + if (!json->HasMember(name.c_str())) { + rapidjson::Value name_value{name.c_str(), allocator}; + json->AddMember(name_value, rapidjson::Value(), allocator); + } + rapidjson::Value& member = (*json)[name.c_str()]; + RapidSerializer s(&member, allocator); + auto res = cb(&s); + if (s.removed) { + json->RemoveMember(name.c_str()); + } + return res; + } + }; + + if (!json()->IsObject()) { + json()->SetObject(); + } + FS fs{json(), allocator}; + return cb(&fs); +} + +void RapidSerializer::remove() { + removed = true; +} + +} // namespace json +} // namespace dap diff --git a/libraries/cppdap/src/rapid_json_serializer.h b/libraries/cppdap/src/rapid_json_serializer.h new file mode 100644 index 000000000..6e8338413 --- /dev/null +++ b/libraries/cppdap/src/rapid_json_serializer.h @@ -0,0 +1,138 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_rapid_json_serializer_h +#define dap_rapid_json_serializer_h + +#include "dap/protocol.h" +#include "dap/serialization.h" +#include "dap/types.h" + +#include + +namespace dap { +namespace json { + +struct RapidDeserializer : public dap::Deserializer { + explicit RapidDeserializer(const std::string&); + ~RapidDeserializer(); + + // dap::Deserializer compliance + bool deserialize(boolean* v) const override; + bool deserialize(integer* v) const override; + bool deserialize(number* v) const override; + bool deserialize(string* v) const override; + bool deserialize(object* v) const override; + bool deserialize(any* v) const override; + size_t count() const override; + bool array(const std::function&) const override; + bool field(const std::string& name, + const std::function&) const override; + + // Unhide base overloads + template + inline bool field(const std::string& name, T* v) { + return dap::Deserializer::field(name, v); + } + + template ::has_custom_serialization>> + inline bool deserialize(T* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::array* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::optional* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool deserialize(dap::variant* v) const { + return dap::Deserializer::deserialize(v); + } + + template + inline bool field(const std::string& name, T* v) const { + return dap::Deserializer::deserialize(name, v); + } + + inline rapidjson::Value* json() const { return (val == nullptr) ? doc : val; } + + private: + RapidDeserializer(rapidjson::Value*); + rapidjson::Document* const doc = nullptr; + rapidjson::Value* const val = nullptr; +}; + +struct RapidSerializer : public dap::Serializer { + RapidSerializer(); + ~RapidSerializer(); + + std::string dump() const; + + // dap::Serializer compliance + bool serialize(boolean v) override; + bool serialize(integer v) override; + bool serialize(number v) override; + bool serialize(const string& v) override; + bool serialize(const dap::object& v) override; + bool serialize(const any& v) override; + bool array(size_t count, + const std::function&) override; + bool object(const std::function&) override; + void remove() override; + + // Unhide base overloads + template ::has_custom_serialization>> + inline bool serialize(const T& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::array& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::optional& v) { + return dap::Serializer::serialize(v); + } + + template + inline bool serialize(const dap::variant& v) { + return dap::Serializer::serialize(v); + } + + inline bool serialize(const char* v) { return dap::Serializer::serialize(v); } + + inline rapidjson::Value* json() const { return (val == nullptr) ? doc : val; } + + private: + RapidSerializer(rapidjson::Value*, rapidjson::Document::AllocatorType&); + rapidjson::Document* const doc = nullptr; + rapidjson::Value* const val = nullptr; + rapidjson::Document::AllocatorType& allocator; + bool removed = false; +}; + +} // namespace json +} // namespace dap + +#endif // dap_rapid_json_serializer_h diff --git a/libraries/cppdap/src/rwmutex.h b/libraries/cppdap/src/rwmutex.h new file mode 100644 index 000000000..9e858913f --- /dev/null +++ b/libraries/cppdap/src/rwmutex.h @@ -0,0 +1,172 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_rwmutex_h +#define dap_rwmutex_h + +#include +#include + +namespace dap { + +//////////////////////////////////////////////////////////////////////////////// +// RWMutex +//////////////////////////////////////////////////////////////////////////////// + +// A RWMutex is a reader/writer mutual exclusion lock. +// The lock can be held by an arbitrary number of readers or a single writer. +// Also known as a shared mutex. +class RWMutex { + public: + inline RWMutex() = default; + + // lockReader() locks the mutex for reading. + // Multiple read locks can be held while there are no writer locks. + inline void lockReader(); + + // unlockReader() unlocks the mutex for reading. + inline void unlockReader(); + + // lockWriter() locks the mutex for writing. + // If the lock is already locked for reading or writing, lockWriter blocks + // until the lock is available. + inline void lockWriter(); + + // unlockWriter() unlocks the mutex for writing. + inline void unlockWriter(); + + private: + RWMutex(const RWMutex&) = delete; + RWMutex& operator=(const RWMutex&) = delete; + + int readLocks = 0; + int pendingWriteLocks = 0; + std::mutex mutex; + std::condition_variable cv; +}; + +void RWMutex::lockReader() { + std::unique_lock lock(mutex); + readLocks++; +} + +void RWMutex::unlockReader() { + std::unique_lock lock(mutex); + readLocks--; + if (readLocks == 0 && pendingWriteLocks > 0) { + cv.notify_one(); + } +} + +void RWMutex::lockWriter() { + std::unique_lock lock(mutex); + if (readLocks > 0) { + pendingWriteLocks++; + cv.wait(lock, [&] { return readLocks == 0; }); + pendingWriteLocks--; + } + lock.release(); // Keep lock held +} + +void RWMutex::unlockWriter() { + if (pendingWriteLocks > 0) { + cv.notify_one(); + } + mutex.unlock(); +} + +//////////////////////////////////////////////////////////////////////////////// +// RLock +//////////////////////////////////////////////////////////////////////////////// + +// RLock is a RAII read lock helper for a RWMutex. +class RLock { + public: + inline RLock(RWMutex& mutex); + inline ~RLock(); + + inline RLock(RLock&&); + inline RLock& operator=(RLock&&); + + private: + RLock(const RLock&) = delete; + RLock& operator=(const RLock&) = delete; + + RWMutex* m; +}; + +RLock::RLock(RWMutex& mutex) : m(&mutex) { + m->lockReader(); +} + +RLock::~RLock() { + if (m != nullptr) { + m->unlockReader(); + } +} + +RLock::RLock(RLock&& other) { + m = other.m; + other.m = nullptr; +} + +RLock& RLock::operator=(RLock&& other) { + m = other.m; + other.m = nullptr; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +// WLock +//////////////////////////////////////////////////////////////////////////////// + +// WLock is a RAII write lock helper for a RWMutex. +class WLock { + public: + inline WLock(RWMutex& mutex); + inline ~WLock(); + + inline WLock(WLock&&); + inline WLock& operator=(WLock&&); + + private: + WLock(const WLock&) = delete; + WLock& operator=(const WLock&) = delete; + + RWMutex* m; +}; + +WLock::WLock(RWMutex& mutex) : m(&mutex) { + m->lockWriter(); +} + +WLock::~WLock() { + if (m != nullptr) { + m->unlockWriter(); + } +} + +WLock::WLock(WLock&& other) { + m = other.m; + other.m = nullptr; +} + +WLock& WLock::operator=(WLock&& other) { + m = other.m; + other.m = nullptr; + return *this; +} +} // namespace dap + +#endif diff --git a/libraries/cppdap/src/rwmutex_test.cpp b/libraries/cppdap/src/rwmutex_test.cpp new file mode 100644 index 000000000..944ed7752 --- /dev/null +++ b/libraries/cppdap/src/rwmutex_test.cpp @@ -0,0 +1,113 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "rwmutex.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace { +constexpr const size_t NumThreads = 8; +} + +// Check that WLock behaves like regular mutex. +TEST(RWMutex, WLock) { + dap::RWMutex rwmutex; + int counter = 0; + + std::vector threads; + for (size_t i = 0; i < NumThreads; i++) { + threads.emplace_back([&] { + for (int j = 0; j < 1000; j++) { + dap::WLock lock(rwmutex); + counter++; + EXPECT_EQ(counter, 1); + counter--; + } + }); + } + + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_EQ(counter, 0); +} + +TEST(RWMutex, NoRLockWithWLock) { + dap::RWMutex rwmutex; + + std::vector threads; + std::array counters = {}; + + { // With WLock held... + dap::WLock wlock(rwmutex); + + for (size_t i = 0; i < counters.size(); i++) { + int* counter = &counters[i]; + threads.emplace_back([&rwmutex, counter] { + dap::RLock lock(rwmutex); + for (int j = 0; j < 1000; j++) { + (*counter)++; + } + }); + } + + // RLocks should block + for (int counter : counters) { + EXPECT_EQ(counter, 0); + } + } + + for (auto& thread : threads) { + thread.join(); + } + + for (int counter : counters) { + EXPECT_EQ(counter, 1000); + } +} + +TEST(RWMutex, NoWLockWithRLock) { + dap::RWMutex rwmutex; + + std::vector threads; + size_t counter = 0; + + { // With RLocks held... + dap::RLock rlockA(rwmutex); + dap::RLock rlockB(rwmutex); + dap::RLock rlockC(rwmutex); + + for (size_t i = 0; i < NumThreads; i++) { + threads.emplace_back(std::thread([&] { + dap::WLock lock(rwmutex); + counter++; + })); + } + + // ... WLocks should block + EXPECT_EQ(counter, 0U); + } + + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_EQ(counter, NumThreads); +} diff --git a/libraries/cppdap/src/session.cpp b/libraries/cppdap/src/session.cpp new file mode 100644 index 000000000..ffc77751f --- /dev/null +++ b/libraries/cppdap/src/session.cpp @@ -0,0 +1,521 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "content_stream.h" + +#include "dap/any.h" +#include "dap/session.h" + +#include "chan.h" +#include "json_serializer.h" +#include "socket.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class Impl : public dap::Session { + public: + void setOnInvalidData(dap::OnInvalidData onInvalidData_) override { + this->onInvalidData = onInvalidData_; + } + + void onError(const ErrorHandler& handler) override { handlers.put(handler); } + + void registerHandler(const dap::TypeInfo* typeinfo, + const GenericRequestHandler& handler) override { + handlers.put(typeinfo, handler); + } + + void registerHandler(const dap::TypeInfo* typeinfo, + const GenericEventHandler& handler) override { + handlers.put(typeinfo, handler); + } + + void registerHandler(const dap::TypeInfo* typeinfo, + const GenericResponseSentHandler& handler) override { + handlers.put(typeinfo, handler); + } + + std::function getPayload() override { + auto request = reader.read(); + if (request.size() > 0) { + if (auto payload = processMessage(request)) { + return payload; + } + } + return {}; + } + + void connect(const std::shared_ptr& r, + const std::shared_ptr& w) override { + if (isBound.exchange(true)) { + handlers.error("Session::connect called twice"); + return; + } + + reader = dap::ContentReader(r, this->onInvalidData); + writer = dap::ContentWriter(w); + } + + void startProcessingMessages( + const ClosedHandler& onClose /* = {} */) override { + if (isProcessingMessages.exchange(true)) { + handlers.error("Session::startProcessingMessages() called twice"); + return; + } + recvThread = std::thread([this, onClose] { + while (reader.isOpen()) { + if (auto payload = getPayload()) { + inbox.put(std::move(payload)); + } + } + if (onClose) { + onClose(); + } + }); + + dispatchThread = std::thread([this] { + while (auto payload = inbox.take()) { + payload.value()(); + } + }); + } + + bool send(const dap::TypeInfo* requestTypeInfo, + const dap::TypeInfo* responseTypeInfo, + const void* request, + const GenericResponseHandler& responseHandler) override { + int seq = nextSeq++; + + handlers.put(seq, responseTypeInfo, responseHandler); + + dap::json::Serializer s; + if (!s.object([&](dap::FieldSerializer* fs) { + return fs->field("seq", dap::integer(seq)) && + fs->field("type", "request") && + fs->field("command", requestTypeInfo->name()) && + fs->field("arguments", [&](dap::Serializer* s) { + return requestTypeInfo->serialize(s, request); + }); + })) { + return false; + } + return send(s.dump()); + } + + bool send(const dap::TypeInfo* typeinfo, const void* event) override { + dap::json::Serializer s; + if (!s.object([&](dap::FieldSerializer* fs) { + return fs->field("seq", dap::integer(nextSeq++)) && + fs->field("type", "event") && + fs->field("event", typeinfo->name()) && + fs->field("body", [&](dap::Serializer* s) { + return typeinfo->serialize(s, event); + }); + })) { + return false; + } + return send(s.dump()); + } + + ~Impl() override { + inbox.close(); + reader.close(); + writer.close(); + if (recvThread.joinable()) { + recvThread.join(); + } + if (dispatchThread.joinable()) { + dispatchThread.join(); + } + } + + private: + using Payload = std::function; + + class EventHandlers { + public: + void put(const ErrorHandler& handler) { + std::unique_lock lock(errorMutex); + errorHandler = handler; + } + + void error(const char* format, ...) { + va_list vararg; + va_start(vararg, format); + std::unique_lock lock(errorMutex); + errorLocked(format, vararg); + va_end(vararg); + } + + std::pair request( + const std::string& name) { + std::unique_lock lock(requestMutex); + auto it = requestMap.find(name); + return (it != requestMap.end()) ? it->second : decltype(it->second){}; + } + + void put(const dap::TypeInfo* typeinfo, + const GenericRequestHandler& handler) { + std::unique_lock lock(requestMutex); + auto added = + requestMap + .emplace(typeinfo->name(), std::make_pair(typeinfo, handler)) + .second; + if (!added) { + errorfLocked("Request handler for '%s' already registered", + typeinfo->name().c_str()); + } + } + + std::pair response( + int64_t seq) { + std::unique_lock lock(responseMutex); + auto responseIt = responseMap.find(seq); + if (responseIt == responseMap.end()) { + errorfLocked("Unknown response with sequence %d", seq); + return {}; + } + auto out = std::move(responseIt->second); + responseMap.erase(seq); + return out; + } + + void put(int seq, + const dap::TypeInfo* typeinfo, + const GenericResponseHandler& handler) { + std::unique_lock lock(responseMutex); + auto added = + responseMap.emplace(seq, std::make_pair(typeinfo, handler)).second; + if (!added) { + errorfLocked("Response handler for sequence %d already registered", + seq); + } + } + + std::pair event( + const std::string& name) { + std::unique_lock lock(eventMutex); + auto it = eventMap.find(name); + return (it != eventMap.end()) ? it->second : decltype(it->second){}; + } + + void put(const dap::TypeInfo* typeinfo, + const GenericEventHandler& handler) { + std::unique_lock lock(eventMutex); + auto added = + eventMap.emplace(typeinfo->name(), std::make_pair(typeinfo, handler)) + .second; + if (!added) { + errorfLocked("Event handler for '%s' already registered", + typeinfo->name().c_str()); + } + } + + GenericResponseSentHandler responseSent(const dap::TypeInfo* typeinfo) { + std::unique_lock lock(responseSentMutex); + auto it = responseSentMap.find(typeinfo); + return (it != responseSentMap.end()) ? it->second + : decltype(it->second){}; + } + + void put(const dap::TypeInfo* typeinfo, + const GenericResponseSentHandler& handler) { + std::unique_lock lock(responseSentMutex); + auto added = responseSentMap.emplace(typeinfo, handler).second; + if (!added) { + errorfLocked("Response sent handler for '%s' already registered", + typeinfo->name().c_str()); + } + } + + private: + void errorfLocked(const char* format, ...) { + va_list vararg; + va_start(vararg, format); + errorLocked(format, vararg); + va_end(vararg); + } + + void errorLocked(const char* format, va_list args) { + char buf[2048]; + vsnprintf(buf, sizeof(buf), format, args); + if (errorHandler) { + errorHandler(buf); + } + } + + std::mutex errorMutex; + ErrorHandler errorHandler; + + std::mutex requestMutex; + std::unordered_map> + requestMap; + + std::mutex responseMutex; + std::unordered_map> + responseMap; + + std::mutex eventMutex; + std::unordered_map> + eventMap; + + std::mutex responseSentMutex; + std::unordered_map + responseSentMap; + }; // EventHandlers + + Payload processMessage(const std::string& str) { + auto d = dap::json::Deserializer(str); + dap::string type; + if (!d.field("type", &type)) { + handlers.error("Message missing string 'type' field"); + return {}; + } + + dap::integer sequence = 0; + if (!d.field("seq", &sequence)) { + handlers.error("Message missing number 'seq' field"); + return {}; + } + + if (type == "request") { + return processRequest(&d, sequence); + } else if (type == "event") { + return processEvent(&d); + } else if (type == "response") { + processResponse(&d); + return {}; + } else { + handlers.error("Unknown message type '%s'", type.c_str()); + } + + return {}; + } + + Payload processRequest(dap::json::Deserializer* d, dap::integer sequence) { + dap::string command; + if (!d->field("command", &command)) { + handlers.error("Request missing string 'command' field"); + return {}; + } + + const dap::TypeInfo* typeinfo; + GenericRequestHandler handler; + std::tie(typeinfo, handler) = handlers.request(command); + if (!typeinfo) { + handlers.error("No request handler registered for command '%s'", + command.c_str()); + return {}; + } + + auto data = new uint8_t[typeinfo->size()]; + typeinfo->construct(data); + + if (!d->field("arguments", [&](dap::Deserializer* d) { + return typeinfo->deserialize(d, data); + })) { + handlers.error("Failed to deserialize request"); + typeinfo->destruct(data); + delete[] data; + return {}; + } + + return [=] { + handler( + data, + [=](const dap::TypeInfo* typeinfo, const void* data) { + // onSuccess + dap::json::Serializer s; + s.object([&](dap::FieldSerializer* fs) { + return fs->field("seq", dap::integer(nextSeq++)) && + fs->field("type", "response") && + fs->field("request_seq", sequence) && + fs->field("success", dap::boolean(true)) && + fs->field("command", command) && + fs->field("body", [&](dap::Serializer* s) { + return typeinfo->serialize(s, data); + }); + }); + send(s.dump()); + + if (auto handler = handlers.responseSent(typeinfo)) { + handler(data, nullptr); + } + }, + [=](const dap::TypeInfo* typeinfo, const dap::Error& error) { + // onError + dap::json::Serializer s; + s.object([&](dap::FieldSerializer* fs) { + return fs->field("seq", dap::integer(nextSeq++)) && + fs->field("type", "response") && + fs->field("request_seq", sequence) && + fs->field("success", dap::boolean(false)) && + fs->field("command", command) && + fs->field("message", error.message); + }); + send(s.dump()); + + if (auto handler = handlers.responseSent(typeinfo)) { + handler(nullptr, &error); + } + }); + typeinfo->destruct(data); + delete[] data; + }; + } + + Payload processEvent(dap::json::Deserializer* d) { + dap::string event; + if (!d->field("event", &event)) { + handlers.error("Event missing string 'event' field"); + return {}; + } + + const dap::TypeInfo* typeinfo; + GenericEventHandler handler; + std::tie(typeinfo, handler) = handlers.event(event); + if (!typeinfo) { + handlers.error("No event handler registered for event '%s'", + event.c_str()); + return {}; + } + + auto data = new uint8_t[typeinfo->size()]; + typeinfo->construct(data); + + // "body" is an optional field for some events, such as "Terminated Event". + bool body_ok = true; + d->field("body", [&](dap::Deserializer* d) { + if (!typeinfo->deserialize(d, data)) { + body_ok = false; + } + return true; + }); + + if (!body_ok) { + handlers.error("Failed to deserialize event '%s' body", event.c_str()); + typeinfo->destruct(data); + delete[] data; + return {}; + } + + return [=] { + handler(data); + typeinfo->destruct(data); + delete[] data; + }; + } + + void processResponse(const dap::Deserializer* d) { + dap::integer requestSeq = 0; + if (!d->field("request_seq", &requestSeq)) { + handlers.error("Response missing int 'request_seq' field"); + return; + } + + const dap::TypeInfo* typeinfo; + GenericResponseHandler handler; + std::tie(typeinfo, handler) = handlers.response(requestSeq); + if (!typeinfo) { + handlers.error("Unknown response with sequence %d", requestSeq); + return; + } + + dap::boolean success = false; + if (!d->field("success", &success)) { + handlers.error("Response missing int 'success' field"); + return; + } + + if (success) { + auto data = std::unique_ptr(new uint8_t[typeinfo->size()]); + typeinfo->construct(data.get()); + + // "body" field in Response is an optional field. + d->field("body", [&](const dap::Deserializer* d) { + return typeinfo->deserialize(d, data.get()); + }); + + handler(data.get(), nullptr); + typeinfo->destruct(data.get()); + } else { + std::string message; + if (!d->field("message", &message)) { + handlers.error("Failed to deserialize message"); + return; + } + auto error = dap::Error("%s", message.c_str()); + handler(nullptr, &error); + } + } + + bool send(const std::string& s) { + std::unique_lock lock(sendMutex); + if (!writer.isOpen()) { + handlers.error("Send failed as the writer is closed"); + return false; + } + return writer.write(s); + } + + std::atomic isBound = {false}; + std::atomic isProcessingMessages = {false}; + dap::ContentReader reader; + dap::ContentWriter writer; + + std::atomic shutdown = {false}; + EventHandlers handlers; + std::thread recvThread; + std::thread dispatchThread; + dap::Chan inbox; + std::atomic nextSeq = {1}; + std::mutex sendMutex; + dap::OnInvalidData onInvalidData = dap::kIgnore; +}; + +} // anonymous namespace + +namespace dap { + +Error::Error(const std::string& message) : message(message) {} + +Error::Error(const char* msg, ...) { + char buf[2048]; + va_list vararg; + va_start(vararg, msg); + vsnprintf(buf, sizeof(buf), msg, vararg); + va_end(vararg); + message = buf; +} + +Session::~Session() = default; + +std::unique_ptr Session::create() { + return std::unique_ptr(new Impl()); +} + +} // namespace dap diff --git a/libraries/cppdap/src/session_test.cpp b/libraries/cppdap/src/session_test.cpp new file mode 100644 index 000000000..361152e74 --- /dev/null +++ b/libraries/cppdap/src/session_test.cpp @@ -0,0 +1,625 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/session.h" +#include "dap/io.h" +#include "dap/protocol.h" + +#include "chan.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace dap { + +struct TestResponse : public Response { + boolean b; + integer i; + number n; + array a; + object o; + string s; + optional o1; + optional o2; +}; + +DAP_STRUCT_TYPEINFO(TestResponse, + "test-response", + DAP_FIELD(b, "res_b"), + DAP_FIELD(i, "res_i"), + DAP_FIELD(n, "res_n"), + DAP_FIELD(a, "res_a"), + DAP_FIELD(o, "res_o"), + DAP_FIELD(s, "res_s"), + DAP_FIELD(o1, "res_o1"), + DAP_FIELD(o2, "res_o2")); + +struct TestRequest : public Request { + using Response = TestResponse; + + boolean b; + integer i; + number n; + array a; + object o; + string s; + optional o1; + optional o2; +}; + +DAP_STRUCT_TYPEINFO(TestRequest, + "test-request", + DAP_FIELD(b, "req_b"), + DAP_FIELD(i, "req_i"), + DAP_FIELD(n, "req_n"), + DAP_FIELD(a, "req_a"), + DAP_FIELD(o, "req_o"), + DAP_FIELD(s, "req_s"), + DAP_FIELD(o1, "req_o1"), + DAP_FIELD(o2, "req_o2")); + +struct TestEvent : public Event { + boolean b; + integer i; + number n; + array a; + object o; + string s; + optional o1; + optional o2; +}; + +DAP_STRUCT_TYPEINFO(TestEvent, + "test-event", + DAP_FIELD(b, "evt_b"), + DAP_FIELD(i, "evt_i"), + DAP_FIELD(n, "evt_n"), + DAP_FIELD(a, "evt_a"), + DAP_FIELD(o, "evt_o"), + DAP_FIELD(s, "evt_s"), + DAP_FIELD(o1, "evt_o1"), + DAP_FIELD(o2, "evt_o2")); + +}; // namespace dap + +namespace { + +dap::TestRequest createRequest() { + dap::TestRequest request; + request.b = false; + request.i = 72; + request.n = 9.87; + request.a = {2, 5, 7, 8}; + request.o = { + std::make_pair("a", dap::integer(1)), + std::make_pair("b", dap::number(2)), + std::make_pair("c", dap::string("3")), + }; + request.s = "request"; + request.o2 = 42; + return request; +} + +dap::TestResponse createResponse() { + dap::TestResponse response; + response.b = true; + response.i = 99; + response.n = 123.456; + response.a = {5, 4, 3, 2, 1}; + response.o = { + std::make_pair("one", dap::integer(1)), + std::make_pair("two", dap::number(2)), + std::make_pair("three", dap::string("3")), + }; + response.s = "ROGER"; + response.o1 = 50; + return response; +} + +dap::TestEvent createEvent() { + dap::TestEvent event; + event.b = false; + event.i = 72; + event.n = 9.87; + event.a = {2, 5, 7, 8}; + event.o = { + std::make_pair("a", dap::integer(1)), + std::make_pair("b", dap::number(2)), + std::make_pair("c", dap::string("3")), + }; + event.s = "event"; + event.o2 = 42; + return event; +} + +} // anonymous namespace + +class SessionTest : public testing::Test { + public: + void bind() { + auto client2server = dap::pipe(); + auto server2client = dap::pipe(); + client->bind(server2client, client2server); + server->bind(client2server, server2client); + } + + std::unique_ptr client = dap::Session::create(); + std::unique_ptr server = dap::Session::create(); +}; + +TEST_F(SessionTest, Request) { + dap::TestRequest received; + server->registerHandler([&](const dap::TestRequest& req) { + received = req; + return createResponse(); + }); + + bind(); + + auto request = createRequest(); + client->send(request).get(); + + // Check request was received correctly. + ASSERT_EQ(received.b, request.b); + ASSERT_EQ(received.i, request.i); + ASSERT_EQ(received.n, request.n); + ASSERT_EQ(received.a, request.a); + ASSERT_EQ(received.o.size(), 3U); + ASSERT_EQ(received.o["a"].get(), + request.o["a"].get()); + ASSERT_EQ(received.o["b"].get(), + request.o["b"].get()); + ASSERT_EQ(received.o["c"].get(), + request.o["c"].get()); + ASSERT_EQ(received.s, request.s); + ASSERT_EQ(received.o1, request.o1); + ASSERT_EQ(received.o2, request.o2); +} + +TEST_F(SessionTest, RequestResponseSuccess) { + server->registerHandler( + [&](const dap::TestRequest&) { return createResponse(); }); + + bind(); + + auto request = createRequest(); + auto response = client->send(request); + + auto got = response.get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.b, dap::boolean(true)); + ASSERT_EQ(got.response.i, dap::integer(99)); + ASSERT_EQ(got.response.n, dap::number(123.456)); + ASSERT_EQ(got.response.a, dap::array({5, 4, 3, 2, 1})); + ASSERT_EQ(got.response.o.size(), 3U); + ASSERT_EQ(got.response.o["one"].get(), dap::integer(1)); + ASSERT_EQ(got.response.o["two"].get(), dap::number(2)); + ASSERT_EQ(got.response.o["three"].get(), dap::string("3")); + ASSERT_EQ(got.response.s, "ROGER"); + ASSERT_EQ(got.response.o1, dap::optional(50)); + ASSERT_FALSE(got.response.o2.has_value()); +} + +TEST_F(SessionTest, BreakPointRequestResponseSuccess) { + server->registerHandler([&](const dap::SetBreakpointsRequest&) { + dap::SetBreakpointsResponse response; + dap::Breakpoint bp; + bp.line = 2; + response.breakpoints.emplace_back(std::move(bp)); + return response; + }); + + bind(); + + auto got = client->send(dap::SetBreakpointsRequest{}).get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.breakpoints.size(), 1U); +} + +TEST_F(SessionTest, RequestResponseOrError) { + server->registerHandler( + [&](const dap::TestRequest&) -> dap::ResponseOrError { + return dap::Error("Oh noes!"); + }); + + bind(); + + auto response = client->send(createRequest()); + + auto got = response.get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, true); + ASSERT_EQ(got.error.message, "Oh noes!"); +} + +TEST_F(SessionTest, RequestResponseError) { + server->registerHandler( + [&](const dap::TestRequest&) { return dap::Error("Oh noes!"); }); + + bind(); + + auto response = client->send(createRequest()); + + auto got = response.get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, true); + ASSERT_EQ(got.error.message, "Oh noes!"); +} + +TEST_F(SessionTest, RequestCallbackResponse) { + using ResponseCallback = std::function; + + server->registerHandler( + [&](const dap::SetBreakpointsRequest&, const ResponseCallback& callback) { + dap::SetBreakpointsResponse response; + dap::Breakpoint bp; + bp.line = 2; + response.breakpoints.emplace_back(std::move(bp)); + callback(response); + }); + + bind(); + + auto got = client->send(dap::SetBreakpointsRequest{}).get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.breakpoints.size(), 1U); +} + +TEST_F(SessionTest, RequestCallbackResponseOrError) { + using ResponseCallback = + std::function)>; + + server->registerHandler( + [&](const dap::SetBreakpointsRequest&, const ResponseCallback& callback) { + dap::SetBreakpointsResponse response; + dap::Breakpoint bp; + bp.line = 2; + response.breakpoints.emplace_back(std::move(bp)); + callback(response); + }); + + bind(); + + auto got = client->send(dap::SetBreakpointsRequest{}).get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.breakpoints.size(), 1U); +} + +TEST_F(SessionTest, RequestCallbackError) { + using ResponseCallback = + std::function)>; + + server->registerHandler( + [&](const dap::SetBreakpointsRequest&, const ResponseCallback& callback) { + callback(dap::Error("Oh noes!")); + }); + + bind(); + + auto got = client->send(dap::SetBreakpointsRequest{}).get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, true); + ASSERT_EQ(got.error.message, "Oh noes!"); +} + +TEST_F(SessionTest, RequestCallbackSuccessAfterReturn) { + using ResponseCallback = + std::function)>; + + ResponseCallback callback; + std::mutex mutex; + std::condition_variable cv; + + server->registerHandler( + [&](const dap::SetBreakpointsRequest&, const ResponseCallback& cb) { + std::unique_lock lock(mutex); + callback = cb; + cv.notify_all(); + }); + + bind(); + + auto future = client->send(dap::SetBreakpointsRequest{}); + + { + dap::SetBreakpointsResponse response; + dap::Breakpoint bp; + bp.line = 2; + response.breakpoints.emplace_back(std::move(bp)); + + // Wait for the handler to be called. + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return static_cast(callback); }); + + // Issue the callback + callback(response); + } + + auto got = future.get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.breakpoints.size(), 1U); +} + +TEST_F(SessionTest, RequestCallbackErrorAfterReturn) { + using ResponseCallback = + std::function)>; + + ResponseCallback callback; + std::mutex mutex; + std::condition_variable cv; + + server->registerHandler( + [&](const dap::SetBreakpointsRequest&, const ResponseCallback& cb) { + std::unique_lock lock(mutex); + callback = cb; + cv.notify_all(); + }); + + bind(); + + auto future = client->send(dap::SetBreakpointsRequest{}); + + { + // Wait for the handler to be called. + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return static_cast(callback); }); + + // Issue the callback + callback(dap::Error("Oh noes!")); + } + + auto got = future.get(); + + // Check response was received correctly. + ASSERT_EQ(got.error, true); + ASSERT_EQ(got.error.message, "Oh noes!"); +} + +TEST_F(SessionTest, ResponseSentHandlerSuccess) { + const auto response = createResponse(); + + dap::Chan> chan; + server->registerHandler([&](const dap::TestRequest&) { return response; }); + server->registerSentHandler( + [&](const dap::ResponseOrError r) { chan.put(r); }); + + bind(); + + client->send(createRequest()); + + auto got = chan.take().value(); + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.b, dap::boolean(true)); + ASSERT_EQ(got.response.i, dap::integer(99)); + ASSERT_EQ(got.response.n, dap::number(123.456)); + ASSERT_EQ(got.response.a, dap::array({5, 4, 3, 2, 1})); + ASSERT_EQ(got.response.o.size(), 3U); + ASSERT_EQ(got.response.o["one"].get(), dap::integer(1)); + ASSERT_EQ(got.response.o["two"].get(), dap::number(2)); + ASSERT_EQ(got.response.o["three"].get(), dap::string("3")); + ASSERT_EQ(got.response.s, "ROGER"); + ASSERT_EQ(got.response.o1, dap::optional(50)); + ASSERT_FALSE(got.response.o2.has_value()); +} + +TEST_F(SessionTest, ResponseSentHandlerError) { + dap::Chan> chan; + server->registerHandler( + [&](const dap::TestRequest&) { return dap::Error("Oh noes!"); }); + server->registerSentHandler( + [&](const dap::ResponseOrError r) { chan.put(r); }); + + bind(); + + client->send(createRequest()); + + auto got = chan.take().value(); + ASSERT_EQ(got.error, true); + ASSERT_EQ(got.error.message, "Oh noes!"); +} + +TEST_F(SessionTest, Event) { + dap::Chan received; + server->registerHandler([&](const dap::TestEvent& e) { received.put(e); }); + + bind(); + + auto event = createEvent(); + client->send(event); + + // Check event was received correctly. + auto got = received.take().value(); + + ASSERT_EQ(got.b, event.b); + ASSERT_EQ(got.i, event.i); + ASSERT_EQ(got.n, event.n); + ASSERT_EQ(got.a, event.a); + ASSERT_EQ(got.o.size(), 3U); + ASSERT_EQ(got.o["a"].get(), event.o["a"].get()); + ASSERT_EQ(got.o["b"].get(), event.o["b"].get()); + ASSERT_EQ(got.o["c"].get(), event.o["c"].get()); + ASSERT_EQ(got.s, event.s); + ASSERT_EQ(got.o1, event.o1); + ASSERT_EQ(got.o2, event.o2); +} + +TEST_F(SessionTest, RegisterHandlerFunction) { + struct S { + static dap::TestResponse requestA(const dap::TestRequest&) { return {}; } + static dap::Error requestB(const dap::TestRequest&) { return {}; } + static dap::ResponseOrError requestC( + const dap::TestRequest&) { + return dap::Error(); + } + static void event(const dap::TestEvent&) {} + static void sent(const dap::ResponseOrError&) {} + }; + client->registerHandler(&S::requestA); + client->registerHandler(&S::requestB); + client->registerHandler(&S::requestC); + client->registerHandler(&S::event); + client->registerSentHandler(&S::sent); +} + +TEST_F(SessionTest, SendRequestNoBind) { + bool errored = false; + client->onError([&](const std::string&) { errored = true; }); + auto res = client->send(createRequest()).get(); + ASSERT_TRUE(errored); + ASSERT_TRUE(res.error); +} + +TEST_F(SessionTest, SendEventNoBind) { + bool errored = false; + client->onError([&](const std::string&) { errored = true; }); + client->send(createEvent()); + ASSERT_TRUE(errored); +} + +TEST_F(SessionTest, SingleThread) { + server->registerHandler( + [&](const dap::TestRequest&) { return createResponse(); }); + + // Explicitly connect and process request on this test thread instead of + // calling bind() which inturn starts processing messages immediately on a new + // thread. + auto client2server = dap::pipe(); + auto server2client = dap::pipe(); + client->connect(server2client, client2server); + server->connect(client2server, server2client); + + auto request = createRequest(); + auto response = client->send(request); + + // Process request and response on this thread + if (auto payload = server->getPayload()) { + payload(); + } + if (auto payload = client->getPayload()) { + payload(); + } + + auto got = response.get(); + // Check response was received correctly. + ASSERT_EQ(got.error, false); + ASSERT_EQ(got.response.b, dap::boolean(true)); + ASSERT_EQ(got.response.i, dap::integer(99)); + ASSERT_EQ(got.response.n, dap::number(123.456)); + ASSERT_EQ(got.response.a, dap::array({5, 4, 3, 2, 1})); + ASSERT_EQ(got.response.o.size(), 3U); + ASSERT_EQ(got.response.o["one"].get(), dap::integer(1)); + ASSERT_EQ(got.response.o["two"].get(), dap::number(2)); + ASSERT_EQ(got.response.o["three"].get(), dap::string("3")); + ASSERT_EQ(got.response.s, "ROGER"); + ASSERT_EQ(got.response.o1, dap::optional(50)); + ASSERT_FALSE(got.response.o2.has_value()); +} + +TEST_F(SessionTest, Concurrency) { + std::atomic numEventsHandled = {0}; + std::atomic done = {false}; + + server->registerHandler( + [](const dap::TestRequest&) { return dap::TestResponse(); }); + + server->registerHandler([&](const dap::TestEvent&) { + if (numEventsHandled++ > 10000) { + done = true; + } + }); + + bind(); + + constexpr int numThreads = 32; + std::array threads; + + for (int i = 0; i < numThreads; i++) { + threads[i] = std::thread([&] { + while (!done) { + client->send(createEvent()); + client->send(createRequest()); + } + }); + } + + for (int i = 0; i < numThreads; i++) { + threads[i].join(); + } + + client.reset(); + server.reset(); +} + +TEST_F(SessionTest, OnClientClosed) { + std::mutex mutex; + std::condition_variable cv; + bool clientClosed = false; + + auto client2server = dap::pipe(); + auto server2client = dap::pipe(); + + client->bind(server2client, client2server); + server->bind(client2server, server2client, [&] { + std::unique_lock lock(mutex); + clientClosed = true; + cv.notify_all(); + }); + + client.reset(); + + // Wait for the client closed handler to be called. + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return static_cast(clientClosed); }); +} + +TEST_F(SessionTest, OnServerClosed) { + std::mutex mutex; + std::condition_variable cv; + bool serverClosed = false; + + auto client2server = dap::pipe(); + auto server2client = dap::pipe(); + + client->bind(server2client, client2server, [&] { + std::unique_lock lock(mutex); + serverClosed = true; + cv.notify_all(); + }); + server->bind(client2server, server2client); + + server.reset(); + + // Wait for the client closed handler to be called. + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return static_cast(serverClosed); }); +} diff --git a/libraries/cppdap/src/socket.cpp b/libraries/cppdap/src/socket.cpp new file mode 100644 index 000000000..ad5691d34 --- /dev/null +++ b/libraries/cppdap/src/socket.cpp @@ -0,0 +1,337 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "socket.h" + +#include "rwmutex.h" + +#if defined(_WIN32) +#include +#include +#else +#include +#include +#include +#include +#include +#include +#endif + +#if defined(_WIN32) +#include +namespace { +std::atomic wsaInitCount = {0}; +} // anonymous namespace +#else +#include +#include +namespace { +using SOCKET = int; +} // anonymous namespace +#endif + +namespace { +constexpr SOCKET InvalidSocket = static_cast(-1); +void init() { +#if defined(_WIN32) + if (wsaInitCount++ == 0) { + WSADATA winsockData; + (void)WSAStartup(MAKEWORD(2, 2), &winsockData); + } +#endif +} + +void term() { +#if defined(_WIN32) + if (--wsaInitCount == 0) { + WSACleanup(); + } +#endif +} + +bool setBlocking(SOCKET s, bool blocking) { +#if defined(_WIN32) + u_long mode = blocking ? 0 : 1; + return ioctlsocket(s, FIONBIO, &mode) == NO_ERROR; +#else + auto arg = fcntl(s, F_GETFL, nullptr); + if (arg < 0) { + return false; + } + arg = blocking ? (arg & ~O_NONBLOCK) : (arg | O_NONBLOCK); + return fcntl(s, F_SETFL, arg) >= 0; +#endif +} + +bool errored(SOCKET s) { + if (s == InvalidSocket) { + return true; + } + char error = 0; + socklen_t len = sizeof(error); + getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len); + return error != 0; +} + +} // anonymous namespace + +class dap::Socket::Shared : public dap::ReaderWriter { + public: + static std::shared_ptr create(const char* address, const char* port) { + init(); + + addrinfo hints = {}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + addrinfo* info = nullptr; + getaddrinfo(address, port, &hints, &info); + + if (info) { + auto socket = + ::socket(info->ai_family, info->ai_socktype, info->ai_protocol); + auto out = std::make_shared(info, socket); + out->setOptions(); + return out; + } + + term(); + return nullptr; + } + + Shared(SOCKET socket) : info(nullptr), s(socket) {} + Shared(addrinfo* info, SOCKET socket) : info(info), s(socket) {} + + ~Shared() { + if (info) { + freeaddrinfo(info); + } + close(); + term(); + } + + template + void lock(FUNCTION&& f) { + RLock l(mutex); + f(s, info); + } + + void setOptions() { + RLock l(mutex); + if (s == InvalidSocket) { + return; + } + + int enable = 1; + +#if !defined(_WIN32) + // Prevent sockets lingering after process termination, causing + // reconnection issues on the same port. + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(enable)); + + struct { + int l_onoff; /* linger active */ + int l_linger; /* how many seconds to linger for */ + } linger = {false, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, (char*)&linger, sizeof(linger)); +#endif // !defined(_WIN32) + + // Enable TCP_NODELAY. + // DAP usually consists of small packet requests, with small packet + // responses. When there are many frequent, blocking requests made, + // Nagle's algorithm can dramatically limit the request->response rates. + setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (char*)&enable, sizeof(enable)); + } + + // dap::ReaderWriter compliance + bool isOpen() { + { + RLock l(mutex); + if ((s != InvalidSocket) && !errored(s)) { + return true; + } + } + WLock lock(mutex); + s = InvalidSocket; + return false; + } + + void close() { + { + RLock l(mutex); + if (s != InvalidSocket) { +#if defined(_WIN32) + closesocket(s); +#elif __APPLE__ + // ::shutdown() *should* be sufficient to unblock ::accept(), but + // apparently on macos it can return ENOTCONN and ::accept() continues + // to block indefinitely. + // Note: There is a race here. Calling ::close() frees the socket ID, + // which may be reused before `s` is assigned InvalidSocket. + ::shutdown(s, SHUT_RDWR); + ::close(s); +#else + // ::shutdown() to unblock ::accept(). We'll actually close the socket + // under lock below. + ::shutdown(s, SHUT_RDWR); +#endif + } + } + + WLock l(mutex); + if (s != InvalidSocket) { +#if !defined(_WIN32) && !defined(__APPLE__) + ::close(s); +#endif + s = InvalidSocket; + } + } + + size_t read(void* buffer, size_t bytes) { + RLock lock(mutex); + if (s == InvalidSocket) { + return 0; + } + auto len = + recv(s, reinterpret_cast(buffer), static_cast(bytes), 0); + return (len < 0) ? 0 : len; + } + + bool write(const void* buffer, size_t bytes) { + RLock lock(mutex); + if (s == InvalidSocket) { + return false; + } + if (bytes == 0) { + return true; + } + return ::send(s, reinterpret_cast(buffer), + static_cast(bytes), 0) > 0; + } + + private: + addrinfo* const info; + SOCKET s = InvalidSocket; + RWMutex mutex; +}; + +namespace dap { + +Socket::Socket(const char* address, const char* port) + : shared(Shared::create(address, port)) { + if (shared) { + bool reset = false; + shared->lock([&](SOCKET socket, const addrinfo* info) { + if (bind(socket, info->ai_addr, (int)info->ai_addrlen) != 0) { + reset = true; + return; + } + + if (listen(socket, 0) != 0) { + reset = true; + } + }); + if (reset) { + shared.reset(); + } + } +} + +std::shared_ptr Socket::accept() const { + std::shared_ptr out; + if (shared) { + shared->lock([&](SOCKET socket, const addrinfo*) { + if (socket != InvalidSocket && !errored(socket)) { + init(); + auto accepted = ::accept(socket, 0, 0); + if (accepted != InvalidSocket) { + out = std::make_shared(accepted); + out->setOptions(); + } + } + }); + } + return out; +} + +bool Socket::isOpen() const { + if (shared) { + return shared->isOpen(); + } + return false; +} + +void Socket::close() const { + if (shared) { + shared->close(); + } +} + +std::shared_ptr Socket::connect(const char* address, + const char* port, + uint32_t timeoutMillis) { + auto shared = Shared::create(address, port); + if (!shared) { + return nullptr; + } + + std::shared_ptr out; + shared->lock([&](SOCKET socket, const addrinfo* info) { + if (socket == InvalidSocket) { + return; + } + + if (timeoutMillis == 0) { + if (::connect(socket, info->ai_addr, (int)info->ai_addrlen) == 0) { + out = shared; + } + return; + } + + if (!setBlocking(socket, false)) { + return; + } + + auto res = ::connect(socket, info->ai_addr, (int)info->ai_addrlen); + if (res == 0) { + if (setBlocking(socket, true)) { + out = shared; + } + } else { + const auto microseconds = timeoutMillis * 1000; + + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(socket, &fdset); + + timeval tv; + tv.tv_sec = microseconds / 1000000; + tv.tv_usec = microseconds - static_cast(tv.tv_sec * 1000000); + res = select(static_cast(socket + 1), nullptr, &fdset, nullptr, &tv); + if (res > 0 && !errored(socket) && setBlocking(socket, true)) { + out = shared; + } + } + }); + + if (!out) { + return nullptr; + } + + return out->isOpen() ? out : nullptr; +} + +} // namespace dap diff --git a/libraries/cppdap/src/socket.h b/libraries/cppdap/src/socket.h new file mode 100644 index 000000000..ec5b0dfbd --- /dev/null +++ b/libraries/cppdap/src/socket.h @@ -0,0 +1,47 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_socket_h +#define dap_socket_h + +#include "dap/io.h" + +#include +#include + +namespace dap { + +class Socket { + public: + class Shared; + + // connect() connects to the given TCP address and port. + // If timeoutMillis is non-zero and no connection was made before + // timeoutMillis milliseconds, then nullptr is returned. + static std::shared_ptr connect(const char* address, + const char* port, + uint32_t timeoutMillis); + + Socket(const char* address, const char* port); + bool isOpen() const; + std::shared_ptr accept() const; + void close() const; + + private: + std::shared_ptr shared; +}; + +} // namespace dap + +#endif // dap_socket_h diff --git a/libraries/cppdap/src/socket_test.cpp b/libraries/cppdap/src/socket_test.cpp new file mode 100644 index 000000000..186fd9a38 --- /dev/null +++ b/libraries/cppdap/src/socket_test.cpp @@ -0,0 +1,104 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "socket.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include +#include + +// Basic socket send & receive test +TEST(Socket, SendRecv) { + const char* port = "19021"; + + auto server = dap::Socket("localhost", port); + + auto client = dap::Socket::connect("localhost", port, 0); + ASSERT_TRUE(client != nullptr); + + const std::string expect = "Hello World!"; + std::string read; + + auto thread = std::thread([&] { + auto conn = server.accept(); + ASSERT_TRUE(conn != nullptr); + char c; + while (conn->read(&c, 1) != 0) { + read += c; + } + }); + + ASSERT_TRUE(client->write(expect.data(), expect.size())); + + client->close(); + thread.join(); + + ASSERT_EQ(read, expect); +} + +// See https://github.com/google/cppdap/issues/37 +TEST(Socket, CloseOnDifferentThread) { + const char* port = "19021"; + + auto server = dap::Socket("localhost", port); + + auto client = dap::Socket::connect("localhost", port, 0); + ASSERT_TRUE(client != nullptr); + + auto conn = server.accept(); + + auto thread = std::thread([&] { + // Closing client on different thread should unblock client->read(). + client->close(); + }); + + char c; + while (client->read(&c, 1) != 0) { + } + + thread.join(); +} + +TEST(Socket, ConnectTimeout) { + const char* port = "19021"; + const int timeoutMillis = 200; + const int maxAttempts = 1024; + + using namespace std::chrono; + + auto server = dap::Socket("localhost", port); + + std::vector> connections; + + for (int i = 0; i < maxAttempts; i++) { + auto start = system_clock::now(); + auto connection = dap::Socket::connect("localhost", port, timeoutMillis); + auto end = system_clock::now(); + + if (!connection) { + auto timeTakenMillis = duration_cast(end - start).count(); + ASSERT_GE(timeTakenMillis + 20, // +20ms for a bit of timing wiggle room + timeoutMillis); + return; + } + + // Keep hold of the connections to saturate any incoming socket buffers. + connections.emplace_back(std::move(connection)); + } + + FAIL() << "Failed to test timeout after " << maxAttempts << " attempts"; +} diff --git a/libraries/cppdap/src/string_buffer.h b/libraries/cppdap/src/string_buffer.h new file mode 100644 index 000000000..1c381971a --- /dev/null +++ b/libraries/cppdap/src/string_buffer.h @@ -0,0 +1,95 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_string_buffer_h +#define dap_string_buffer_h + +#include "dap/io.h" + +#include // std::min +#include // memcpy +#include +#include // std::unique_ptr +#include + +namespace dap { + +class StringBuffer : public virtual Reader, public virtual Writer { + public: + static inline std::unique_ptr create(); + + inline bool write(const std::string& s); + inline std::string string() const; + + // Reader / Writer compilance + inline bool isOpen() override; + inline void close() override; + inline size_t read(void* buffer, size_t bytes) override; + inline bool write(const void* buffer, size_t bytes) override; + + private: + std::string str; + std::deque chunk_lengths; + bool closed = false; +}; + +bool StringBuffer::isOpen() { + return !closed; +} +void StringBuffer::close() { + closed = true; +} + +std::unique_ptr StringBuffer::create() { + return std::unique_ptr(new StringBuffer()); +} + +bool StringBuffer::write(const std::string& s) { + return write(s.data(), s.size()); +} + +std::string StringBuffer::string() const { + return str; +} + +size_t StringBuffer::read(void* buffer, size_t bytes) { + if (closed || bytes == 0 || str.size() == 0 || chunk_lengths.size() == 0) { + return 0; + } + size_t& chunk_length = chunk_lengths.front(); + + auto len = std::min(bytes, chunk_length); + memcpy(buffer, str.data(), len); + str = std::string(str.begin() + len, str.end()); + if (bytes < chunk_length) { + chunk_length -= bytes; + } else { + chunk_lengths.pop_front(); + } + return len; +} + +bool StringBuffer::write(const void* buffer, size_t bytes) { + if (closed) { + return false; + } + auto chars = reinterpret_cast(buffer); + str.append(chars, chars + bytes); + chunk_lengths.push_back(bytes); + return true; +} + +} // namespace dap + +#endif // dap_string_buffer_h diff --git a/libraries/cppdap/src/traits_test.cpp b/libraries/cppdap/src/traits_test.cpp new file mode 100644 index 000000000..aafca04e1 --- /dev/null +++ b/libraries/cppdap/src/traits_test.cpp @@ -0,0 +1,387 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/traits.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace dap { +namespace traits { + +namespace { +struct S {}; +struct E : S {}; +void F1(S) {} +void F3(int, S, float) {} +void E1(E) {} +void E3(int, E, float) {} +} // namespace + +TEST(ParameterType, Function) { + F1({}); // Avoid unused method warning + F3(0, {}, 0); // Avoid unused method warning + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, int>::value, ""); + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, float>::value, + ""); +} + +TEST(ParameterType, Method) { + class C { + public: + void F1(S) {} + void F3(int, S, float) {} + }; + C().F1({}); // Avoid unused method warning + C().F3(0, {}, 0); // Avoid unused method warning + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, int>::value, + ""); + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, float>::value, + ""); +} + +TEST(ParameterType, ConstMethod) { + class C { + public: + void F1(S) const {} + void F3(int, S, float) const {} + }; + C().F1({}); // Avoid unused method warning + C().F3(0, {}, 0); // Avoid unused method warning + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, int>::value, + ""); + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, float>::value, + ""); +} + +TEST(ParameterType, StaticMethod) { + class C { + public: + static void F1(S) {} + static void F3(int, S, float) {} + }; + C::F1({}); // Avoid unused method warning + C::F3(0, {}, 0); // Avoid unused method warning + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, int>::value, + ""); + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, float>::value, + ""); +} + +TEST(ParameterType, FunctionLike) { + using F1 = std::function; + using F3 = std::function; + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, int>::value, ""); + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, float>::value, ""); +} + +TEST(ParameterType, Lambda) { + auto l1 = [](S) {}; + auto l3 = [](int, S, float) {}; + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, int>::value, ""); + static_assert(std::is_same, S>::value, ""); + static_assert(std::is_same, float>::value, ""); +} + +TEST(HasSignature, Function) { + F1({}); // Avoid unused method warning + F3(0, {}, 0); // Avoid unused method warning + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); +} + +TEST(HasSignature, Method) { + class C { + public: + void F1(S) {} + void F3(int, S, float) {} + }; + C().F1({}); // Avoid unused method warning + C().F3(0, {}, 0); // Avoid unused method warning + + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); +} + +TEST(HasSignature, ConstMethod) { + class C { + public: + void F1(S) const {} + void F3(int, S, float) const {} + }; + C().F1({}); // Avoid unused method warning + C().F3(0, {}, 0); // Avoid unused method warning + + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); +} + +TEST(HasSignature, StaticMethod) { + class C { + public: + static void F1(S) {} + static void F3(int, S, float) {} + }; + C::F1({}); // Avoid unused method warning + C::F3(0, {}, 0); // Avoid unused method warning + + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); +} + +TEST(HasSignature, FunctionLike) { + using f1 = std::function; + using f3 = std::function; + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); +} + +TEST(HasSignature, Lambda) { + auto l1 = [](S) {}; + auto l3 = [](int, S, float) {}; + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); + static_assert(!HasSignature::value, ""); +} + +//// + +TEST(CompatibleWith, Function) { + F1({}); // Avoid unused method warning + F3(0, {}, 0); // Avoid unused method warning + E1({}); // Avoid unused method warning + E3(0, {}, 0); // Avoid unused method warning + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); +} + +TEST(CompatibleWith, Method) { + class C { + public: + void F1(S) {} + void F3(int, S, float) {} + void E1(E) {} + void E3(int, E, float) {} + }; + C().F1({}); // Avoid unused method warning + C().F3(0, {}, 0); // Avoid unused method warning + C().E1({}); // Avoid unused method warning + C().E3(0, {}, 0); // Avoid unused method warning + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); +} + +TEST(CompatibleWith, ConstMethod) { + class C { + public: + void F1(S) const {} + void F3(int, S, float) const {} + void E1(E) const {} + void E3(int, E, float) const {} + }; + C().F1({}); // Avoid unused method warning + C().F3(0, {}, 0); // Avoid unused method warning + C().E1({}); // Avoid unused method warning + C().E3(0, {}, 0); // Avoid unused method warning + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); +} + +TEST(CompatibleWith, StaticMethod) { + class C { + public: + static void F1(S) {} + static void F3(int, S, float) {} + static void E1(E) {} + static void E3(int, E, float) {} + }; + C::F1({}); // Avoid unused method warning + C::F3(0, {}, 0); // Avoid unused method warning + C::E1({}); // Avoid unused method warning + C::E3(0, {}, 0); // Avoid unused method warning + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); +} + +TEST(CompatibleWith, FunctionLike) { + using f1 = std::function; + using f3 = std::function; + using e1 = std::function; + using e3 = std::function; + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); +} + +TEST(CompatibleWith, Lambda) { + auto f1 = [](S) {}; + auto f3 = [](int, S, float) {}; + auto e1 = [](E) {}; + auto e3 = [](int, E, float) {}; + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + static_assert(CompatibleWith::value, ""); + + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); + static_assert(!CompatibleWith::value, ""); +} + +} // namespace traits +} // namespace dap diff --git a/libraries/cppdap/src/typeinfo.cpp b/libraries/cppdap/src/typeinfo.cpp new file mode 100644 index 000000000..dda481f9c --- /dev/null +++ b/libraries/cppdap/src/typeinfo.cpp @@ -0,0 +1,21 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/typeinfo.h" + +namespace dap { + +TypeInfo::~TypeInfo() = default; + +} // namespace dap diff --git a/libraries/cppdap/src/typeinfo_test.cpp b/libraries/cppdap/src/typeinfo_test.cpp new file mode 100644 index 000000000..23d579317 --- /dev/null +++ b/libraries/cppdap/src/typeinfo_test.cpp @@ -0,0 +1,65 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/typeof.h" +#include "dap/types.h" +#include "json_serializer.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace dap { + +struct BaseStruct { + dap::integer i; + dap::number n; +}; + +DAP_STRUCT_TYPEINFO(BaseStruct, + "BaseStruct", + DAP_FIELD(i, "i"), + DAP_FIELD(n, "n")); + +struct DerivedStruct : public BaseStruct { + dap::string s; + dap::boolean b; +}; + +DAP_STRUCT_TYPEINFO_EXT(DerivedStruct, + BaseStruct, + "DerivedStruct", + DAP_FIELD(s, "s"), + DAP_FIELD(b, "b")); + +} // namespace dap + +TEST(TypeInfo, Derived) { + dap::DerivedStruct in; + in.s = "hello world"; + in.b = true; + in.i = 42; + in.n = 3.14; + + dap::json::Serializer s; + ASSERT_TRUE(s.serialize(in)); + + dap::DerivedStruct out; + dap::json::Deserializer d(s.dump()); + ASSERT_TRUE(d.deserialize(&out)) << "Failed to deserialize\n" << s.dump(); + + ASSERT_EQ(out.s, "hello world"); + ASSERT_EQ(out.b, true); + ASSERT_EQ(out.i, 42); + ASSERT_EQ(out.n, 3.14); +} diff --git a/libraries/cppdap/src/typeof.cpp b/libraries/cppdap/src/typeof.cpp new file mode 100644 index 000000000..055421c66 --- /dev/null +++ b/libraries/cppdap/src/typeof.cpp @@ -0,0 +1,144 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/typeof.h" + +#include +#include +#include + +namespace { + +// TypeInfos owns all the dap::TypeInfo instances. +struct TypeInfos { + // get() returns the TypeInfos singleton pointer. + // TypeInfos is constructed with an internal reference count of 1. + static TypeInfos* get(); + + // reference() increments the TypeInfos reference count. + inline void reference() { + assert(refcount.load() > 0); + refcount++; + } + + // release() decrements the TypeInfos reference count. + // If the reference count becomes 0, then the TypeInfos is destructed. + inline void release() { + if (--refcount == 0) { + this->~TypeInfos(); + } + } + + struct NullTI : public dap::TypeInfo { + using null = dap::null; + inline std::string name() const override { return "null"; } + inline size_t size() const override { return sizeof(null); } + inline size_t alignment() const override { return alignof(null); } + inline void construct(void* ptr) const override { new (ptr) null(); } + inline void copyConstruct(void* dst, const void* src) const override { + new (dst) null(*reinterpret_cast(src)); + } + inline void destruct(void* ptr) const override { + reinterpret_cast(ptr)->~null(); + } + inline bool deserialize(const dap::Deserializer*, void*) const override { + return true; + } + inline bool serialize(dap::Serializer*, const void*) const override { + return true; + } + }; + + dap::BasicTypeInfo boolean = {"boolean"}; + dap::BasicTypeInfo string = {"string"}; + dap::BasicTypeInfo integer = {"integer"}; + dap::BasicTypeInfo number = {"number"}; + dap::BasicTypeInfo object = {"object"}; + dap::BasicTypeInfo any = {"any"}; + NullTI null; + std::vector> types; + + private: + TypeInfos() = default; + ~TypeInfos() = default; + std::atomic refcount = {1}; +}; + +// aligned_storage() is a replacement for std::aligned_storage that isn't busted +// on older versions of MSVC. +template +struct aligned_storage { + struct alignas(ALIGNMENT) type { + unsigned char data[SIZE]; + }; +}; + +TypeInfos* TypeInfos::get() { + static aligned_storage::type memory; + + struct Instance { + TypeInfos* ptr() { return reinterpret_cast(memory.data); } + Instance() { new (ptr()) TypeInfos(); } + ~Instance() { ptr()->release(); } + }; + + static Instance instance; + return instance.ptr(); +} + +} // namespace + +namespace dap { + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->boolean; +} + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->string; +} + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->integer; +} + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->number; +} + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->object; +} + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->any; +} + +const TypeInfo* TypeOf::type() { + return &TypeInfos::get()->null; +} + +void TypeInfo::deleteOnExit(TypeInfo* ti) { + TypeInfos::get()->types.emplace_back(std::unique_ptr(ti)); +} + +void initialize() { + TypeInfos::get()->reference(); +} + +void terminate() { + TypeInfos::get()->release(); +} + +} // namespace dap diff --git a/libraries/cppdap/src/variant_test.cpp b/libraries/cppdap/src/variant_test.cpp new file mode 100644 index 000000000..5a1f4ebaf --- /dev/null +++ b/libraries/cppdap/src/variant_test.cpp @@ -0,0 +1,94 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dap/variant.h" +#include "dap/typeof.h" +#include "dap/types.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace dap { + +struct VariantTestObject { + dap::integer i; + dap::number n; +}; + +DAP_STRUCT_TYPEINFO(VariantTestObject, + "VariantTestObject", + DAP_FIELD(i, "i"), + DAP_FIELD(n, "n")); + +} // namespace dap + +TEST(Variant, EmptyConstruct) { + dap::variant variant; + ASSERT_TRUE(variant.is()); + ASSERT_FALSE(variant.is()); + ASSERT_FALSE(variant.is()); +} + +TEST(Variant, Boolean) { + dap::variant variant( + dap::boolean(true)); + ASSERT_TRUE(variant.is()); + ASSERT_EQ(variant.get(), dap::boolean(true)); +} + +TEST(Variant, Integer) { + dap::variant variant( + dap::integer(10)); + ASSERT_TRUE(variant.is()); + ASSERT_EQ(variant.get(), dap::integer(10)); +} + +TEST(Variant, TestObject) { + dap::variant variant( + dap::VariantTestObject{5, 3.0}); + ASSERT_TRUE(variant.is()); + ASSERT_EQ(variant.get().i, 5); + ASSERT_EQ(variant.get().n, 3.0); +} + +TEST(Variant, Assign) { + dap::variant variant( + dap::integer(10)); + variant = dap::integer(10); + ASSERT_TRUE(variant.is()); + ASSERT_FALSE(variant.is()); + ASSERT_FALSE(variant.is()); + ASSERT_EQ(variant.get(), dap::integer(10)); + variant = dap::boolean(true); + ASSERT_FALSE(variant.is()); + ASSERT_TRUE(variant.is()); + ASSERT_FALSE(variant.is()); + ASSERT_EQ(variant.get(), dap::boolean(true)); + variant = dap::VariantTestObject{5, 3.0}; + ASSERT_FALSE(variant.is()); + ASSERT_FALSE(variant.is()); + ASSERT_TRUE(variant.is()); + ASSERT_EQ(variant.get().i, 5); + ASSERT_EQ(variant.get().n, 3.0); +} + +TEST(Variant, Accepts) { + using variant = + dap::variant; + ASSERT_TRUE(variant::accepts()); + ASSERT_TRUE(variant::accepts()); + ASSERT_TRUE(variant::accepts()); + ASSERT_FALSE(variant::accepts()); + ASSERT_FALSE(variant::accepts()); +} diff --git a/libraries/cppdap/third_party/json/.circleci/config.yml b/libraries/cppdap/third_party/json/.circleci/config.yml new file mode 100644 index 000000000..0d2edbf6a --- /dev/null +++ b/libraries/cppdap/third_party/json/.circleci/config.yml @@ -0,0 +1,27 @@ +version: 2 +jobs: + build: + docker: + - image: debian:stretch + + steps: + - checkout + + - run: + name: Install sudo + command: 'apt-get update && apt-get install -y sudo && rm -rf /var/lib/apt/lists/*' + - run: + name: Install GCC + command: 'apt-get update && apt-get install -y gcc g++' + - run: + name: Install CMake + command: 'apt-get update && sudo apt-get install -y cmake' + - run: + name: Create build files + command: 'mkdir build ; cd build ; cmake ..' + - run: + name: Compile + command: 'cmake --build build' + - run: + name: Execute test suite + command: 'cd build ; ctest -j 2' diff --git a/libraries/cppdap/third_party/json/.clang-tidy b/libraries/cppdap/third_party/json/.clang-tidy new file mode 100644 index 000000000..feee81945 --- /dev/null +++ b/libraries/cppdap/third_party/json/.clang-tidy @@ -0,0 +1,26 @@ +Checks: '-*, + bugprone-*, + cert-*, + clang-analyzer-*, + google-*, + -google-runtime-references, + -google-explicit-constructor, + hicpp-*, + -hicpp-no-array-decay, + -hicpp-uppercase-literal-suffix, + -hicpp-explicit-conversions, + misc-*, + -misc-non-private-member-variables-in-classes, + llvm-*, + -llvm-header-guard, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + portability-*, + readability-*, + -readability-magic-numbers, + -readability-uppercase-literal-suffix' + +CheckOptions: + - key: hicpp-special-member-functions.AllowSoleDefaultDtor + value: 1 diff --git a/libraries/cppdap/third_party/json/.doozer.json b/libraries/cppdap/third_party/json/.doozer.json new file mode 100644 index 000000000..687aead8a --- /dev/null +++ b/libraries/cppdap/third_party/json/.doozer.json @@ -0,0 +1,82 @@ +{ + "targets": { + "raspbian-jessie": { + "buildenv": "raspbian-jessie", + "builddeps": ["build-essential", "wget"], + "buildcmd": [ + "uname -a", + "cat /etc/os-release", + "g++ --version", + "cd", + "wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0.tar.gz", + "tar xfz cmake-3.14.0.tar.gz", + "cd cmake-3.14.0", + "./bootstrap", + "make -j8", + "cd", + "mkdir build", + "cd build", + "../cmake-3.14.0/bin/cmake /project/repo/checkout", + "make -j8", + "../cmake-3.14.0/bin/ctest -VV -j4 --timeout 10000" + ] + }, + "xenial-armhf": { + "buildenv": "xenial-armhf", + "builddeps": ["build-essential", "wget"], + "buildcmd": [ + "uname -a", + "lsb_release -a", + "g++ --version", + "cd", + "wget --no-check-certificate https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0.tar.gz", + "tar xfz cmake-3.14.0.tar.gz", + "cd cmake-3.14.0", + "./bootstrap", + "make -j8", + "cd", + "mkdir build", + "cd build", + "../cmake-3.14.0/bin/cmake /project/repo/checkout", + "make -j8", + "../cmake-3.14.0/bin/ctest -VV -j4 --timeout 10000" + ] + }, + "fedora24-x86_64": { + "buildenv": "fedora24-x86_64", + "builddeps": ["cmake", "make", "gcc gcc-c++"], + "buildcmd": [ + "uname -a", + "cat /etc/fedora-release", + "g++ --version", + "cd", + "mkdir build", + "cd build", + "cmake /project/repo/checkout", + "make -j8", + "ctest -VV -j8" + ] + }, + "centos7-x86_64": { + "buildenv": "centos7-x86_64", + "builddeps": ["make", "wget", "gcc-c++"], + "buildcmd": [ + "uname -a", + "rpm -q centos-release", + "g++ --version", + "cd", + "wget https://github.com/Kitware/CMake/releases/download/v3.14.0/cmake-3.14.0.tar.gz", + "tar xfz cmake-3.14.0.tar.gz", + "cd cmake-3.14.0", + "./bootstrap", + "make -j8", + "cd", + "mkdir build", + "cd build", + "../cmake-3.14.0/bin/cmake /project/repo/checkout", + "make -j8", + "../cmake-3.14.0/bin/ctest -VV -j8" + ] + } + } +} diff --git a/libraries/cppdap/third_party/json/.github/CODEOWNERS b/libraries/cppdap/third_party/json/.github/CODEOWNERS new file mode 100644 index 000000000..5d9d51d6b --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# JSON for Modern C++ has been originally written by Niels Lohmann. +# Since 2013 over 140 contributors have helped to improve the library. +# This CODEOWNERS file is only to make sure that @nlohmann is requsted +# for a code review in case of a pull request. + +* @nlohmann diff --git a/libraries/cppdap/third_party/json/.github/CONTRIBUTING.md b/libraries/cppdap/third_party/json/.github/CONTRIBUTING.md new file mode 100644 index 000000000..2287cad47 --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/CONTRIBUTING.md @@ -0,0 +1,71 @@ +[![Issue Stats](http://issuestats.com/github/nlohmann/json/badge/pr?style=flat)](http://issuestats.com/github/nlohmann/json) [![Issue Stats](http://issuestats.com/github/nlohmann/json/badge/issue?style=flat)](http://issuestats.com/github/nlohmann/json) + +# How to contribute + +This project started as a little excuse to exercise some of the cool new C++11 features. Over time, people actually started to use the JSON library (yey!) and started to help improve it by proposing features, finding bugs, or even fixing my mistakes. I am really [thankful](https://github.com/nlohmann/json/blob/master/README.md#thanks) for this and try to keep track of all the helpers. + +To make it as easy as possible for you to contribute and for me to keep an overview, here are a few guidelines which should help us avoid all kinds of unnecessary work or disappointment. And of course, this document is subject to discussion, so please [create an issue](https://github.com/nlohmann/json/issues/new/choose) or a pull request if you find a way to improve it! + +## Private reports + +Usually, all issues are tracked publicly on [GitHub](https://github.com/nlohmann/json/issues). If you want to make a private report (e.g., for a vulnerability or to attach an example that is not meant to be published), please send an email to . + +## Prerequisites + +Please [create an issue](https://github.com/nlohmann/json/issues/new/choose), assuming one does not already exist, and describe your concern. Note you need a [GitHub account](https://github.com/signup/free) for this. + +## Describe your issue + +Clearly describe the issue: + +- If it is a bug, please describe how to **reproduce** it. If possible, attach a complete example which demonstrates the error. Please also state what you **expected** to happen instead of the error. +- If you propose a change or addition, try to give an **example** how the improved code could look like or how to use it. +- If you found a compilation error, please tell us which **compiler** (version and operating system) you used and paste the (relevant part of) the error messages to the ticket. + +Please stick to the provided issue templates ([bug report](https://github.com/nlohmann/json/blob/develop/.github/ISSUE_TEMPLATE/Bug_report.md), [feature request](https://github.com/nlohmann/json/blob/develop/.github/ISSUE_TEMPLATE/Feature_request.md), or [question](https://github.com/nlohmann/json/blob/develop/.github/ISSUE_TEMPLATE/question.md)) if possible. + +## Files to change + +:exclamation: Before you make any changes, note the single-header file [`single_include/nlohmann/json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp) is **generated** from the source files in the [`include/nlohmann` directory](https://github.com/nlohmann/json/tree/develop/include/nlohmann). Please **do not** edit file `single_include/nlohmann/json.hpp` directly, but change the `include/nlohmann` sources and regenerate file `single_include/nlohmann/json.hpp` by executing `make amalgamate`. + +To make changes, you need to edit the following files: + +1. [`include/nlohmann/*`](https://github.com/nlohmann/json/tree/develop/include/nlohmann) - These files are the sources of the library. Before testing or creating a pull request, execute `make amalgamate` to regenerate `single_include/nlohmann/json.hpp`. + +2. [`test/src/unit-*.cpp`](https://github.com/nlohmann/json/tree/develop/test/src) - These files contain the [doctest](https://github.com/onqtam/doctest) unit tests which currently cover [100 %](https://coveralls.io/github/nlohmann/json) of the library's code. + + If you add or change a feature, please also add a unit test to this file. The unit tests can be compiled and executed with + + ```sh + $ mkdir build + $ cd build + $ cmake .. + $ cmake --build . + $ ctest + ``` + + The test cases are also executed with several different compilers on [Travis](https://travis-ci.org/nlohmann/json) once you open a pull request. + + +## Note + +- If you open a pull request, the code will be automatically tested with [Valgrind](http://valgrind.org)'s Memcheck tool to detect memory leaks. Please be aware that the execution with Valgrind _may_ in rare cases yield different behavior than running the code directly. This can result in failing unit tests which run successfully without Valgrind. +- There is a Makefile target `make pretty` which runs [Artistic Style](http://astyle.sourceforge.net) to fix indentation. If possible, run it before opening the pull request. Otherwise, we shall run it afterward. + +## Please don't + +- The C++11 support varies between different **compilers** and versions. Please note the [list of supported compilers](https://github.com/nlohmann/json/blob/master/README.md#supported-compilers). Some compilers like GCC 4.7 (and earlier), Clang 3.3 (and earlier), or Microsoft Visual Studio 13.0 and earlier are known not to work due to missing or incomplete C++11 support. Please refrain from proposing changes that work around these compiler's limitations with `#ifdef`s or other means. +- Specifically, I am aware of compilation problems with **Microsoft Visual Studio** (there even is an [issue label](https://github.com/nlohmann/json/issues?utf8=✓&q=label%3A%22visual+studio%22+) for these kind of bugs). I understand that even in 2016, complete C++11 support isn't there yet. But please also understand that I do not want to drop features or uglify the code just to make Microsoft's sub-standard compiler happy. The past has shown that there are ways to express the functionality such that the code compiles with the most recent MSVC - unfortunately, this is not the main objective of the project. +- Please refrain from proposing changes that would **break [JSON](http://json.org) conformance**. If you propose a conformant extension of JSON to be supported by the library, please motivate this extension. + - We shall not extend the library to **support comments**. There is quite some [controversy](https://www.reddit.com/r/programming/comments/4v6chu/why_json_doesnt_support_comments_douglas_crockford/) around this topic, and there were quite some [issues](https://github.com/nlohmann/json/issues/376) on this. We believe that JSON is fine without comments. + - We do not preserve the **insertion order of object elements**. The [JSON standard](https://tools.ietf.org/html/rfc7159.html) defines objects as "an unordered collection of zero or more name/value pairs". To this end, this library does not preserve insertion order of name/value pairs. (In fact, keys will be traversed in alphabetical order as `std::map` with `std::less` is used by default.) Note this behavior conforms to the standard, and we shall not change it to any other order. If you do want to preserve the insertion order, you can specialize the object type with containers like [`tsl::ordered_map`](https://github.com/Tessil/ordered-map) or [`nlohmann::fifo_map`](https://github.com/nlohmann/fifo_map). + +- Please do not open pull requests that address **multiple issues**. + +## Wanted + +The following areas really need contribution: + +- Extending the **continuous integration** toward more exotic compilers such as Android NDK, Intel's Compiler, or the bleeding-edge versions of GCC or Clang. +- Improving the efficiency of the **JSON parser**. The current parser is implemented as a naive recursive descent parser with hand coded string handling. More sophisticated approaches like LALR parsers would be really appreciated. That said, parser generators like Bison or ANTLR do not play nice with single-header files -- I really would like to keep the parser inside the `json.hpp` header, and I am not aware of approaches similar to [`re2c`](http://re2c.org) for parsing. +- Extending and updating existing **benchmarks** to include (the most recent version of) this library. Though efficiency is not everything, speed and memory consumption are very important characteristics for C++ developers, so having proper comparisons would be interesting. diff --git a/libraries/cppdap/third_party/json/.github/FUNDING.yml b/libraries/cppdap/third_party/json/.github/FUNDING.yml new file mode 100644 index 000000000..c202804be --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: http://paypal.me/nlohmann diff --git a/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Bug_report.md b/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 000000000..748357a95 --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'kind: bug' +assignees: '' + +--- + +- What is the issue you have? + +- Please describe the steps to reproduce the issue. Can you provide a small but working code example? + +- What is the expected behavior? + +- And what is the actual behavior instead? + +- Which compiler and operating system are you using? Is it a [supported compiler](https://github.com/nlohmann/json#supported-compilers)? + +- Did you use a released version of the library or the version from the `develop` branch? + +- If you experience a compilation error: can you [compile and run the unit tests](https://github.com/nlohmann/json#execute-unit-tests)? diff --git a/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Feature_request.md b/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 000000000..05a2c946e --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,12 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'kind: enhancement/improvement' +assignees: '' + +--- + +- Describe the feature in as much detail as possible. + +- Include sample usage where appropriate. diff --git a/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/question.md b/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..0870cd17b --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,16 @@ +--- +name: Question +about: Ask a question regarding the library. +title: '' +labels: 'kind: question' +assignees: '' + +--- + +- Describe what you want to achieve. + +- Describe what you tried. + +- Describe which system (OS, compiler) you are using. + +- Describe which version of the library you are using (release version, develop branch). diff --git a/libraries/cppdap/third_party/json/.github/PULL_REQUEST_TEMPLATE.md b/libraries/cppdap/third_party/json/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..f2c3af6a8 --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +[Describe your pull request here. Please read the text below the line, and make sure you follow the checklist.] + +* * * + +## Pull request checklist + +Read the [Contribution Guidelines](https://github.com/nlohmann/json/blob/develop/.github/CONTRIBUTING.md) for detailed information. + +- [ ] Changes are described in the pull request, or an [existing issue is referenced](https://github.com/nlohmann/json/issues). +- [ ] The test suite [compiles and runs](https://github.com/nlohmann/json/blob/develop/README.md#execute-unit-tests) without error. +- [ ] [Code coverage](https://coveralls.io/github/nlohmann/json) is 100%. Test cases can be added by editing the [test suite](https://github.com/nlohmann/json/tree/develop/test/src). +- [ ] The source code is amalgamated; that is, after making changes to the sources in the `include/nlohmann` directory, run `make amalgamate` to create the single-header file `single_include/nlohmann/json.hpp`. The whole process is described [here](https://github.com/nlohmann/json/blob/develop/.github/CONTRIBUTING.md#files-to-change). + +## Please don't + +- The C++11 support varies between different **compilers** and versions. Please note the [list of supported compilers](https://github.com/nlohmann/json/blob/master/README.md#supported-compilers). Some compilers like GCC 4.7 (and earlier), Clang 3.3 (and earlier), or Microsoft Visual Studio 13.0 and earlier are known not to work due to missing or incomplete C++11 support. Please refrain from proposing changes that work around these compiler's limitations with `#ifdef`s or other means. +- Specifically, I am aware of compilation problems with **Microsoft Visual Studio** (there even is an [issue label](https://github.com/nlohmann/json/issues?utf8=✓&q=label%3A%22visual+studio%22+) for these kind of bugs). I understand that even in 2016, complete C++11 support isn't there yet. But please also understand that I do not want to drop features or uglify the code just to make Microsoft's sub-standard compiler happy. The past has shown that there are ways to express the functionality such that the code compiles with the most recent MSVC - unfortunately, this is not the main objective of the project. +- Please refrain from proposing changes that would **break [JSON](http://json.org) conformance**. If you propose a conformant extension of JSON to be supported by the library, please motivate this extension. +- Please do not open pull requests that address **multiple issues**. diff --git a/libraries/cppdap/third_party/json/.github/SECURITY.md b/libraries/cppdap/third_party/json/.github/SECURITY.md new file mode 100644 index 000000000..4d010ebda --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Usually, all issues are tracked publicly on [GitHub](https://github.com/nlohmann/json/issues). If you want to make a private report (e.g., for a vulnerability or to attach an example that is not meant to be published), please send an email to . You can use [this key](https://keybase.io/nlohmann/pgp_keys.asc?fingerprint=797167ae41c0a6d9232e48457f3cea63ae251b69) for encryption. diff --git a/libraries/cppdap/third_party/json/.github/config.yml b/libraries/cppdap/third_party/json/.github/config.yml new file mode 100644 index 000000000..7a8f41e6d --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/config.yml @@ -0,0 +1,19 @@ +# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot + +# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic +# Anything higher than this threshold will be marked as toxic and commented on +sentimentBotToxicityThreshold: .7 + +# *Required* Comment to reply with +sentimentBotReplyComment: > + Please be sure to review the [code of conduct](https://github.com/nlohmann/json/blob/develop/CODE_OF_CONDUCT.md) and be respectful of other users. cc/ @nlohmann + + +# Configuration for request-info - https://github.com/behaviorbot/request-info + +# *Required* Comment to reply with +requestInfoReplyComment: > + We would appreciate it if you could provide us with more info about this issue or pull request! Please check the [issue template](https://github.com/nlohmann/json/blob/develop/.github/ISSUE_TEMPLATE.md) and the [pull request template](https://github.com/nlohmann/json/blob/develop/.github/PULL_REQUEST_TEMPLATE.md). + +# *OPTIONAL* Label to be added to Issues and Pull Requests with insufficient information given +requestInfoLabelToAdd: "state: needs more info" diff --git a/libraries/cppdap/third_party/json/.github/stale.yml b/libraries/cppdap/third_party/json/.github/stale.yml new file mode 100644 index 000000000..d30c78be7 --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 30 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: "state: stale" +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/libraries/cppdap/third_party/json/.github/workflows/ccpp.yml b/libraries/cppdap/third_party/json/.github/workflows/ccpp.yml new file mode 100644 index 000000000..c6f8afb3c --- /dev/null +++ b/libraries/cppdap/third_party/json/.github/workflows/ccpp.yml @@ -0,0 +1,19 @@ +name: C/C++ CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: prepare + run: mkdir build + - name: cmake + run: cd build ; cmake .. + - name: build + run: make -C build + - name: test + run: cd build ; ctest -j 10 diff --git a/libraries/cppdap/third_party/json/.gitignore b/libraries/cppdap/third_party/json/.gitignore new file mode 100644 index 000000000..086e855c0 --- /dev/null +++ b/libraries/cppdap/third_party/json/.gitignore @@ -0,0 +1,25 @@ +json_unit +json_benchmarks +json_benchmarks_simple +fuzz-testing + +*.dSYM +*.o +*.gcno +*.gcda + +build +build_coverage +clang_analyze_build + +doc/xml +doc/html +me.nlohmann.json.docset + +benchmarks/files/numbers/*.json + +.idea +cmake-build-debug + +test/test-* +/.vs diff --git a/libraries/cppdap/third_party/json/.travis.yml b/libraries/cppdap/third_party/json/.travis.yml new file mode 100644 index 000000000..8f871c09a --- /dev/null +++ b/libraries/cppdap/third_party/json/.travis.yml @@ -0,0 +1,345 @@ +######################### +# project configuration # +######################### + +# C++ project +language: cpp + +dist: trusty +sudo: required +group: edge + + +################### +# global settings # +################### + +env: + global: + # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created + # via the "travis encrypt" command using the project repo's public key + - secure: "m89SSgE+ASLO38rSKx7MTXK3n5NkP9bIx95jwY71YEiuFzib30PDJ/DifKnXxBjvy/AkCGztErQRk/8ZCvq+4HXozU2knEGnL/RUitvlwbhzfh2D4lmS3BvWBGS3N3NewoPBrRmdcvnT0xjOGXxtZaJ3P74TkB9GBnlz/HmKORA=" + + +################ +# build matrix # +################ + +matrix: + include: + + # Valgrind + - os: linux + compiler: gcc + env: + - COMPILER=g++-4.9 + - CMAKE_OPTIONS=-DJSON_Valgrind=ON + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.9', 'valgrind', 'ninja-build'] + + # clang sanitizer + - os: linux + compiler: clang + env: + - COMPILER=clang++-7 + - CMAKE_OPTIONS=-DJSON_Sanitizer=ON + - UBSAN_OPTIONS=print_stacktrace=1,suppressions=$(pwd)/test/src/UBSAN.supp + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-7'] + packages: ['g++-6', 'clang-7', 'ninja-build'] + before_script: + - export PATH=$PATH:/usr/lib/llvm-7/bin + + # cppcheck + - os: linux + compiler: gcc + env: + - COMPILER=g++-4.9 + - SPECIAL=cppcheck + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.9', 'cppcheck', 'ninja-build'] + after_success: + - make cppcheck + + # no exceptions + - os: linux + compiler: gcc + env: + - COMPILER=g++-4.9 + - CMAKE_OPTIONS=-DJSON_NoExceptions=ON + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.9', 'ninja-build'] + + # check amalgamation + - os: linux + compiler: gcc + env: + - COMPILER=g++-4.9 + - SPECIAL=amalgamation + - MULTIPLE_HEADERS=ON + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.9', 'astyle', 'ninja-build'] + after_success: + - make check-amalgamation + + # Coveralls (http://gronlier.fr/blog/2015/01/adding-code-coverage-to-your-c-project/) + + - os: linux + compiler: gcc + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.9', 'ninja-build'] + before_script: + - pip install --user cpp-coveralls + after_success: + - coveralls --build-root test --include include/nlohmann --gcov 'gcov-4.9' --gcov-options '\-lp' + env: + - COMPILER=g++-4.9 + - CMAKE_OPTIONS=-DJSON_Coverage=ON + - MULTIPLE_HEADERS=ON + + # Coverity (only for branch coverity_scan) + + - os: linux + compiler: clang + before_install: echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca-certificates.crt + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.6'] + packages: ['g++-6', 'clang-3.6', 'ninja-build'] + coverity_scan: + project: + name: "nlohmann/json" + description: "Build submitted via Travis CI" + notification_email: niels.lohmann@gmail.com + build_command_prepend: "mkdir coverity_build ; cd coverity_build ; cmake .. ; cd .." + build_command: "make -C coverity_build" + branch_pattern: coverity_scan + env: + - SPECIAL=coverity + - COMPILER=clang++-3.6 + + # OSX / Clang + + - os: osx + osx_image: xcode8.3 + + - os: osx + osx_image: xcode9 + + - os: osx + osx_image: xcode9.1 + + - os: osx + osx_image: xcode9.2 + + - os: osx + osx_image: xcode9.3 + + - os: osx + osx_image: xcode9.4 + + - os: osx + osx_image: xcode10 + + - os: osx + osx_image: xcode10.1 + + # Linux / GCC + + - os: linux + compiler: gcc + env: compiler=g++-4.8 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.8', 'ninja-build'] + + - os: linux + compiler: gcc + env: compiler=g++-4.9 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-4.9', 'ninja-build'] + + - os: linux + compiler: gcc + env: COMPILER=g++-5 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-5', 'ninja-build'] + + - os: linux + compiler: gcc + env: COMPILER=g++-6 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-6', 'ninja-build'] + + - os: linux + compiler: gcc + env: COMPILER=g++-7 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-7', 'ninja-build'] + + - os: linux + compiler: gcc + env: COMPILER=g++-8 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-8', 'ninja-build'] + + - os: linux + compiler: gcc + env: COMPILER=g++-9 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-9', 'ninja-build'] + + - os: linux + compiler: gcc + env: + - COMPILER=g++-9 + - CXXFLAGS=-std=c++2a + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-9', 'ninja-build'] + + # Linux / Clang + + - os: linux + compiler: clang + env: COMPILER=clang++-3.5 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.5'] + packages: ['g++-6', 'clang-3.5', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-3.6 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.6'] + packages: ['g++-6', 'clang-3.6', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-3.7 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.7'] + packages: ['g++-6', 'clang-3.7', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-3.8 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-6', 'clang-3.8', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-3.9 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ['g++-6', 'clang-3.9', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-4.0 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-4.0'] + packages: ['g++-6', 'clang-4.0', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-5.0 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-5.0'] + packages: ['g++-6', 'clang-5.0', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-6.0 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-6.0'] + packages: ['g++-6', 'clang-6.0', 'ninja-build'] + + - os: linux + compiler: clang + env: COMPILER=clang++-7 + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-7'] + packages: ['g++-6', 'clang-7', 'ninja-build'] + + - os: linux + compiler: clang + env: + - COMPILER=clang++-7 + - CXXFLAGS=-std=c++1z + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-7'] + packages: ['g++-6', 'clang-7', 'ninja-build'] + +################ +# build script # +################ + +script: + # get CMake and Ninja (only for systems with brew - macOS) + - | + if [[ (-x $(which brew)) ]]; then + brew update + brew install cmake ninja + brew upgrade cmake + cmake --version + fi + + # make sure CXX is correctly set + - if [[ "${COMPILER}" != "" ]]; then export CXX=${COMPILER}; fi + # by default, use the single-header version + - if [[ "${MULTIPLE_HEADERS}" == "" ]]; then export MULTIPLE_HEADERS=OFF; fi + + # show OS/compiler version + - uname -a + - $CXX --version + + # compile and execute unit tests + - mkdir -p build && cd build + - cmake .. ${CMAKE_OPTIONS} -DJSON_MultipleHeaders=${MULTIPLE_HEADERS} -GNinja && cmake --build . --config Release + - ctest -C Release --timeout 2700 -V -j + - cd .. + + # check if homebrew works (only checks develop branch) + - if [ `which brew` ]; then + brew update ; + brew tap nlohmann/json ; + brew install nlohmann_json --HEAD ; + brew test nlohmann_json ; + fi diff --git a/libraries/cppdap/third_party/json/CMakeLists.txt b/libraries/cppdap/third_party/json/CMakeLists.txt new file mode 100644 index 000000000..f717ff462 --- /dev/null +++ b/libraries/cppdap/third_party/json/CMakeLists.txt @@ -0,0 +1,131 @@ +cmake_minimum_required(VERSION 3.1) + +## +## PROJECT +## name and version +## +project(nlohmann_json VERSION 3.7.0 LANGUAGES CXX) + +## +## INCLUDE +## +## +include(ExternalProject) + +## +## OPTIONS +## +option(JSON_BuildTests "Build the unit tests when BUILD_TESTING is enabled." ON) +option(JSON_Install "Install CMake targets during install step." ON) +option(JSON_MultipleHeaders "Use non-amalgamated version of the library." OFF) + +## +## CONFIGURATION +## +include(GNUInstallDirs) + +set(NLOHMANN_JSON_TARGET_NAME ${PROJECT_NAME}) +set(NLOHMANN_JSON_CONFIG_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" CACHE INTERNAL "") +set(NLOHMANN_JSON_INCLUDE_INSTALL_DIR "${CMAKE_INSTALL_INCLUDEDIR}") +set(NLOHMANN_JSON_TARGETS_EXPORT_NAME "${PROJECT_NAME}Targets") +set(NLOHMANN_JSON_CMAKE_CONFIG_TEMPLATE "cmake/config.cmake.in") +set(NLOHMANN_JSON_CMAKE_CONFIG_DIR "${CMAKE_CURRENT_BINARY_DIR}") +set(NLOHMANN_JSON_CMAKE_VERSION_CONFIG_FILE "${NLOHMANN_JSON_CMAKE_CONFIG_DIR}/${PROJECT_NAME}ConfigVersion.cmake") +set(NLOHMANN_JSON_CMAKE_PROJECT_CONFIG_FILE "${NLOHMANN_JSON_CMAKE_CONFIG_DIR}/${PROJECT_NAME}Config.cmake") +set(NLOHMANN_JSON_CMAKE_PROJECT_TARGETS_FILE "${NLOHMANN_JSON_CMAKE_CONFIG_DIR}/${PROJECT_NAME}Targets.cmake") + +if (JSON_MultipleHeaders) + set(NLOHMANN_JSON_INCLUDE_BUILD_DIR "${PROJECT_SOURCE_DIR}/include/") + message(STATUS "Using the multi-header code from ${NLOHMANN_JSON_INCLUDE_BUILD_DIR}") +else() + set(NLOHMANN_JSON_INCLUDE_BUILD_DIR "${PROJECT_SOURCE_DIR}/single_include/") + message(STATUS "Using the single-header code from ${NLOHMANN_JSON_INCLUDE_BUILD_DIR}") +endif() + +## +## TARGET +## create target and add include path +## +add_library(${NLOHMANN_JSON_TARGET_NAME} INTERFACE) +add_library(${PROJECT_NAME}::${NLOHMANN_JSON_TARGET_NAME} ALIAS ${NLOHMANN_JSON_TARGET_NAME}) +if (${CMAKE_VERSION} VERSION_LESS "3.8.0") + target_compile_features(${NLOHMANN_JSON_TARGET_NAME} INTERFACE cxx_range_for) +else() + target_compile_features(${NLOHMANN_JSON_TARGET_NAME} INTERFACE cxx_std_11) +endif() + +target_include_directories( + ${NLOHMANN_JSON_TARGET_NAME} + INTERFACE + $ + $ +) + +## add debug view definition file for msvc (natvis) +if (MSVC) + set(NLOHMANN_ADD_NATVIS TRUE) + set(NLOHMANN_NATVIS_FILE "nlohmann_json.natvis") + target_sources( + ${NLOHMANN_JSON_TARGET_NAME} + INTERFACE + $ + $ + ) +endif() + +## +## TESTS +## create and configure the unit test target +## +include(CTest) #adds option BUILD_TESTING (default ON) + +if(BUILD_TESTING AND JSON_BuildTests) + enable_testing() + add_subdirectory(test) +endif() + +## +## INSTALL +## install header files, generate and install cmake config files for find_package() +## +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + ${NLOHMANN_JSON_CMAKE_VERSION_CONFIG_FILE} COMPATIBILITY SameMajorVersion +) +configure_file( + ${NLOHMANN_JSON_CMAKE_CONFIG_TEMPLATE} + ${NLOHMANN_JSON_CMAKE_PROJECT_CONFIG_FILE} + @ONLY +) + +if(JSON_Install) + install( + DIRECTORY ${NLOHMANN_JSON_INCLUDE_BUILD_DIR} + DESTINATION ${NLOHMANN_JSON_INCLUDE_INSTALL_DIR} + ) + install( + FILES ${NLOHMANN_JSON_CMAKE_PROJECT_CONFIG_FILE} ${NLOHMANN_JSON_CMAKE_VERSION_CONFIG_FILE} + DESTINATION ${NLOHMANN_JSON_CONFIG_INSTALL_DIR} + ) + if (NLOHMANN_ADD_NATVIS) + install( + FILES ${NLOHMANN_NATVIS_FILE} + DESTINATION . + ) + endif() + export( + TARGETS ${NLOHMANN_JSON_TARGET_NAME} + NAMESPACE ${PROJECT_NAME}:: + FILE ${NLOHMANN_JSON_CMAKE_PROJECT_TARGETS_FILE} + ) + install( + TARGETS ${NLOHMANN_JSON_TARGET_NAME} + EXPORT ${NLOHMANN_JSON_TARGETS_EXPORT_NAME} + INCLUDES DESTINATION ${NLOHMANN_JSON_INCLUDE_INSTALL_DIR} + ) + install( + EXPORT ${NLOHMANN_JSON_TARGETS_EXPORT_NAME} + NAMESPACE ${PROJECT_NAME}:: + DESTINATION ${NLOHMANN_JSON_CONFIG_INSTALL_DIR} + ) +endif() diff --git a/libraries/cppdap/third_party/json/CODE_OF_CONDUCT.md b/libraries/cppdap/third_party/json/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..770b8173e --- /dev/null +++ b/libraries/cppdap/third_party/json/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mail@nlohmann.me. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/libraries/cppdap/third_party/json/ChangeLog.md b/libraries/cppdap/third_party/json/ChangeLog.md new file mode 100644 index 000000000..dffc858d2 --- /dev/null +++ b/libraries/cppdap/third_party/json/ChangeLog.md @@ -0,0 +1,1670 @@ +# Change Log +All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). + +## [v3.7.0](https://github.com/nlohmann/json/releases/tag/v3.7.0) (2019-07-28) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.6.1...v3.7.0) + +- How can I retrieve uknown strings from json file in my C++ program. [\#1684](https://github.com/nlohmann/json/issues/1684) +- contains\(\) is sometimes causing stack-based buffer overrun exceptions [\#1683](https://github.com/nlohmann/json/issues/1683) +- How to deserialize arrays from json [\#1681](https://github.com/nlohmann/json/issues/1681) +- Compilation failed in VS2015 [\#1678](https://github.com/nlohmann/json/issues/1678) +- Why the compiled object file is so huge? [\#1677](https://github.com/nlohmann/json/issues/1677) +- From Version 2.1.1 to 3.6.1 serialize std::set [\#1676](https://github.com/nlohmann/json/issues/1676) +- Qt deprecation model halting compiltion [\#1675](https://github.com/nlohmann/json/issues/1675) +- Build For Raspberry pi , Rapbery with new Compiler C++17 [\#1671](https://github.com/nlohmann/json/issues/1671) +- Build from Raspberry pi [\#1667](https://github.com/nlohmann/json/issues/1667) +- Can not translate map with integer key to dict string ? [\#1664](https://github.com/nlohmann/json/issues/1664) +- Double type converts to scientific notation [\#1661](https://github.com/nlohmann/json/issues/1661) +- Missing v3.6.1 tag on master branch [\#1657](https://github.com/nlohmann/json/issues/1657) +- Support Fleese Binary Data Format [\#1654](https://github.com/nlohmann/json/issues/1654) +- Suggestion: replace alternative tokens for !, && and || with their symbols [\#1652](https://github.com/nlohmann/json/issues/1652) +- Build failure test-allocator.vcxproj [\#1651](https://github.com/nlohmann/json/issues/1651) +- How to provide function json& to\_json\(\) which is similar as 'void to\_json\(json&j, const CObject& obj\)' ? [\#1650](https://github.com/nlohmann/json/issues/1650) +- Can't throw exception when starting file is a number [\#1649](https://github.com/nlohmann/json/issues/1649) +- to\_json / from\_json with nested type [\#1648](https://github.com/nlohmann/json/issues/1648) +- How to create a json object from a std::string, created by j.dump? [\#1645](https://github.com/nlohmann/json/issues/1645) +- Problem getting vector \(array\) of strings [\#1644](https://github.com/nlohmann/json/issues/1644) +- json.hpp compilation issue with other typedefs with same name [\#1642](https://github.com/nlohmann/json/issues/1642) +- nlohmann::adl\_serializer\::to\_json no matching overloaded function found [\#1641](https://github.com/nlohmann/json/issues/1641) +- overwrite adl\_serializer\ to change behaviour [\#1638](https://github.com/nlohmann/json/issues/1638) +- json.SelectToken\("Manufacturers.Products.Price"\); [\#1637](https://github.com/nlohmann/json/issues/1637) +- Add json type as value [\#1636](https://github.com/nlohmann/json/issues/1636) +- Unit conversion test error: conversion from 'nlohmann::json' to non-scalar type 'std::string\_view' requested [\#1634](https://github.com/nlohmann/json/issues/1634) +- nlohmann VS JsonCpp by C++17 [\#1633](https://github.com/nlohmann/json/issues/1633) +- To integrate an inline helper function that return type name as string [\#1632](https://github.com/nlohmann/json/issues/1632) +- Return JSON as reference [\#1631](https://github.com/nlohmann/json/issues/1631) +- Updating from an older version causes problems with assing a json object to a struct [\#1630](https://github.com/nlohmann/json/issues/1630) +- Can without default constructor function for user defined classes when only to\_json is needed? [\#1629](https://github.com/nlohmann/json/issues/1629) +- Compilation fails with clang 6.x-8.x in C++14 mode [\#1628](https://github.com/nlohmann/json/issues/1628) +- Treating floating point as string [\#1627](https://github.com/nlohmann/json/issues/1627) +- error parsing character Ã¥ [\#1626](https://github.com/nlohmann/json/issues/1626) +- \[Help\] How to Improve Json Output Performance with Large Json Arrays [\#1624](https://github.com/nlohmann/json/issues/1624) +- Suggested link changes for reporting new issues \[blob/develop/REAME.md and blob/develop/.github/CONTRIBUTING.md\] [\#1623](https://github.com/nlohmann/json/issues/1623) +- Broken link to issue template in CONTRIBUTING.md [\#1622](https://github.com/nlohmann/json/issues/1622) +- Missing word in README.md file [\#1621](https://github.com/nlohmann/json/issues/1621) +- Package manager instructions in README for brew is incorrect [\#1620](https://github.com/nlohmann/json/issues/1620) +- Building with Visual Studio 2019 [\#1619](https://github.com/nlohmann/json/issues/1619) +- Precedence of to\_json and builtin harmful [\#1617](https://github.com/nlohmann/json/issues/1617) +- The type json is missing from the html documentation [\#1616](https://github.com/nlohmann/json/issues/1616) +- variant is not support in Release 3.6.1? [\#1615](https://github.com/nlohmann/json/issues/1615) +- Replace assert with throw for const operator\[\] [\#1614](https://github.com/nlohmann/json/issues/1614) +- Memory Overhead is Too High \(10x or more\) [\#1613](https://github.com/nlohmann/json/issues/1613) +- program crash everytime, when other data type incomming in json stream as expected [\#1612](https://github.com/nlohmann/json/issues/1612) +- Improved Enum Support [\#1611](https://github.com/nlohmann/json/issues/1611) +- is it possible convert json object back to stl container ? [\#1610](https://github.com/nlohmann/json/issues/1610) +- Add C++17-like emplace.back\(\) for arrays. [\#1609](https://github.com/nlohmann/json/issues/1609) +- is\_nothrow\_copy\_constructible fails for json::const\_iterator on MSVC2015 x86 Debug build [\#1608](https://github.com/nlohmann/json/issues/1608) +- Reading and writing array elements [\#1607](https://github.com/nlohmann/json/issues/1607) +- Converting json::value to int [\#1605](https://github.com/nlohmann/json/issues/1605) +- I have a vector of keys and and a string of value and i want to create nested json array [\#1604](https://github.com/nlohmann/json/issues/1604) +- In compatible JSON object from nlohmann::json to nohman::json - unexpected end of input; expected '\[', '{', or a literal [\#1603](https://github.com/nlohmann/json/issues/1603) +- json parser crash if having a large number integer in message [\#1602](https://github.com/nlohmann/json/issues/1602) +- Value method with undocumented throwing 302 exception [\#1601](https://github.com/nlohmann/json/issues/1601) +- Accessing value with json pointer adds key if not existing [\#1600](https://github.com/nlohmann/json/issues/1600) +- README.md broken link to project documentation [\#1597](https://github.com/nlohmann/json/issues/1597) +- Random Kudos: Thanks for your work on this! [\#1596](https://github.com/nlohmann/json/issues/1596) +- json::parse return value and errors [\#1595](https://github.com/nlohmann/json/issues/1595) +- initializer list constructor makes curly brace initialization fragile [\#1594](https://github.com/nlohmann/json/issues/1594) +- trying to log message for missing keyword, difference between \["foo"\] and at\("foo"\) [\#1593](https://github.com/nlohmann/json/issues/1593) +- std::string and std::wstring `to\_json` [\#1592](https://github.com/nlohmann/json/issues/1592) +- I have a C structure which I need to convert to a JSON. How do I do it? Haven't found proper examples so far. [\#1591](https://github.com/nlohmann/json/issues/1591) +- dump\_escaped possible error ? [\#1589](https://github.com/nlohmann/json/issues/1589) +- json::parse\(\) into a vector\ results in unhandled exception [\#1587](https://github.com/nlohmann/json/issues/1587) +- push\_back\(\)/emplace\_back\(\) on array invalidates pointers to existing array items [\#1586](https://github.com/nlohmann/json/issues/1586) +- Getting nlohmann::detail::parse\_error on JSON generated by nlohmann::json not sure why [\#1583](https://github.com/nlohmann/json/issues/1583) +- getting error terminate called after throwing an instance of 'std::domain\_error' what\(\): cannot use at\(\) with string [\#1582](https://github.com/nlohmann/json/issues/1582) +- how i create json file [\#1581](https://github.com/nlohmann/json/issues/1581) +- prevent rounding of double datatype values [\#1580](https://github.com/nlohmann/json/issues/1580) +- Documentation Container Overview Doesn't Reference Const Methods [\#1579](https://github.com/nlohmann/json/issues/1579) +- Writing an array into a nlohmann::json object [\#1578](https://github.com/nlohmann/json/issues/1578) +- compilation error when using with another library [\#1577](https://github.com/nlohmann/json/issues/1577) +- Homebrew on OSX doesn't install cmake config file [\#1576](https://github.com/nlohmann/json/issues/1576) +- `unflatten` vs objects with number-ish keys [\#1575](https://github.com/nlohmann/json/issues/1575) +- JSON Parse Out of Range Error [\#1574](https://github.com/nlohmann/json/issues/1574) +- Integrating into existing CMake Project [\#1573](https://github.com/nlohmann/json/issues/1573) +- A "thinner" source code tar as part of release? [\#1572](https://github.com/nlohmann/json/issues/1572) +- conversion to std::string failed [\#1571](https://github.com/nlohmann/json/issues/1571) +- jPtr operation does not throw [\#1569](https://github.com/nlohmann/json/issues/1569) +- How to generate dll file for this project [\#1568](https://github.com/nlohmann/json/issues/1568) +- how to pass variable data to json in c [\#1567](https://github.com/nlohmann/json/issues/1567) +- I want to achieve an upgraded function. [\#1566](https://github.com/nlohmann/json/issues/1566) +- How to determine the type of elements read from a JSON array? [\#1564](https://github.com/nlohmann/json/issues/1564) +- try\_get\_to [\#1563](https://github.com/nlohmann/json/issues/1563) +- example code compile error [\#1562](https://github.com/nlohmann/json/issues/1562) +- How to iterate over nested json object [\#1561](https://github.com/nlohmann/json/issues/1561) +- Build Option/Separate Function to Allow to Throw on Duplicate Keys [\#1560](https://github.com/nlohmann/json/issues/1560) +- Compiler Switches -Weffc++ & -Wshadow are throwing errors [\#1558](https://github.com/nlohmann/json/issues/1558) +- warning: use of the 'nodiscard' attribute is a C++17 extension [\#1557](https://github.com/nlohmann/json/issues/1557) +- Import/Export compressed JSON files [\#1556](https://github.com/nlohmann/json/issues/1556) +- GDB renderers for json library [\#1554](https://github.com/nlohmann/json/issues/1554) +- Is it possible to construct a json string object from a binary buffer? [\#1553](https://github.com/nlohmann/json/issues/1553) +- json objects in list [\#1552](https://github.com/nlohmann/json/issues/1552) +- Matrix output [\#1550](https://github.com/nlohmann/json/issues/1550) +- Using json merge\_patch on ordered non-alphanumeric datasets [\#1549](https://github.com/nlohmann/json/issues/1549) +- Invalid parsed value for big integer [\#1548](https://github.com/nlohmann/json/issues/1548) +- Integrating with android ndk issues. [\#1547](https://github.com/nlohmann/json/issues/1547) +- add noexcept json::value\("key", default\) method variant? [\#1546](https://github.com/nlohmann/json/issues/1546) +- Thank you! 🙌 [\#1545](https://github.com/nlohmann/json/issues/1545) +- Output and input matrix [\#1544](https://github.com/nlohmann/json/issues/1544) +- Add regression tests for MSVC [\#1543](https://github.com/nlohmann/json/issues/1543) +- \[Help Needed!\] Season of Docs [\#1542](https://github.com/nlohmann/json/issues/1542) +- program still abort\(\) or exit\(\) with try catch [\#1541](https://github.com/nlohmann/json/issues/1541) +- Have a json::type\_error exception because of JSON object [\#1540](https://github.com/nlohmann/json/issues/1540) +- Using versioned namespaces [\#1539](https://github.com/nlohmann/json/issues/1539) +- Quoted numbers [\#1538](https://github.com/nlohmann/json/issues/1538) +- Reading a JSON file into an object [\#1537](https://github.com/nlohmann/json/issues/1537) +- Releases 3.6.0 and 3.6.1 don't build on conda / windows [\#1536](https://github.com/nlohmann/json/issues/1536) +- \[Clang\] warning: use of the 'nodiscard' attribute is a C++17 extension \[-Wc++17-extensions\] [\#1535](https://github.com/nlohmann/json/issues/1535) +- wchar\_t/std::wstring json can be created but not accessed [\#1533](https://github.com/nlohmann/json/issues/1533) +- json stringify [\#1532](https://github.com/nlohmann/json/issues/1532) +- How can I use std::string\_view as the json\_key to "operator \[\]" ? [\#1529](https://github.com/nlohmann/json/issues/1529) +- How can I use it from gcc on RPI [\#1528](https://github.com/nlohmann/json/issues/1528) +- std::pair treated as an array instead of key-value in `std::vector\\>` [\#1520](https://github.com/nlohmann/json/issues/1520) +- Excessive Memory Usage for Large Json File [\#1516](https://github.com/nlohmann/json/issues/1516) +- SAX dumper [\#1512](https://github.com/nlohmann/json/issues/1512) +- Conversion to user type containing a std::vector not working with documented approach [\#1511](https://github.com/nlohmann/json/issues/1511) +- How to get position info or parser context with custom from\_json\(\) that may throw exceptions? [\#1508](https://github.com/nlohmann/json/issues/1508) +- Inconsistent use of type alias. [\#1507](https://github.com/nlohmann/json/issues/1507) +- Is there a current way to represent strings as json int? [\#1503](https://github.com/nlohmann/json/issues/1503) +- Intermittent issues with loadJSON [\#1484](https://github.com/nlohmann/json/issues/1484) +- use json construct std::string [\#1462](https://github.com/nlohmann/json/issues/1462) +- JSON Creation [\#1461](https://github.com/nlohmann/json/issues/1461) +- Substantial performance penalty caused by polymorphic input adapter [\#1457](https://github.com/nlohmann/json/issues/1457) +- Null bytes in files are treated like EOF [\#1095](https://github.com/nlohmann/json/issues/1095) +- Feature: to\_string\(const json& j\); [\#916](https://github.com/nlohmann/json/issues/916) + +- Use GNUInstallDirs instead of hard-coded path. [\#1673](https://github.com/nlohmann/json/pull/1673) ([remyabel](https://github.com/remyabel)) +- Package Manager: MSYS2 \(pacman\) [\#1670](https://github.com/nlohmann/json/pull/1670) ([podsvirov](https://github.com/podsvirov)) +- Fix json.hpp compilation issue with other typedefs with same name \(Issue \#1642\) [\#1643](https://github.com/nlohmann/json/pull/1643) ([kevinlul](https://github.com/kevinlul)) +- Add explicit conversion from json to std::string\_view in conversion unit test [\#1639](https://github.com/nlohmann/json/pull/1639) ([taylorhoward92](https://github.com/taylorhoward92)) +- Minor fixes in docs [\#1625](https://github.com/nlohmann/json/pull/1625) ([nickaein](https://github.com/nickaein)) +- Fix broken links to documentation [\#1598](https://github.com/nlohmann/json/pull/1598) ([nickaein](https://github.com/nickaein)) +- Added to\_string and added basic tests [\#1585](https://github.com/nlohmann/json/pull/1585) ([Macr0Nerd](https://github.com/Macr0Nerd)) +- Regression tests for MSVC [\#1570](https://github.com/nlohmann/json/pull/1570) ([nickaein](https://github.com/nickaein)) +- Fix/1511 [\#1555](https://github.com/nlohmann/json/pull/1555) ([theodelrieu](https://github.com/theodelrieu)) +- Remove C++17 extension warning from clang; \#1535 [\#1551](https://github.com/nlohmann/json/pull/1551) ([heavywatal](https://github.com/heavywatal)) +- moved from Catch to doctest for unit tests [\#1439](https://github.com/nlohmann/json/pull/1439) ([onqtam](https://github.com/onqtam)) + +## [v3.6.1](https://github.com/nlohmann/json/releases/tag/v3.6.1) (2019-03-20) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.6.0...v3.6.1) + +- Failed to build with \ [\#1531](https://github.com/nlohmann/json/issues/1531) +- Compiling 3.6.0 with GCC \> 7, array vs std::array \#590 is back [\#1530](https://github.com/nlohmann/json/issues/1530) +- 3.6.0: warning: missing initializer for member 'std::array\::\_M\_elems' \[-Wmissing-field-initializers\] [\#1527](https://github.com/nlohmann/json/issues/1527) +- unable to parse json [\#1525](https://github.com/nlohmann/json/issues/1525) + +## [v3.6.0](https://github.com/nlohmann/json/releases/tag/v3.6.0) (2019-03-19) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.5.0...v3.6.0) + +- How can I turn a string of a json array into a json array? [\#1526](https://github.com/nlohmann/json/issues/1526) +- Minor: missing a std:: namespace tag [\#1521](https://github.com/nlohmann/json/issues/1521) +- how to precision to four decimal for double when use to\_json [\#1519](https://github.com/nlohmann/json/issues/1519) +- error parse [\#1518](https://github.com/nlohmann/json/issues/1518) +- Compile error: template argument deduction/substitution failed [\#1515](https://github.com/nlohmann/json/issues/1515) +- Support for Comments [\#1513](https://github.com/nlohmann/json/issues/1513) +- std::complex type [\#1510](https://github.com/nlohmann/json/issues/1510) +- CBOR byte string support [\#1509](https://github.com/nlohmann/json/issues/1509) +- Compilation error getting a std::pair\<\> on latest VS 2017 compiler [\#1506](https://github.com/nlohmann/json/issues/1506) +- "Integration" section of documentation needs update? [\#1505](https://github.com/nlohmann/json/issues/1505) +- Json object from string from a TCP socket [\#1504](https://github.com/nlohmann/json/issues/1504) +- MSVC warning C4946 \("reinterpret\_cast used between related classes"\) compiling json.hpp [\#1502](https://github.com/nlohmann/json/issues/1502) +- How to programmatically fill an n-th dimensional JSON object? [\#1501](https://github.com/nlohmann/json/issues/1501) +- Error compiling with clang and `JSON\_NOEXCEPTION`: need to include `cstdlib` [\#1500](https://github.com/nlohmann/json/issues/1500) +- The code compiles unsuccessfully with android-ndk-r10e [\#1499](https://github.com/nlohmann/json/issues/1499) +- Cmake 3.1 in develop, when is it likely to make it into a stable release? [\#1498](https://github.com/nlohmann/json/issues/1498) +- Repository is almost 450MB [\#1497](https://github.com/nlohmann/json/issues/1497) +- Some Help please object inside array [\#1494](https://github.com/nlohmann/json/issues/1494) +- How to get data into vector of user-defined type from a Json object [\#1493](https://github.com/nlohmann/json/issues/1493) +- how to find subelement without loop [\#1490](https://github.com/nlohmann/json/issues/1490) +- json to std::map [\#1487](https://github.com/nlohmann/json/issues/1487) +- Type in README.md [\#1486](https://github.com/nlohmann/json/issues/1486) +- Error in parsing and reading msgpack-lite [\#1485](https://github.com/nlohmann/json/issues/1485) +- Compiling issues with libc 2.12 [\#1483](https://github.com/nlohmann/json/issues/1483) +- How do I use reference or pointer binding values? [\#1482](https://github.com/nlohmann/json/issues/1482) +- Compilation fails in MSVC with the Microsoft Language Extensions disabled [\#1481](https://github.com/nlohmann/json/issues/1481) +- Functional visit [\#1480](https://github.com/nlohmann/json/issues/1480) +- \[Question\] Unescaped dump [\#1479](https://github.com/nlohmann/json/issues/1479) +- Some Help please [\#1478](https://github.com/nlohmann/json/issues/1478) +- Global variables are stored within the JSON file, how do I declare them as global variables when I read them out in my C++ program? [\#1476](https://github.com/nlohmann/json/issues/1476) +- Unable to modify one of the values within the JSON file, and save it [\#1475](https://github.com/nlohmann/json/issues/1475) +- Documentation of parse function has two identical @pre causes [\#1473](https://github.com/nlohmann/json/issues/1473) +- GCC 9.0 build failure [\#1472](https://github.com/nlohmann/json/issues/1472) +- Can we have an `exists\(\)` method? [\#1471](https://github.com/nlohmann/json/issues/1471) +- How to parse multi object json from file? [\#1470](https://github.com/nlohmann/json/issues/1470) +- How to returns the name of the upper object? [\#1467](https://github.com/nlohmann/json/issues/1467) +- Error: "tuple\_size" has already been declared in the current scope [\#1466](https://github.com/nlohmann/json/issues/1466) +- Checking keys of two jsons against eachother [\#1465](https://github.com/nlohmann/json/issues/1465) +- Disable installation when used as meson subproject [\#1463](https://github.com/nlohmann/json/issues/1463) +- Unpack list of integers to a std::vector\ [\#1460](https://github.com/nlohmann/json/issues/1460) +- Implement DRY definition of JSON representation of a c++ class [\#1459](https://github.com/nlohmann/json/issues/1459) +- json.exception.type\_error.305 with GCC 4.9 when using C++ {} initializer [\#1458](https://github.com/nlohmann/json/issues/1458) +- API to convert an "uninitialized" json into an empty object or empty array [\#1456](https://github.com/nlohmann/json/issues/1456) +- How to parse a vector of objects with const attributes [\#1453](https://github.com/nlohmann/json/issues/1453) +- NLOHMANN\_JSON\_SERIALIZE\_ENUM potentially requires duplicate definitions [\#1450](https://github.com/nlohmann/json/issues/1450) +- Question about making json object from file directory [\#1449](https://github.com/nlohmann/json/issues/1449) +- .get\(\) throws error if used with userdefined structs in unordered\_map [\#1448](https://github.com/nlohmann/json/issues/1448) +- Integer Overflow \(OSS-Fuzz 12506\) [\#1447](https://github.com/nlohmann/json/issues/1447) +- If a string has too many invalid UTF-8 characters, json::dump attempts to index an array out of bounds. [\#1445](https://github.com/nlohmann/json/issues/1445) +- Setting values of .JSON file [\#1444](https://github.com/nlohmann/json/issues/1444) +- alias object\_t::key\_type in basic\_json [\#1442](https://github.com/nlohmann/json/issues/1442) +- Latest Ubuntu package is 2.1.1 [\#1438](https://github.com/nlohmann/json/issues/1438) +- lexer.hpp\(1363\) '\_snprintf': is not a member | Visualstudio 2017 [\#1437](https://github.com/nlohmann/json/issues/1437) +- Static method invites inadvertent logic error. [\#1433](https://github.com/nlohmann/json/issues/1433) +- EOS compilation produces "fatal error: 'nlohmann/json.hpp' file not found" [\#1432](https://github.com/nlohmann/json/issues/1432) +- Support for bad commas [\#1429](https://github.com/nlohmann/json/issues/1429) +- Please have one base exception class for all json exceptions [\#1427](https://github.com/nlohmann/json/issues/1427) +- Compilation warning: 'tuple\_size' defined as a class template here but previously declared as a struct template [\#1426](https://github.com/nlohmann/json/issues/1426) +- Which version can be used with GCC 4.8.2 ? [\#1424](https://github.com/nlohmann/json/issues/1424) +- Ignore nullptr values on constructing json object from a container [\#1422](https://github.com/nlohmann/json/issues/1422) +- Support for custom float precision via unquoted strings [\#1421](https://github.com/nlohmann/json/issues/1421) +- Segmentation fault \(stack overflow\) due to unbounded recursion [\#1419](https://github.com/nlohmann/json/issues/1419) +- It is possible to call `json::find` with a json\_pointer as argument. This causes runtime UB/crash. [\#1418](https://github.com/nlohmann/json/issues/1418) +- Dump throwing exception [\#1416](https://github.com/nlohmann/json/issues/1416) +- Build error [\#1415](https://github.com/nlohmann/json/issues/1415) +- Append version to include.zip [\#1412](https://github.com/nlohmann/json/issues/1412) +- error C2039: '\_snprintf': is not a member of 'std' - Windows [\#1408](https://github.com/nlohmann/json/issues/1408) +- Deserializing to vector [\#1407](https://github.com/nlohmann/json/issues/1407) +- Efficient way to set a `json` object as value into another `json` key [\#1406](https://github.com/nlohmann/json/issues/1406) +- Document return value of parse\(\) when allow\_exceptions == false and parsing fails [\#1405](https://github.com/nlohmann/json/issues/1405) +- Unexpected behaviour with structured binding [\#1404](https://github.com/nlohmann/json/issues/1404) +- Which native types does get\\(\) allow? [\#1403](https://github.com/nlohmann/json/issues/1403) +- Add something like Json::StaticString [\#1402](https://github.com/nlohmann/json/issues/1402) +- -Wmismatched-tags in 3.5.0? [\#1401](https://github.com/nlohmann/json/issues/1401) +- Coverity Scan reports an UNCAUGHT\_EXCEPT issue [\#1400](https://github.com/nlohmann/json/issues/1400) +- fff [\#1399](https://github.com/nlohmann/json/issues/1399) +- sorry this is not an issue, just a Question, How to change a key value in a file and save it ? [\#1398](https://github.com/nlohmann/json/issues/1398) +- Add library versioning using inline namespaces [\#1394](https://github.com/nlohmann/json/issues/1394) +- appveyor x64 builds appear to be using Win32 toolset [\#1374](https://github.com/nlohmann/json/issues/1374) +- Serializing/Deserializing a Class containing a vector of itself [\#1373](https://github.com/nlohmann/json/issues/1373) +- Retrieving array elements. [\#1369](https://github.com/nlohmann/json/issues/1369) +- Deserialize [\#1366](https://github.com/nlohmann/json/issues/1366) +- call of overloaded for push\_back and operator+= is ambiguous [\#1352](https://github.com/nlohmann/json/issues/1352) +- got an error and cann't figure it out [\#1351](https://github.com/nlohmann/json/issues/1351) +- Improve number-to-string conversion [\#1334](https://github.com/nlohmann/json/issues/1334) +- Implicit type conversion error on MSVC [\#1333](https://github.com/nlohmann/json/issues/1333) +- NuGet Package [\#1132](https://github.com/nlohmann/json/issues/1132) + +- Change macros to numeric\_limits [\#1514](https://github.com/nlohmann/json/pull/1514) ([naszta](https://github.com/naszta)) +- fix GCC 7.1.1 - 7.2.1 on CentOS [\#1496](https://github.com/nlohmann/json/pull/1496) ([lieff](https://github.com/lieff)) +- Update Buckaroo instructions in README.md [\#1495](https://github.com/nlohmann/json/pull/1495) ([njlr](https://github.com/njlr)) +- Fix gcc9 build error test/src/unit-allocator.cpp \(Issue \#1472\) [\#1492](https://github.com/nlohmann/json/pull/1492) ([stac47](https://github.com/stac47)) +- Fix typo in README.md [\#1491](https://github.com/nlohmann/json/pull/1491) ([nickaein](https://github.com/nickaein)) +- Do proper endian conversions [\#1489](https://github.com/nlohmann/json/pull/1489) ([andreas-schwab](https://github.com/andreas-schwab)) +- Fix documentation [\#1477](https://github.com/nlohmann/json/pull/1477) ([nickaein](https://github.com/nickaein)) +- Implement contains\(\) member function [\#1474](https://github.com/nlohmann/json/pull/1474) ([nickaein](https://github.com/nickaein)) +- Add operator/= and operator/ to construct a JSON pointer by appending two JSON pointers [\#1469](https://github.com/nlohmann/json/pull/1469) ([garethsb-sony](https://github.com/garethsb-sony)) +- Disable Clang -Wmismatched-tags warning on tuple\_size / tuple\_element [\#1468](https://github.com/nlohmann/json/pull/1468) ([past-due](https://github.com/past-due)) +- Disable installation when used as meson subproject. \#1463 [\#1464](https://github.com/nlohmann/json/pull/1464) ([elvisoric](https://github.com/elvisoric)) +- docs: README typo [\#1455](https://github.com/nlohmann/json/pull/1455) ([wythe](https://github.com/wythe)) +- remove extra semicolon from readme [\#1451](https://github.com/nlohmann/json/pull/1451) ([Afforix](https://github.com/Afforix)) +- attempt to fix \#1445, flush buffer in serializer::dump\_escaped in UTF8\_REJECT case. [\#1446](https://github.com/nlohmann/json/pull/1446) ([scinart](https://github.com/scinart)) +- Use C++11 features supported by CMake 3.1. [\#1441](https://github.com/nlohmann/json/pull/1441) ([iwanders](https://github.com/iwanders)) +- :rotating\_light: fixed unused variable warning [\#1435](https://github.com/nlohmann/json/pull/1435) ([pboettch](https://github.com/pboettch)) +- allow push\_back\(\) and pop\_back\(\) calls on json\_pointer [\#1434](https://github.com/nlohmann/json/pull/1434) ([pboettch](https://github.com/pboettch)) +- Add instructions about using nlohmann/json with the conda package manager [\#1430](https://github.com/nlohmann/json/pull/1430) ([nicoddemus](https://github.com/nicoddemus)) +- Updated year in README.md [\#1425](https://github.com/nlohmann/json/pull/1425) ([hijxf](https://github.com/hijxf)) +- Fixed broken links in the README file [\#1423](https://github.com/nlohmann/json/pull/1423) ([skypjack](https://github.com/skypjack)) +- Fixed broken links in the README file [\#1420](https://github.com/nlohmann/json/pull/1420) ([skypjack](https://github.com/skypjack)) +- docs: typo in README [\#1417](https://github.com/nlohmann/json/pull/1417) ([wythe](https://github.com/wythe)) +- Fix x64 target platform for appveyor [\#1414](https://github.com/nlohmann/json/pull/1414) ([nickaein](https://github.com/nickaein)) +- Improve dump\_integer performance [\#1411](https://github.com/nlohmann/json/pull/1411) ([nickaein](https://github.com/nickaein)) +- buildsystem: relax requirement on cmake version [\#1409](https://github.com/nlohmann/json/pull/1409) ([yann-morin-1998](https://github.com/yann-morin-1998)) +- CMake: Optional Install if Embedded [\#1330](https://github.com/nlohmann/json/pull/1330) ([ax3l](https://github.com/ax3l)) + +## [v3.5.0](https://github.com/nlohmann/json/releases/tag/v3.5.0) (2018-12-21) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.4.0...v3.5.0) + +- Copyconstructor inserts original into array with single element [\#1397](https://github.com/nlohmann/json/issues/1397) +- Get value without explicit typecasting [\#1395](https://github.com/nlohmann/json/issues/1395) +- Big file parsing [\#1393](https://github.com/nlohmann/json/issues/1393) +- some static analysis warning at line 11317 [\#1390](https://github.com/nlohmann/json/issues/1390) +- Adding Structured Binding Support [\#1388](https://github.com/nlohmann/json/issues/1388) +- map\ exhibits unexpected behavior [\#1387](https://github.com/nlohmann/json/issues/1387) +- Error Code Return [\#1386](https://github.com/nlohmann/json/issues/1386) +- using unordered\_map as object type [\#1385](https://github.com/nlohmann/json/issues/1385) +- float precision [\#1384](https://github.com/nlohmann/json/issues/1384) +- \[json.exception.type\_error.316\] invalid UTF-8 byte at index 1: 0xC3 [\#1383](https://github.com/nlohmann/json/issues/1383) +- Inconsistent Constructor \(GCC vs. Clang\) [\#1381](https://github.com/nlohmann/json/issues/1381) +- \#define or || [\#1379](https://github.com/nlohmann/json/issues/1379) +- How to iterate inside the values ? [\#1377](https://github.com/nlohmann/json/issues/1377) +- items\(\) unable to get the elements [\#1375](https://github.com/nlohmann/json/issues/1375) +- conversion json to std::map doesn't work for types \ [\#1372](https://github.com/nlohmann/json/issues/1372) +- A minor issue in the build instructions [\#1371](https://github.com/nlohmann/json/issues/1371) +- Using this library without stream ? [\#1370](https://github.com/nlohmann/json/issues/1370) +- Writing and reading BSON data [\#1368](https://github.com/nlohmann/json/issues/1368) +- Retrieving array elements from object type iterator. [\#1367](https://github.com/nlohmann/json/issues/1367) +- json::dump\(\) silently crashes if items contain accented letters [\#1365](https://github.com/nlohmann/json/issues/1365) +- warnings in MSVC \(2015\) in 3.4.0 related to bool... [\#1364](https://github.com/nlohmann/json/issues/1364) +- Cant compile with -C++17 and beyond compiler options [\#1362](https://github.com/nlohmann/json/issues/1362) +- json to concrete type conversion through reference or pointer fails [\#1361](https://github.com/nlohmann/json/issues/1361) +- the first attributes of JSON string is misplaced [\#1360](https://github.com/nlohmann/json/issues/1360) +- Copy-construct using initializer-list converts objects to arrays [\#1359](https://github.com/nlohmann/json/issues/1359) +- About value\(key, default\_value\) and operator\[\]\(key\) [\#1358](https://github.com/nlohmann/json/issues/1358) +- Problem with printing json response object [\#1356](https://github.com/nlohmann/json/issues/1356) +- Serializing pointer segfaults [\#1355](https://github.com/nlohmann/json/issues/1355) +- Read `long long int` data as a number. [\#1354](https://github.com/nlohmann/json/issues/1354) +- eclipse oxygen in ubuntu get\ is ambiguous [\#1353](https://github.com/nlohmann/json/issues/1353) +- Can't build on Visual Studio 2017 v15.8.9 [\#1350](https://github.com/nlohmann/json/issues/1350) +- cannot parse from string? [\#1349](https://github.com/nlohmann/json/issues/1349) +- Error: out\_of\_range [\#1348](https://github.com/nlohmann/json/issues/1348) +- expansion pattern 'CompatibleObjectType' contains no argument packs, with CUDA 10 [\#1347](https://github.com/nlohmann/json/issues/1347) +- Unable to update a value for a nested\(multi-level\) json file [\#1344](https://github.com/nlohmann/json/issues/1344) +- Fails to compile when std::iterator\_traits is not SFINAE friendly. [\#1341](https://github.com/nlohmann/json/issues/1341) +- EOF flag not set on exhausted input streams. [\#1340](https://github.com/nlohmann/json/issues/1340) +- Shadowed Member in merge\_patch [\#1339](https://github.com/nlohmann/json/issues/1339) +- Periods/literal dots in keys? [\#1338](https://github.com/nlohmann/json/issues/1338) +- Protect macro expansion of commonly defined macros [\#1337](https://github.com/nlohmann/json/issues/1337) +- How to validate an input before parsing? [\#1336](https://github.com/nlohmann/json/issues/1336) +- Non-verifying dump\(\) alternative for debugging/logging needed [\#1335](https://github.com/nlohmann/json/issues/1335) +- Json Libarary is not responding for me in c++ [\#1332](https://github.com/nlohmann/json/issues/1332) +- Question - how to find an object in an array [\#1331](https://github.com/nlohmann/json/issues/1331) +- Nesting additional data in json object [\#1328](https://github.com/nlohmann/json/issues/1328) +- can to\_json\(\) be defined inside a class? [\#1324](https://github.com/nlohmann/json/issues/1324) +- CodeBlocks IDE can't find `json.hpp` header [\#1318](https://github.com/nlohmann/json/issues/1318) +- Change json\_pointer to provide an iterator begin/end/etc, don't use vectors, and also enable string\_view [\#1312](https://github.com/nlohmann/json/issues/1312) +- Xcode - adding it to library [\#1300](https://github.com/nlohmann/json/issues/1300) +- unicode: accept char16\_t, char32\_t sequences [\#1298](https://github.com/nlohmann/json/issues/1298) +- unicode: char16\_t\* is compiler error, but char16\_t\[\] is accepted [\#1297](https://github.com/nlohmann/json/issues/1297) +- Dockerfile Project Help Needed [\#1296](https://github.com/nlohmann/json/issues/1296) +- Comparisons between large unsigned and negative signed integers [\#1295](https://github.com/nlohmann/json/issues/1295) +- CMake alias to `nlohmann::json` [\#1291](https://github.com/nlohmann/json/issues/1291) +- Release zips without tests [\#1285](https://github.com/nlohmann/json/issues/1285) +- Suggestion to improve value\(\) accessors with respect to move semantics [\#1275](https://github.com/nlohmann/json/issues/1275) +- separate object\_t::key\_type from basic\_json::key\_type, and use an allocator which returns object\_t::key\_type [\#1274](https://github.com/nlohmann/json/issues/1274) +- Is there a nice way to associate external values with json elements? [\#1256](https://github.com/nlohmann/json/issues/1256) +- Delete by json\_pointer [\#1248](https://github.com/nlohmann/json/issues/1248) +- Expose lexer, as a StAX parser [\#1219](https://github.com/nlohmann/json/issues/1219) +- Subclassing json\(\) & error on recursive load [\#1201](https://github.com/nlohmann/json/issues/1201) +- Check value for existence by json\_pointer [\#1194](https://github.com/nlohmann/json/issues/1194) + +- Feature/add file input adapter [\#1392](https://github.com/nlohmann/json/pull/1392) ([dumarjo](https://github.com/dumarjo)) +- Added Support for Structured Bindings [\#1391](https://github.com/nlohmann/json/pull/1391) ([pratikpc](https://github.com/pratikpc)) +- Link to issue \#958 broken [\#1382](https://github.com/nlohmann/json/pull/1382) ([kjpus](https://github.com/kjpus)) +- readme: fix typo [\#1380](https://github.com/nlohmann/json/pull/1380) ([manu-chroma](https://github.com/manu-chroma)) +- recommend using explicit from JSON conversions [\#1363](https://github.com/nlohmann/json/pull/1363) ([theodelrieu](https://github.com/theodelrieu)) +- Fix merge\_patch shadow warning [\#1346](https://github.com/nlohmann/json/pull/1346) ([ax3l](https://github.com/ax3l)) +- Allow installation via Meson [\#1345](https://github.com/nlohmann/json/pull/1345) ([mpoquet](https://github.com/mpoquet)) +- Set eofbit on exhausted input stream. [\#1343](https://github.com/nlohmann/json/pull/1343) ([mefyl](https://github.com/mefyl)) +- Add a SFINAE friendly iterator\_traits and use that instead. [\#1342](https://github.com/nlohmann/json/pull/1342) ([dgavedissian](https://github.com/dgavedissian)) +- Fix EOL Whitespaces & CMake Spelling [\#1329](https://github.com/nlohmann/json/pull/1329) ([ax3l](https://github.com/ax3l)) + +## [v3.4.0](https://github.com/nlohmann/json/releases/tag/v3.4.0) (2018-10-30) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.3.0...v3.4.0) + +- Big uint64\_t values are serialized wrong [\#1327](https://github.com/nlohmann/json/issues/1327) +- \[Question\] Efficient check for equivalency? [\#1325](https://github.com/nlohmann/json/issues/1325) +- Can't use ifstream and .clear\(\) [\#1321](https://github.com/nlohmann/json/issues/1321) +- \[Warning\] -Wparentheses on line 555 on single\_include [\#1319](https://github.com/nlohmann/json/issues/1319) +- Compilation error using at and find with enum struct [\#1316](https://github.com/nlohmann/json/issues/1316) +- Parsing JSON from a web address [\#1311](https://github.com/nlohmann/json/issues/1311) +- How to convert JSON to Struct with embeded subject [\#1310](https://github.com/nlohmann/json/issues/1310) +- Null safety/coalescing function? [\#1309](https://github.com/nlohmann/json/issues/1309) +- Building fails using single include file: json.hpp [\#1308](https://github.com/nlohmann/json/issues/1308) +- json::parse\(std::string\) Exception inside packaged Lib [\#1306](https://github.com/nlohmann/json/issues/1306) +- Problem in Dockerfile with installation of library [\#1304](https://github.com/nlohmann/json/issues/1304) +- compile error in from\_json converting to container with std::pair [\#1299](https://github.com/nlohmann/json/issues/1299) +- Json that I am trying to parse, and I am lost Structure Array below top level [\#1293](https://github.com/nlohmann/json/issues/1293) +- Serializing std::variant causes stack overflow [\#1292](https://github.com/nlohmann/json/issues/1292) +- How do I go about customising from\_json to support \_\_int128\_t/\_\_uint128\_t? [\#1290](https://github.com/nlohmann/json/issues/1290) +- merge\_patch: inconsistent behaviour merging empty sub-object [\#1289](https://github.com/nlohmann/json/issues/1289) +- Buffer over/underrun using UBJson? [\#1288](https://github.com/nlohmann/json/issues/1288) +- Enable the latest C++ standard with Visual Studio [\#1287](https://github.com/nlohmann/json/issues/1287) +- truncation of constant value in to\_cbor\(\) [\#1286](https://github.com/nlohmann/json/issues/1286) +- eosio.wasmsdk error [\#1284](https://github.com/nlohmann/json/issues/1284) +- use the same interface for writing arrays and non-arrays [\#1283](https://github.com/nlohmann/json/issues/1283) +- How to read json file with optional entries and entries with different types [\#1281](https://github.com/nlohmann/json/issues/1281) +- merge result not as espected [\#1279](https://github.com/nlohmann/json/issues/1279) +- how to get only "name" from below json [\#1278](https://github.com/nlohmann/json/issues/1278) +- syntax error on right json string [\#1276](https://github.com/nlohmann/json/issues/1276) +- Parsing JSON Array where members have no key, using custom types [\#1267](https://github.com/nlohmann/json/issues/1267) +- I get a json exception periodically from json::parse for the same json [\#1263](https://github.com/nlohmann/json/issues/1263) +- serialize std::variant\<...\> [\#1261](https://github.com/nlohmann/json/issues/1261) +- GCC 8.2.1. Compilation error: invalid conversion from... [\#1246](https://github.com/nlohmann/json/issues/1246) +- BSON support [\#1244](https://github.com/nlohmann/json/issues/1244) +- enum to json mapping [\#1208](https://github.com/nlohmann/json/issues/1208) +- Soften the landing when dumping non-UTF8 strings \(type\_error.316 exception\) [\#1198](https://github.com/nlohmann/json/issues/1198) +- CMakeLists.txt in release zips? [\#1184](https://github.com/nlohmann/json/issues/1184) + +- Add macro to define enum/JSON mapping [\#1323](https://github.com/nlohmann/json/pull/1323) ([nlohmann](https://github.com/nlohmann)) +- Add BSON support [\#1320](https://github.com/nlohmann/json/pull/1320) ([nlohmann](https://github.com/nlohmann)) +- Properly convert constants to CharType [\#1315](https://github.com/nlohmann/json/pull/1315) ([nlohmann](https://github.com/nlohmann)) +- Allow to set error handler for decoding errors [\#1314](https://github.com/nlohmann/json/pull/1314) ([nlohmann](https://github.com/nlohmann)) +- Add Meson related info to README [\#1305](https://github.com/nlohmann/json/pull/1305) ([koponomarenko](https://github.com/koponomarenko)) +- Improve diagnostic messages for binary formats [\#1303](https://github.com/nlohmann/json/pull/1303) ([nlohmann](https://github.com/nlohmann)) +- add new is\_constructible\_\* traits used in from\_json [\#1301](https://github.com/nlohmann/json/pull/1301) ([theodelrieu](https://github.com/theodelrieu)) +- add constraints for variadic json\_ref constructors [\#1294](https://github.com/nlohmann/json/pull/1294) ([theodelrieu](https://github.com/theodelrieu)) +- Improve diagnostic messages [\#1282](https://github.com/nlohmann/json/pull/1282) ([nlohmann](https://github.com/nlohmann)) +- Removed linter warnings [\#1280](https://github.com/nlohmann/json/pull/1280) ([nlohmann](https://github.com/nlohmann)) +- Thirdparty benchmark: Fix Clang detection. [\#1277](https://github.com/nlohmann/json/pull/1277) ([Lord-Kamina](https://github.com/Lord-Kamina)) + +## [v3.3.0](https://github.com/nlohmann/json/releases/tag/v3.3.0) (2018-10-05) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.2.0...v3.3.0) + +- When key is not found print the key name into error too [\#1273](https://github.com/nlohmann/json/issues/1273) +- Visual Studio 2017 15.8.5 "conditional expression is constant" warning on Line 1851 in json.hpp [\#1268](https://github.com/nlohmann/json/issues/1268) +- how can we get this working on WSL? [\#1264](https://github.com/nlohmann/json/issues/1264) +- Help needed [\#1259](https://github.com/nlohmann/json/issues/1259) +- A way to get to a JSON values "key" [\#1258](https://github.com/nlohmann/json/issues/1258) +- While compiling got 76 errors [\#1255](https://github.com/nlohmann/json/issues/1255) +- Two blackslashes on json output file [\#1253](https://github.com/nlohmann/json/issues/1253) +- Including nlohmann the badwrong way. [\#1250](https://github.com/nlohmann/json/issues/1250) +- how to build with clang? [\#1247](https://github.com/nlohmann/json/issues/1247) +- Cmake target\_link\_libraries unable to find nlohmann\_json since version 3.2.0 [\#1243](https://github.com/nlohmann/json/issues/1243) +- \[Question\] Access to end\(\) iterator reference [\#1242](https://github.com/nlohmann/json/issues/1242) +- Parsing different json format [\#1241](https://github.com/nlohmann/json/issues/1241) +- Parsing Multiple JSON Files [\#1240](https://github.com/nlohmann/json/issues/1240) +- Doesn't compile under C++17 [\#1239](https://github.com/nlohmann/json/issues/1239) +- Conversion operator for nlohmann::json is not SFINAE friendly [\#1237](https://github.com/nlohmann/json/issues/1237) +- Custom deserialization of number\_float\_t [\#1236](https://github.com/nlohmann/json/issues/1236) +- Move tests to a separate repo [\#1235](https://github.com/nlohmann/json/issues/1235) +- deprecated-declarations warnings when compiling tests with GCC 8.2.1. [\#1233](https://github.com/nlohmann/json/issues/1233) +- Incomplete type with json\_fwd.hpp [\#1232](https://github.com/nlohmann/json/issues/1232) +- Parse Error [\#1229](https://github.com/nlohmann/json/issues/1229) +- json::get function with argument [\#1227](https://github.com/nlohmann/json/issues/1227) +- questions regarding from\_json [\#1226](https://github.com/nlohmann/json/issues/1226) +- Lambda in unevaluated context [\#1225](https://github.com/nlohmann/json/issues/1225) +- NLohmann doesn't compile when enabling strict warning policies [\#1224](https://github.com/nlohmann/json/issues/1224) +- Creating array of objects [\#1223](https://github.com/nlohmann/json/issues/1223) +- Somewhat unhelpful error message "cannot use operator\[\] with object" [\#1220](https://github.com/nlohmann/json/issues/1220) +- single\_include json.hpp [\#1218](https://github.com/nlohmann/json/issues/1218) +- Maps with enum class keys which are convertible to JSON strings should be converted to JSON dictionaries [\#1217](https://github.com/nlohmann/json/issues/1217) +- Adding JSON Array to the Array [\#1216](https://github.com/nlohmann/json/issues/1216) +- Best way to output a vector of a given type to json [\#1215](https://github.com/nlohmann/json/issues/1215) +- compiler warning: double definition of macro JSON\_INTERNAL\_CATCH [\#1213](https://github.com/nlohmann/json/issues/1213) +- Compilation error when using MOCK\_METHOD1 from GMock and nlohmann::json [\#1212](https://github.com/nlohmann/json/issues/1212) +- Issues parsing a previously encoded binary \(non-UTF8\) string. [\#1211](https://github.com/nlohmann/json/issues/1211) +- Yet another ordering question: char \* and parse\(\) [\#1209](https://github.com/nlohmann/json/issues/1209) +- Error using gcc 8.1.0 on Ubuntu 14.04 [\#1207](https://github.com/nlohmann/json/issues/1207) +- "type must be string, but is " std::string\(j.type\_name\(\) [\#1206](https://github.com/nlohmann/json/issues/1206) +- Returning empty json object from a function of type const json& ? [\#1205](https://github.com/nlohmann/json/issues/1205) +- VS2017 compiler suggests using constexpr if [\#1204](https://github.com/nlohmann/json/issues/1204) +- Template instatiation error on compiling [\#1203](https://github.com/nlohmann/json/issues/1203) +- BUG - json dump field with unicode -\> array of ints \(instead of string\) [\#1197](https://github.com/nlohmann/json/issues/1197) +- Compile error using Code::Blocks // mingw-w64 GCC 8.1.0 - "Incomplete Type" [\#1193](https://github.com/nlohmann/json/issues/1193) +- SEGFAULT on arm target [\#1190](https://github.com/nlohmann/json/issues/1190) +- Compiler crash with old Clang [\#1179](https://github.com/nlohmann/json/issues/1179) +- Custom Precision on floating point numbers [\#1170](https://github.com/nlohmann/json/issues/1170) +- Can we have a json\_view class like std::string\_view? [\#1158](https://github.com/nlohmann/json/issues/1158) +- improve error handling [\#1152](https://github.com/nlohmann/json/issues/1152) +- We should remove static\_asserts [\#960](https://github.com/nlohmann/json/issues/960) + +- Fix warning C4127: conditional expression is constant [\#1272](https://github.com/nlohmann/json/pull/1272) ([antonioborondo](https://github.com/antonioborondo)) +- Turn off additional deprecation warnings for GCC. [\#1271](https://github.com/nlohmann/json/pull/1271) ([chuckatkins](https://github.com/chuckatkins)) +- docs: Add additional CMake documentation [\#1270](https://github.com/nlohmann/json/pull/1270) ([chuckatkins](https://github.com/chuckatkins)) +- unit-testsuites.cpp: fix hangup if file not found [\#1262](https://github.com/nlohmann/json/pull/1262) ([knilch0r](https://github.com/knilch0r)) +- Fix broken cmake imported target alias [\#1260](https://github.com/nlohmann/json/pull/1260) ([chuckatkins](https://github.com/chuckatkins)) +- GCC 48 [\#1257](https://github.com/nlohmann/json/pull/1257) ([henryiii](https://github.com/henryiii)) +- Add version and license to meson.build [\#1252](https://github.com/nlohmann/json/pull/1252) ([koponomarenko](https://github.com/koponomarenko)) +- \#1179 Reordered the code. It seems to stop clang 3.4.2 in RHEL 7 from crash… [\#1249](https://github.com/nlohmann/json/pull/1249) ([LEgregius](https://github.com/LEgregius)) +- Use a version check to provide backwards comatible CMake imported target names [\#1245](https://github.com/nlohmann/json/pull/1245) ([chuckatkins](https://github.com/chuckatkins)) +- Fix issue \#1237 [\#1238](https://github.com/nlohmann/json/pull/1238) ([theodelrieu](https://github.com/theodelrieu)) +- Add a get overload taking a parameter. [\#1231](https://github.com/nlohmann/json/pull/1231) ([theodelrieu](https://github.com/theodelrieu)) +- Move lambda out of unevaluated context [\#1230](https://github.com/nlohmann/json/pull/1230) ([mandreyel](https://github.com/mandreyel)) +- Remove static asserts [\#1228](https://github.com/nlohmann/json/pull/1228) ([theodelrieu](https://github.com/theodelrieu)) +- Better error 305 [\#1221](https://github.com/nlohmann/json/pull/1221) ([rivertam](https://github.com/rivertam)) +- Fix \#1213 [\#1214](https://github.com/nlohmann/json/pull/1214) ([simnalamburt](https://github.com/simnalamburt)) +- Export package to allow builds without installing [\#1202](https://github.com/nlohmann/json/pull/1202) ([dennisfischer](https://github.com/dennisfischer)) + +## [v3.2.0](https://github.com/nlohmann/json/releases/tag/v3.2.0) (2018-08-20) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.1.2...v3.2.0) + +- Am I doing this wrong? Getting an empty string [\#1199](https://github.com/nlohmann/json/issues/1199) +- Incompatible Pointer Type [\#1196](https://github.com/nlohmann/json/issues/1196) +- json.exception.type\_error.316 [\#1195](https://github.com/nlohmann/json/issues/1195) +- Strange warnings in Code::Blocks 17.12, GNU GCC [\#1192](https://github.com/nlohmann/json/issues/1192) +- \[Question\] Current place in code to change floating point resolution [\#1191](https://github.com/nlohmann/json/issues/1191) +- Add key name when throwing type error [\#1189](https://github.com/nlohmann/json/issues/1189) +- Not able to include in visual studio code? [\#1188](https://github.com/nlohmann/json/issues/1188) +- Get an Index or row number of an element [\#1186](https://github.com/nlohmann/json/issues/1186) +- reduce repos size [\#1185](https://github.com/nlohmann/json/issues/1185) +- Difference between `merge\_patch` and `update` [\#1183](https://github.com/nlohmann/json/issues/1183) +- Is there a way to get an element from a JSON without throwing an exception on failure? [\#1182](https://github.com/nlohmann/json/issues/1182) +- to\_string? [\#1181](https://github.com/nlohmann/json/issues/1181) +- How to cache a json object's pointer into a map? [\#1180](https://github.com/nlohmann/json/issues/1180) +- Can this library work within a Qt project for Android using Qt Creator? [\#1178](https://github.com/nlohmann/json/issues/1178) +- How to get all keys of one object? [\#1177](https://github.com/nlohmann/json/issues/1177) +- How can I only parse the first level and get the value as string? [\#1175](https://github.com/nlohmann/json/issues/1175) +- I have a query regarding nlohmann::basic\_json::basic\_json [\#1174](https://github.com/nlohmann/json/issues/1174) +- unordered\_map with vectors won't convert to json? [\#1173](https://github.com/nlohmann/json/issues/1173) +- return json objects from functions [\#1172](https://github.com/nlohmann/json/issues/1172) +- Problem when exporting to CBOR [\#1171](https://github.com/nlohmann/json/issues/1171) +- Roundtripping null to nullptr does not work [\#1169](https://github.com/nlohmann/json/issues/1169) +- MSVC fails to compile std::swap specialization for nlohmann::json [\#1168](https://github.com/nlohmann/json/issues/1168) +- Unexpected behaviour of is\_null - Part II [\#1167](https://github.com/nlohmann/json/issues/1167) +- Floating point imprecision [\#1166](https://github.com/nlohmann/json/issues/1166) +- Combine json objects into one? [\#1165](https://github.com/nlohmann/json/issues/1165) +- Is there any way to know if the object has changed? [\#1164](https://github.com/nlohmann/json/issues/1164) +- Value throws on null string [\#1163](https://github.com/nlohmann/json/issues/1163) +- Weird template issue in large project [\#1162](https://github.com/nlohmann/json/issues/1162) +- \_json returns a different result vs ::parse [\#1161](https://github.com/nlohmann/json/issues/1161) +- Showing difference between two json objects [\#1160](https://github.com/nlohmann/json/issues/1160) +- no instance of overloaded function "std::swap" matches the specified type [\#1159](https://github.com/nlohmann/json/issues/1159) +- resize\(...\)? [\#1157](https://github.com/nlohmann/json/issues/1157) +- Issue with struct nested in class' to\_json [\#1155](https://github.com/nlohmann/json/issues/1155) +- Deserialize std::map with std::nan [\#1154](https://github.com/nlohmann/json/issues/1154) +- Parse throwing errors [\#1149](https://github.com/nlohmann/json/issues/1149) +- cocoapod integration [\#1148](https://github.com/nlohmann/json/issues/1148) +- wstring parsing [\#1147](https://github.com/nlohmann/json/issues/1147) +- Is it possible to dump a two-dimensional array to "\[\[null\],\[1,2,3\]\]"? [\#1146](https://github.com/nlohmann/json/issues/1146) +- Want to write a class member variable and a struct variable \( this structure is inside the class\) to the json file [\#1145](https://github.com/nlohmann/json/issues/1145) +- Does json support converting an instance of a struct into json string? [\#1143](https://github.com/nlohmann/json/issues/1143) +- \#Most efficient way to search for child parameters \(recursive find?\) [\#1141](https://github.com/nlohmann/json/issues/1141) +- could not find to\_json\(\) method in T's namespace [\#1140](https://github.com/nlohmann/json/issues/1140) +- chars get treated as JSON numbers not JSON strings [\#1139](https://github.com/nlohmann/json/issues/1139) +- How do I count number of objects in array? [\#1137](https://github.com/nlohmann/json/issues/1137) +- Serializing a vector of classes? [\#1136](https://github.com/nlohmann/json/issues/1136) +- Compile error. Unable convert form nullptr to nullptr&& [\#1135](https://github.com/nlohmann/json/issues/1135) +- std::unordered\_map in struct, serialization [\#1133](https://github.com/nlohmann/json/issues/1133) +- dump\(\) can't handle umlauts [\#1131](https://github.com/nlohmann/json/issues/1131) +- Add a way to get a key reference from the iterator [\#1127](https://github.com/nlohmann/json/issues/1127) +- can't not parse "\\“ string [\#1123](https://github.com/nlohmann/json/issues/1123) +- if json file contain Internationalization chars , get exception [\#1122](https://github.com/nlohmann/json/issues/1122) +- How to use a json::iterator dereferenced value in code? [\#1120](https://github.com/nlohmann/json/issues/1120) +- clang compiler: error : unknown type name 'not' [\#1119](https://github.com/nlohmann/json/issues/1119) +- Disable implicit conversions from json to std::initializer\_list\ for any T [\#1118](https://github.com/nlohmann/json/issues/1118) +- Implicit conversions to complex types can lead to surprising and confusing errors [\#1116](https://github.com/nlohmann/json/issues/1116) +- How can I write from\_json for a complex datatype that is not default constructible? [\#1115](https://github.com/nlohmann/json/issues/1115) +- Compile error in VS2015 when compiling unit-conversions.cpp [\#1114](https://github.com/nlohmann/json/issues/1114) +- ADL Serializer for std::any / boost::any [\#1113](https://github.com/nlohmann/json/issues/1113) +- Unexpected behaviour of is\_null [\#1112](https://github.com/nlohmann/json/issues/1112) +- How to resolve " undefined reference to `std::\_\_throw\_bad\_cast\(\)'" [\#1111](https://github.com/nlohmann/json/issues/1111) +- cannot compile on ubuntu 18.04 and 16.04 [\#1110](https://github.com/nlohmann/json/issues/1110) +- JSON representation for floating point values has too many digits [\#1109](https://github.com/nlohmann/json/issues/1109) +- Not working for classes containing "\_declspec\(dllimport\)" in their declaration [\#1108](https://github.com/nlohmann/json/issues/1108) +- Get keys from json object [\#1107](https://github.com/nlohmann/json/issues/1107) +- dump\(\) without alphabetical order [\#1106](https://github.com/nlohmann/json/issues/1106) +- Cannot deserialize types using std::ratio [\#1105](https://github.com/nlohmann/json/issues/1105) +- i want to learn json [\#1104](https://github.com/nlohmann/json/issues/1104) +- Type checking during compile [\#1103](https://github.com/nlohmann/json/issues/1103) +- Iterate through sub items [\#1102](https://github.com/nlohmann/json/issues/1102) +- cppcheck failing for version 3.1.2 [\#1101](https://github.com/nlohmann/json/issues/1101) +- Deserializing std::map [\#1100](https://github.com/nlohmann/json/issues/1100) +- accessing key by reference [\#1098](https://github.com/nlohmann/json/issues/1098) +- clang 3.8.0 croaks while trying to compile with debug symbols [\#1097](https://github.com/nlohmann/json/issues/1097) +- Serialize a list of class objects with json [\#1096](https://github.com/nlohmann/json/issues/1096) +- Small question [\#1094](https://github.com/nlohmann/json/issues/1094) +- Upgrading to 3.x: to\_/from\_json with enum class [\#1093](https://github.com/nlohmann/json/issues/1093) +- Q: few questions about json construction [\#1092](https://github.com/nlohmann/json/issues/1092) +- general crayCC compilation failure [\#1091](https://github.com/nlohmann/json/issues/1091) +- Merge Patch clears original data [\#1090](https://github.com/nlohmann/json/issues/1090) +- \[Question\] how to use nlohmann/json in c++? [\#1088](https://github.com/nlohmann/json/issues/1088) +- C++17 decomposition declaration support [\#1087](https://github.com/nlohmann/json/issues/1087) +- \[Question\] Access multi-level json objects [\#1086](https://github.com/nlohmann/json/issues/1086) +- Serializing vector [\#1085](https://github.com/nlohmann/json/issues/1085) +- update nested value in multi hierarchy json object [\#1084](https://github.com/nlohmann/json/issues/1084) +- Overriding default values? [\#1083](https://github.com/nlohmann/json/issues/1083) +- detail namespace collision with Cereal? [\#1082](https://github.com/nlohmann/json/issues/1082) +- Error using json.dump\(\); [\#1081](https://github.com/nlohmann/json/issues/1081) +- Consuming TCP Stream [\#1080](https://github.com/nlohmann/json/issues/1080) +- Compilation error with strong typed enums in map in combination with namespaces [\#1079](https://github.com/nlohmann/json/issues/1079) +- cassert error [\#1076](https://github.com/nlohmann/json/issues/1076) +- Valid json data not being parsed [\#1075](https://github.com/nlohmann/json/issues/1075) +- Feature request :: Better testing for key existance without try/catch [\#1074](https://github.com/nlohmann/json/issues/1074) +- Hi, I have input like a.b.c and want to convert it to \"a\"{\"b\": \"c\"} form. Any suggestions how do I do this? Thanks. [\#1073](https://github.com/nlohmann/json/issues/1073) +- ADL deserializer not picked up for non default-constructible type [\#1072](https://github.com/nlohmann/json/issues/1072) +- Deserializing std::array doesn't compiler \(no insert\(\)\) [\#1071](https://github.com/nlohmann/json/issues/1071) +- Serializing OpenCV Mat problem [\#1070](https://github.com/nlohmann/json/issues/1070) +- Compilation error with ICPC compiler [\#1068](https://github.com/nlohmann/json/issues/1068) +- Minimal branch? [\#1066](https://github.com/nlohmann/json/issues/1066) +- Not existing value, crash [\#1065](https://github.com/nlohmann/json/issues/1065) +- cyryllic symbols [\#1064](https://github.com/nlohmann/json/issues/1064) +- newbie usage question [\#1063](https://github.com/nlohmann/json/issues/1063) +- Trying j\["strTest"\] = "%A" produces "strTest": "-0X1.CCCCCCCCCCCCCP+205" [\#1062](https://github.com/nlohmann/json/issues/1062) +- convert json value to std::string??? [\#1061](https://github.com/nlohmann/json/issues/1061) +- Commented out test cases, should they be removed? [\#1060](https://github.com/nlohmann/json/issues/1060) +- different behaviour between clang and gcc with braced initialization [\#1059](https://github.com/nlohmann/json/issues/1059) +- json array: initialize with prescribed size and `resize` method. [\#1057](https://github.com/nlohmann/json/issues/1057) +- Is it possible to use exceptions istead of assertions? [\#1056](https://github.com/nlohmann/json/issues/1056) +- when using assign operator in with json object a static assertion fails.. [\#1055](https://github.com/nlohmann/json/issues/1055) +- Iterate over leafs of a JSON data structure: enrich the JSON pointer API [\#1054](https://github.com/nlohmann/json/issues/1054) +- \[Feature request\] Access by path [\#1053](https://github.com/nlohmann/json/issues/1053) +- document that implicit js -\> primitive conversion does not work for std::string::value\_type and why [\#1052](https://github.com/nlohmann/json/issues/1052) +- error: ‘BasicJsonType’ in namespace ‘::’ does not name a type [\#1051](https://github.com/nlohmann/json/issues/1051) +- Destructor is called when filling object through assignement [\#1050](https://github.com/nlohmann/json/issues/1050) +- Is this thing thread safe for reads? [\#1049](https://github.com/nlohmann/json/issues/1049) +- clang-tidy: Call to virtual function during construction [\#1046](https://github.com/nlohmann/json/issues/1046) +- Using STL algorithms with JSON containers with expected results? [\#1045](https://github.com/nlohmann/json/issues/1045) +- Usage with gtest/gmock not working as expected [\#1044](https://github.com/nlohmann/json/issues/1044) +- Consequences of from\_json / to\_json being in namespace of data struct. [\#1042](https://github.com/nlohmann/json/issues/1042) +- const\_reference operator\[\]\(const typename object\_t::key\_type& key\) const throw instead of assert [\#1039](https://github.com/nlohmann/json/issues/1039) +- Trying to retrieve data from nested objects [\#1038](https://github.com/nlohmann/json/issues/1038) +- Direct download link for json\_fwd.hpp? [\#1037](https://github.com/nlohmann/json/issues/1037) +- I know the library supports UTF-8, but failed to dump the value [\#1036](https://github.com/nlohmann/json/issues/1036) +- Putting a Vec3-like vector into a json object [\#1035](https://github.com/nlohmann/json/issues/1035) +- Ternary operator crash [\#1034](https://github.com/nlohmann/json/issues/1034) +- Issued with Clion Inspection Resolution since 2018.1 [\#1033](https://github.com/nlohmann/json/issues/1033) +- Some testcases fail and one never finishes [\#1032](https://github.com/nlohmann/json/issues/1032) +- Can this class work with wchar\_t / std::wstring? [\#1031](https://github.com/nlohmann/json/issues/1031) +- Makefile: Valgrind flags have no effect [\#1030](https://github.com/nlohmann/json/issues/1030) +- 「==〠Should be 「\>〠[\#1029](https://github.com/nlohmann/json/issues/1029) +- HOCON reader? [\#1027](https://github.com/nlohmann/json/issues/1027) +- add json string in previous string?? [\#1025](https://github.com/nlohmann/json/issues/1025) +- RFC: fluent parsing interface [\#1023](https://github.com/nlohmann/json/issues/1023) +- Does it support chinese character? [\#1022](https://github.com/nlohmann/json/issues/1022) +- to/from\_msgpack only works with standard typization [\#1021](https://github.com/nlohmann/json/issues/1021) +- Build failure using latest clang and GCC compilers [\#1020](https://github.com/nlohmann/json/issues/1020) +- can two json objects be concatenated? [\#1019](https://github.com/nlohmann/json/issues/1019) +- Erase by integer index [\#1018](https://github.com/nlohmann/json/issues/1018) +- Function find overload taking a json\_pointer [\#1017](https://github.com/nlohmann/json/issues/1017) +- I think should implement an parser function [\#1016](https://github.com/nlohmann/json/issues/1016) +- Readme gif [\#1015](https://github.com/nlohmann/json/issues/1015) +- Python bindings [\#1014](https://github.com/nlohmann/json/issues/1014) +- how to add two json string in single object?? [\#1012](https://github.com/nlohmann/json/issues/1012) +- how to serialize class Object \(convert data in object into json\)?? [\#1011](https://github.com/nlohmann/json/issues/1011) +- Enable forward declaration of json by making json a class instead of a using declaration [\#997](https://github.com/nlohmann/json/issues/997) +- compilation error while using intel c++ compiler 2018 [\#994](https://github.com/nlohmann/json/issues/994) +- How to create a json variable? [\#990](https://github.com/nlohmann/json/issues/990) +- istream \>\> json --- 1st character skipped in stream [\#976](https://github.com/nlohmann/json/issues/976) +- Add a SAX parser [\#971](https://github.com/nlohmann/json/issues/971) +- Add Key name to Exception [\#932](https://github.com/nlohmann/json/issues/932) +- How to solve large json file? [\#927](https://github.com/nlohmann/json/issues/927) +- json\_pointer public push\_back, pop\_back [\#837](https://github.com/nlohmann/json/issues/837) +- Using input\_adapter in a slightly unexpected way [\#834](https://github.com/nlohmann/json/issues/834) +- Stack-overflow \(OSS-Fuzz 4234\) [\#832](https://github.com/nlohmann/json/issues/832) + +- Fix -Wno-sometimes-uninitialized by initializing "result" in parse\_sax [\#1200](https://github.com/nlohmann/json/pull/1200) ([thyu](https://github.com/thyu)) +- \[RFC\] Introduce a new macro function: JSON\_INTERNAL\_CATCH [\#1187](https://github.com/nlohmann/json/pull/1187) ([simnalamburt](https://github.com/simnalamburt)) +- Fix unit tests that were silently skipped or crashed \(depending on the compiler\) [\#1176](https://github.com/nlohmann/json/pull/1176) ([grembo](https://github.com/grembo)) +- Refactor/no virtual sax [\#1153](https://github.com/nlohmann/json/pull/1153) ([theodelrieu](https://github.com/theodelrieu)) +- Fixed compiler error in VS 2015 for debug mode [\#1151](https://github.com/nlohmann/json/pull/1151) ([sonulohani](https://github.com/sonulohani)) +- Fix links to cppreference named requirements \(formerly concepts\) [\#1144](https://github.com/nlohmann/json/pull/1144) ([jrakow](https://github.com/jrakow)) +- meson: fix include directory [\#1142](https://github.com/nlohmann/json/pull/1142) ([jrakow](https://github.com/jrakow)) +- Feature/unordered map conversion [\#1138](https://github.com/nlohmann/json/pull/1138) ([theodelrieu](https://github.com/theodelrieu)) +- fixed compile error for \#1045 [\#1134](https://github.com/nlohmann/json/pull/1134) ([Daniel599](https://github.com/Daniel599)) +- test \(non\)equality for alt\_string implementation [\#1130](https://github.com/nlohmann/json/pull/1130) ([agrianius](https://github.com/agrianius)) +- remove stringstream dependency [\#1117](https://github.com/nlohmann/json/pull/1117) ([TinyTinni](https://github.com/TinyTinni)) +- Provide a from\_json overload for std::map [\#1089](https://github.com/nlohmann/json/pull/1089) ([theodelrieu](https://github.com/theodelrieu)) +- fix typo in README [\#1078](https://github.com/nlohmann/json/pull/1078) ([martin-mfg](https://github.com/martin-mfg)) +- Fix typo [\#1058](https://github.com/nlohmann/json/pull/1058) ([dns13](https://github.com/dns13)) +- Misc cmake packaging enhancements [\#1048](https://github.com/nlohmann/json/pull/1048) ([chuckatkins](https://github.com/chuckatkins)) +- Fixed incorrect LLVM version number in README [\#1047](https://github.com/nlohmann/json/pull/1047) ([jammehcow](https://github.com/jammehcow)) +- Fix trivial typo in comment. [\#1043](https://github.com/nlohmann/json/pull/1043) ([coryan](https://github.com/coryan)) +- Package Manager: Spack [\#1041](https://github.com/nlohmann/json/pull/1041) ([ax3l](https://github.com/ax3l)) +- CMake: 3.8+ is Sufficient [\#1040](https://github.com/nlohmann/json/pull/1040) ([ax3l](https://github.com/ax3l)) +- Added support for string\_view in C++17 [\#1028](https://github.com/nlohmann/json/pull/1028) ([gracicot](https://github.com/gracicot)) +- Added public target\_compile\_features for auto and constexpr [\#1026](https://github.com/nlohmann/json/pull/1026) ([ktonon](https://github.com/ktonon)) + +## [v3.1.2](https://github.com/nlohmann/json/releases/tag/v3.1.2) (2018-03-14) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.1.1...v3.1.2) + +- STL containers are always serialized to a nested array like \[\[1,2,3\]\] [\#1013](https://github.com/nlohmann/json/issues/1013) +- The library doesn't want to insert an unordered\_map [\#1010](https://github.com/nlohmann/json/issues/1010) +- Convert Json to uint8\_t [\#1008](https://github.com/nlohmann/json/issues/1008) +- How to compare two JSON objects? [\#1007](https://github.com/nlohmann/json/issues/1007) +- Syntax checking [\#1003](https://github.com/nlohmann/json/issues/1003) +- more than one operator '=' matches these operands [\#1002](https://github.com/nlohmann/json/issues/1002) +- How to check if key existed [\#1000](https://github.com/nlohmann/json/issues/1000) +- nlohmann::json::parse exhaust memory in go binding [\#999](https://github.com/nlohmann/json/issues/999) +- Range-based iteration over a non-array object [\#998](https://github.com/nlohmann/json/issues/998) +- get\ for types that are not default constructible [\#996](https://github.com/nlohmann/json/issues/996) +- Prevent Null values to appear in .dump\(\) [\#995](https://github.com/nlohmann/json/issues/995) +- number parsing [\#993](https://github.com/nlohmann/json/issues/993) +- C2664 \(C++/CLR\) cannot convert 'nullptr' to 'nullptr &&' [\#987](https://github.com/nlohmann/json/issues/987) +- Uniform initialization from another json object differs between gcc and clang. [\#985](https://github.com/nlohmann/json/issues/985) +- Problem with adding the lib as a submodule [\#983](https://github.com/nlohmann/json/issues/983) +- UTF-8/Unicode error [\#982](https://github.com/nlohmann/json/issues/982) +- "forcing MSVC stacktrace to show which T we're talking about." error [\#980](https://github.com/nlohmann/json/issues/980) +- reverse order of serialization [\#979](https://github.com/nlohmann/json/issues/979) +- Assigning between different json types [\#977](https://github.com/nlohmann/json/issues/977) +- Support serialisation of `unique\_ptr\<\>` and `shared\_ptr\<\>` [\#975](https://github.com/nlohmann/json/issues/975) +- Unexpected end of input \(not same as one before\) [\#974](https://github.com/nlohmann/json/issues/974) +- Segfault on direct initializing json object [\#973](https://github.com/nlohmann/json/issues/973) +- Segmentation fault on G++ when trying to assign json string literal to custom json type. [\#972](https://github.com/nlohmann/json/issues/972) +- os\_defines.h:44:19: error: missing binary operator before token "\(" [\#970](https://github.com/nlohmann/json/issues/970) +- Passing an iteration object by reference to a function [\#967](https://github.com/nlohmann/json/issues/967) +- Json and fmt::lib's format\_arg\(\) [\#964](https://github.com/nlohmann/json/issues/964) + +- Allowing for user-defined string type in lexer/parser [\#1009](https://github.com/nlohmann/json/pull/1009) ([nlohmann](https://github.com/nlohmann)) +- dump to alternative string type, as defined in basic\_json template [\#1006](https://github.com/nlohmann/json/pull/1006) ([agrianius](https://github.com/agrianius)) +- Fix memory leak during parser callback [\#1001](https://github.com/nlohmann/json/pull/1001) ([nlohmann](https://github.com/nlohmann)) +- fixed misprinted condition detected by PVS Studio. [\#992](https://github.com/nlohmann/json/pull/992) ([bogemic](https://github.com/bogemic)) +- Fix/basic json conversion [\#986](https://github.com/nlohmann/json/pull/986) ([theodelrieu](https://github.com/theodelrieu)) +- Make integration section concise [\#981](https://github.com/nlohmann/json/pull/981) ([wla80](https://github.com/wla80)) + +## [v3.1.1](https://github.com/nlohmann/json/releases/tag/v3.1.1) (2018-02-13) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.1.0...v3.1.1) + +- Updation of child object isn't reflected in parent Object [\#968](https://github.com/nlohmann/json/issues/968) +- How to add user defined C++ path to sublime text [\#966](https://github.com/nlohmann/json/issues/966) +- fast number parsing [\#965](https://github.com/nlohmann/json/issues/965) +- With non-unique keys, later stored entries are not taken into account anymore [\#963](https://github.com/nlohmann/json/issues/963) +- Timeout \(OSS-Fuzz 6034\) [\#962](https://github.com/nlohmann/json/issues/962) +- Incorrect parsing of indefinite length CBOR strings. [\#961](https://github.com/nlohmann/json/issues/961) +- Reload a json file at runtime without emptying my std::ifstream [\#959](https://github.com/nlohmann/json/issues/959) +- Split headers should be part of the release [\#956](https://github.com/nlohmann/json/issues/956) +- Coveralls shows no coverage data [\#953](https://github.com/nlohmann/json/issues/953) +- Feature request: Implicit conversion to bool [\#951](https://github.com/nlohmann/json/issues/951) +- converting json to vector of type with templated constructor [\#924](https://github.com/nlohmann/json/issues/924) +- No structured bindings support? [\#901](https://github.com/nlohmann/json/issues/901) +- \[Request\] Macro generating from\_json\(\) and to\_json\(\) [\#895](https://github.com/nlohmann/json/issues/895) +- basic\_json::value throws exception instead of returning default value [\#871](https://github.com/nlohmann/json/issues/871) + +- Fix constraints on from\_json\(CompatibleArrayType\) [\#969](https://github.com/nlohmann/json/pull/969) ([theodelrieu](https://github.com/theodelrieu)) +- Make coveralls watch the include folder [\#957](https://github.com/nlohmann/json/pull/957) ([theodelrieu](https://github.com/theodelrieu)) +- Fix links in README.md [\#955](https://github.com/nlohmann/json/pull/955) ([patrikhuber](https://github.com/patrikhuber)) +- Add a note about installing the library with cget [\#954](https://github.com/nlohmann/json/pull/954) ([pfultz2](https://github.com/pfultz2)) + +## [v3.1.0](https://github.com/nlohmann/json/releases/tag/v3.1.0) (2018-02-01) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.0.1...v3.1.0) + +- Order of the elements in JSON object [\#952](https://github.com/nlohmann/json/issues/952) +- I have a proposal [\#949](https://github.com/nlohmann/json/issues/949) +- VERSION define\(s\) [\#948](https://github.com/nlohmann/json/issues/948) +- v3.0.1 compile error in icc 16.0.4 [\#947](https://github.com/nlohmann/json/issues/947) +- Use in VS2017 15.5.5 [\#946](https://github.com/nlohmann/json/issues/946) +- Process for reporting Security Bugs? [\#945](https://github.com/nlohmann/json/issues/945) +- Please expose a NLOHMANN\_JSON\_VERSION macro [\#943](https://github.com/nlohmann/json/issues/943) +- Change header include directory to nlohmann/json [\#942](https://github.com/nlohmann/json/issues/942) +- string\_type in binary\_reader [\#941](https://github.com/nlohmann/json/issues/941) +- compile error with clang 5.0 -std=c++1z and no string\_view [\#939](https://github.com/nlohmann/json/issues/939) +- Allow overriding JSON\_THROW to something else than abort\(\) [\#938](https://github.com/nlohmann/json/issues/938) +- Handle invalid string in Json file [\#937](https://github.com/nlohmann/json/issues/937) +- Unused variable 'kMinExp' [\#935](https://github.com/nlohmann/json/issues/935) +- yytext is already defined [\#933](https://github.com/nlohmann/json/issues/933) +- Equality operator fails [\#931](https://github.com/nlohmann/json/issues/931) +- use in visual studio 2015 [\#929](https://github.com/nlohmann/json/issues/929) +- Relative includes of json\_fwd.hpp in detail/meta.hpp. \[Develop branch\] [\#928](https://github.com/nlohmann/json/issues/928) +- GCC 7.x issue [\#926](https://github.com/nlohmann/json/issues/926) +- json\_fwd.hpp not installed [\#923](https://github.com/nlohmann/json/issues/923) +- Use Google Benchmarks [\#921](https://github.com/nlohmann/json/issues/921) +- Move class json\_pointer to separate file [\#920](https://github.com/nlohmann/json/issues/920) +- Unable to locate 'to\_json\(\)' and 'from\_json\(\)' methods in the same namespace [\#917](https://github.com/nlohmann/json/issues/917) +- \[answered\]Read key1 from .value example [\#914](https://github.com/nlohmann/json/issues/914) +- Don't use `define private public` in test files [\#913](https://github.com/nlohmann/json/issues/913) +- value\(\) template argument type deduction [\#912](https://github.com/nlohmann/json/issues/912) +- Installation path is incorrect [\#910](https://github.com/nlohmann/json/issues/910) +- H [\#909](https://github.com/nlohmann/json/issues/909) +- Build failure using clang 5 [\#908](https://github.com/nlohmann/json/issues/908) +- Amalgate [\#907](https://github.com/nlohmann/json/issues/907) +- Update documentation and tests wrt. split headers [\#906](https://github.com/nlohmann/json/issues/906) +- Lib not working on ubuntu 16.04 [\#905](https://github.com/nlohmann/json/issues/905) +- Problem when writing to file. [\#904](https://github.com/nlohmann/json/issues/904) +- C2864 error when compiling with VS2015 and VS 2017 [\#903](https://github.com/nlohmann/json/issues/903) +- \[json.exception.type\_error.304\] cannot use at\(\) with object [\#902](https://github.com/nlohmann/json/issues/902) +- How do I forward nlohmann::json declaration? [\#899](https://github.com/nlohmann/json/issues/899) +- How to effectively store binary data? [\#898](https://github.com/nlohmann/json/issues/898) +- How to get the length of a JSON string without retrieving its std::string? [\#897](https://github.com/nlohmann/json/issues/897) +- Regression Tests Failure using "ctest" [\#887](https://github.com/nlohmann/json/issues/887) +- Discuss: add JSON Merge Patch \(RFC 7396\)? [\#877](https://github.com/nlohmann/json/issues/877) +- Discuss: replace static "iterator\_wrapper" function with "items" member function [\#874](https://github.com/nlohmann/json/issues/874) +- Make optional user-data available in from\_json [\#864](https://github.com/nlohmann/json/issues/864) +- Casting to std::string not working in VS2015 [\#861](https://github.com/nlohmann/json/issues/861) +- Sequential reading of JSON arrays [\#851](https://github.com/nlohmann/json/issues/851) +- Idea: Handle Multimaps Better [\#816](https://github.com/nlohmann/json/issues/816) +- Floating point rounding [\#777](https://github.com/nlohmann/json/issues/777) +- Loss of precision when serializing \ [\#360](https://github.com/nlohmann/json/issues/360) + +- Templatize std::string in binary\_reader \#941 [\#950](https://github.com/nlohmann/json/pull/950) ([kaidokert](https://github.com/kaidokert)) +- fix cmake install directory \(for real this time\) [\#944](https://github.com/nlohmann/json/pull/944) ([theodelrieu](https://github.com/theodelrieu)) +- Allow overriding THROW/CATCH/TRY macros with no-exceptions \#938 [\#940](https://github.com/nlohmann/json/pull/940) ([kaidokert](https://github.com/kaidokert)) +- Removed compiler warning about unused variable 'kMinExp' [\#936](https://github.com/nlohmann/json/pull/936) ([zerodefect](https://github.com/zerodefect)) +- Fix a typo in README.md [\#930](https://github.com/nlohmann/json/pull/930) ([Pipeliner](https://github.com/Pipeliner)) +- Howto installation of json\_fwd.hpp \(fixes \#923\) [\#925](https://github.com/nlohmann/json/pull/925) ([zerodefect](https://github.com/zerodefect)) +- fix sfinae on basic\_json UDT constructor [\#919](https://github.com/nlohmann/json/pull/919) ([theodelrieu](https://github.com/theodelrieu)) +- Floating-point formatting [\#915](https://github.com/nlohmann/json/pull/915) ([abolz](https://github.com/abolz)) +- Fix/cmake install [\#911](https://github.com/nlohmann/json/pull/911) ([theodelrieu](https://github.com/theodelrieu)) +- fix link to the documentation of the emplace function [\#900](https://github.com/nlohmann/json/pull/900) ([Dobiasd](https://github.com/Dobiasd)) +- JSON Merge Patch \(RFC 7396\) [\#876](https://github.com/nlohmann/json/pull/876) ([nlohmann](https://github.com/nlohmann)) +- Refactor/split it [\#700](https://github.com/nlohmann/json/pull/700) ([theodelrieu](https://github.com/theodelrieu)) + +## [v3.0.1](https://github.com/nlohmann/json/releases/tag/v3.0.1) (2017-12-29) +[Full Changelog](https://github.com/nlohmann/json/compare/v3.0.0...v3.0.1) + +- Problem parsing array to global vector [\#896](https://github.com/nlohmann/json/issues/896) +- Invalid RFC6902 copy operation succeeds [\#894](https://github.com/nlohmann/json/issues/894) +- How to rename a key during looping? [\#893](https://github.com/nlohmann/json/issues/893) +- clang++-6.0 \(6.0.0-svn321357-1\) warning [\#892](https://github.com/nlohmann/json/issues/892) +- Make json.hpp aware of the modules TS? [\#891](https://github.com/nlohmann/json/issues/891) +- All enum values not handled in switch cases. \( -Wswitch-enum \) [\#889](https://github.com/nlohmann/json/issues/889) +- JSON Pointer resolve failure resulting in incorrect exception code [\#888](https://github.com/nlohmann/json/issues/888) +- Unexpected nested arrays from std::vector [\#886](https://github.com/nlohmann/json/issues/886) +- erase multiple elements from a json object [\#884](https://github.com/nlohmann/json/issues/884) +- Container function overview in Doxygen is not updated [\#883](https://github.com/nlohmann/json/issues/883) +- How to use this for binary file uploads [\#881](https://github.com/nlohmann/json/issues/881) +- Allow setting JSON\_BuildTests=OFF from parent CMakeLists.txt [\#846](https://github.com/nlohmann/json/issues/846) +- Unit test fails for local-independent str-to-num [\#845](https://github.com/nlohmann/json/issues/845) +- Another idea about type support [\#774](https://github.com/nlohmann/json/issues/774) + +- Includes CTest module/adds BUILD\_TESTING option [\#885](https://github.com/nlohmann/json/pull/885) ([TinyTinni](https://github.com/TinyTinni)) +- Fix MSVC warning C4819 [\#882](https://github.com/nlohmann/json/pull/882) ([erengy](https://github.com/erengy)) +- Merge branch 'develop' into coverity\_scan [\#880](https://github.com/nlohmann/json/pull/880) ([nlohmann](https://github.com/nlohmann)) +- :wrench: Fix up a few more effc++ items [\#858](https://github.com/nlohmann/json/pull/858) ([mattismyname](https://github.com/mattismyname)) + +## [v3.0.0](https://github.com/nlohmann/json/releases/tag/v3.0.0) (2017-12-17) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.1.1...v3.0.0) + +- unicode strings [\#878](https://github.com/nlohmann/json/issues/878) +- Visual Studio 2017 15.5 C++17 std::allocator deprecations [\#872](https://github.com/nlohmann/json/issues/872) +- Typo "excpetion" [\#869](https://github.com/nlohmann/json/issues/869) +- Explicit array example in README.md incorrect [\#867](https://github.com/nlohmann/json/issues/867) +- why don't you release this from Feb. ? [\#865](https://github.com/nlohmann/json/issues/865) +- json::parse throws std::invalid\_argument when processing string generated by json::dump\(\) [\#863](https://github.com/nlohmann/json/issues/863) +- code analysis: potential bug? [\#859](https://github.com/nlohmann/json/issues/859) +- MSVC2017, 15.5 new issues. [\#857](https://github.com/nlohmann/json/issues/857) +- very basic: fetching string value/content without quotes [\#853](https://github.com/nlohmann/json/issues/853) +- Ambiguous function call to get with pointer type and constant json object in VS2015 \(15.4.4\) [\#852](https://github.com/nlohmann/json/issues/852) +- How to put object in the array as a member? [\#850](https://github.com/nlohmann/json/issues/850) +- misclick, please ignore [\#849](https://github.com/nlohmann/json/issues/849) +- Make XML great again. [\#847](https://github.com/nlohmann/json/issues/847) +- Converting to array not working [\#843](https://github.com/nlohmann/json/issues/843) +- Iteration weirdness [\#842](https://github.com/nlohmann/json/issues/842) +- Use reference or pointer as Object value [\#841](https://github.com/nlohmann/json/issues/841) +- Ambiguity in parsing nested maps [\#840](https://github.com/nlohmann/json/issues/840) +- could not find from\_json\(\) method in T's namespace [\#839](https://github.com/nlohmann/json/issues/839) +- Incorrect parse error with binary data in keys? [\#838](https://github.com/nlohmann/json/issues/838) +- using dump\(\) when std::wstring is StringType with VS2017 [\#836](https://github.com/nlohmann/json/issues/836) +- Show the path of the currently parsed value when an error occurs [\#835](https://github.com/nlohmann/json/issues/835) +- Repetitive data type while reading [\#833](https://github.com/nlohmann/json/issues/833) +- Storing multiple types inside map [\#831](https://github.com/nlohmann/json/issues/831) +- Application terminating [\#830](https://github.com/nlohmann/json/issues/830) +- Missing CMake hunter package? [\#828](https://github.com/nlohmann/json/issues/828) +- std::map\ from json object yields C2665: 'std::pair\::pair': none of the 2 overloads could convert all the argument types [\#827](https://github.com/nlohmann/json/issues/827) +- object.dump gives quoted string, want to use .dump\(\) to generate javascripts. [\#826](https://github.com/nlohmann/json/issues/826) +- Assertion failed on \["NoExistKey"\] of an not existing key of const json& [\#825](https://github.com/nlohmann/json/issues/825) +- vs2015 error : static member will remain uninitialized at runtime but use in constant-expressions is supported [\#824](https://github.com/nlohmann/json/issues/824) +- Code Checking Warnings from json.hpp on VS2017 Community [\#821](https://github.com/nlohmann/json/issues/821) +- Missing iostream in try online [\#820](https://github.com/nlohmann/json/issues/820) +- Floating point value loses decimal point during dump [\#818](https://github.com/nlohmann/json/issues/818) +- Conan package for the library [\#817](https://github.com/nlohmann/json/issues/817) +- stream error [\#815](https://github.com/nlohmann/json/issues/815) +- Link error when using find\(\) on the latest commit [\#814](https://github.com/nlohmann/json/issues/814) +- ABI issue with json object between 2 shared libraries [\#813](https://github.com/nlohmann/json/issues/813) +- scan\_string\(\) return token\_type::parse\_error; when parse ansi file [\#812](https://github.com/nlohmann/json/issues/812) +- segfault when using fifo\_map with json [\#810](https://github.com/nlohmann/json/issues/810) +- This shit is shit [\#809](https://github.com/nlohmann/json/issues/809) +- \_finite and \_isnan are no members of "std" [\#808](https://github.com/nlohmann/json/issues/808) +- how to print out the line which causing exception? [\#806](https://github.com/nlohmann/json/issues/806) +- {} uses copy constructor, while = does not [\#805](https://github.com/nlohmann/json/issues/805) +- json.hpp:8955: multiple definition of function that is not defined twice or more. [\#804](https://github.com/nlohmann/json/issues/804) +- \[question\] to\_json for base and derived class [\#803](https://github.com/nlohmann/json/issues/803) +- Misleading error message - unexpected '"' - on incorrect utf-8 symbol [\#802](https://github.com/nlohmann/json/issues/802) +- json data = std::string\_view\("hi"\); doesn't work? [\#801](https://github.com/nlohmann/json/issues/801) +- Thread safety of parse\(\) [\#800](https://github.com/nlohmann/json/issues/800) +- Numbers as strings [\#799](https://github.com/nlohmann/json/issues/799) +- Tests failing on arm [\#797](https://github.com/nlohmann/json/issues/797) +- Using your library \(without modification\) in another library [\#796](https://github.com/nlohmann/json/issues/796) +- Iterating over sub-object [\#794](https://github.com/nlohmann/json/issues/794) +- how to get the json object again from which printed by the method of dump\(\) [\#792](https://github.com/nlohmann/json/issues/792) +- ppa to include source [\#791](https://github.com/nlohmann/json/issues/791) +- Different include paths in macOS and Ubuntu [\#790](https://github.com/nlohmann/json/issues/790) +- Missing break after line 12886 in switch/case [\#789](https://github.com/nlohmann/json/issues/789) +- All unit tests fail? [\#787](https://github.com/nlohmann/json/issues/787) +- More use of move semantics in deserialization [\#786](https://github.com/nlohmann/json/issues/786) +- warning C4706 - Visual Studio 2017 \(/W4\) [\#784](https://github.com/nlohmann/json/issues/784) +- Compile error in clang 5.0 [\#782](https://github.com/nlohmann/json/issues/782) +- Error Installing appium\_lib with Ruby v2.4.2 Due to JSON [\#781](https://github.com/nlohmann/json/issues/781) +- ::get\\(\) fails in new\(er\) release \[MSVC\] [\#780](https://github.com/nlohmann/json/issues/780) +- Type Conversion [\#779](https://github.com/nlohmann/json/issues/779) +- Segfault on nested parsing [\#778](https://github.com/nlohmann/json/issues/778) +- Build warnings: shadowing exception id [\#776](https://github.com/nlohmann/json/issues/776) +- multi-level JSON support. [\#775](https://github.com/nlohmann/json/issues/775) +- SIGABRT on dump\(\) [\#773](https://github.com/nlohmann/json/issues/773) +- \[Question\] Custom StringType template parameter \(possibility for a KeyType template parameter\) [\#772](https://github.com/nlohmann/json/issues/772) +- constexpr ALL the Things! [\#771](https://github.com/nlohmann/json/issues/771) +- error: ‘BasicJsonType’ in namespace ‘::’ does not name a type [\#770](https://github.com/nlohmann/json/issues/770) +- Program calls abort function [\#769](https://github.com/nlohmann/json/issues/769) +- \[Question\] Floating point resolution config during dump\(\) ? [\#768](https://github.com/nlohmann/json/issues/768) +- make check - no test ran [\#767](https://github.com/nlohmann/json/issues/767) +- The library cannot work properly with custom allocator based containers [\#766](https://github.com/nlohmann/json/issues/766) +- Documentation or feature request. [\#763](https://github.com/nlohmann/json/issues/763) +- warnings in msvc about mix/max macro while windows.h is used in the project [\#762](https://github.com/nlohmann/json/issues/762) +- std::signbit ambiguous [\#761](https://github.com/nlohmann/json/issues/761) +- How to use value for std::experimental::optional type? [\#760](https://github.com/nlohmann/json/issues/760) +- Cannot load json file properly [\#759](https://github.com/nlohmann/json/issues/759) +- Compilation error with unordered\_map\< int, int \> [\#758](https://github.com/nlohmann/json/issues/758) +- CBOR string [\#757](https://github.com/nlohmann/json/issues/757) +- Proposal: out\_of\_range should be a subclass of std::out\_of\_range [\#756](https://github.com/nlohmann/json/issues/756) +- Compiling with icpc [\#755](https://github.com/nlohmann/json/issues/755) +- Getter is setting the value to null if the key does not exist [\#754](https://github.com/nlohmann/json/issues/754) +- parsing works sometimes and crashes others [\#752](https://github.com/nlohmann/json/issues/752) +- Static\_assert failed "incompatible pointer type" with Xcode [\#751](https://github.com/nlohmann/json/issues/751) +- user-defined literal operator not found [\#750](https://github.com/nlohmann/json/issues/750) +- getting clean string from it.key\(\) [\#748](https://github.com/nlohmann/json/issues/748) +- Best method for exploring and obtaining values of nested json objects when the names are not known beforehand? [\#747](https://github.com/nlohmann/json/issues/747) +- null char at the end of string [\#746](https://github.com/nlohmann/json/issues/746) +- Incorrect sample for operator \>\> in docs [\#745](https://github.com/nlohmann/json/issues/745) +- User-friendly documentation [\#744](https://github.com/nlohmann/json/issues/744) +- Retrieve all values that match a json path [\#743](https://github.com/nlohmann/json/issues/743) +- Compilation issue with gcc 7.2 [\#742](https://github.com/nlohmann/json/issues/742) +- CMake target nlohmann\_json does not have src into its interface includes [\#741](https://github.com/nlohmann/json/issues/741) +- Error when serializing empty json: type must be string, but is object [\#740](https://github.com/nlohmann/json/issues/740) +- Conversion error for std::map\ [\#739](https://github.com/nlohmann/json/issues/739) +- Dumping Json to file as array [\#738](https://github.com/nlohmann/json/issues/738) +- nesting json objects [\#737](https://github.com/nlohmann/json/issues/737) +- where to find general help? [\#736](https://github.com/nlohmann/json/issues/736) +- Compilation Error on Clang 5.0 Upgrade [\#735](https://github.com/nlohmann/json/issues/735) +- Compilation error with std::map\ on vs 2015 [\#734](https://github.com/nlohmann/json/issues/734) +- Benchmarks for Binary formats [\#733](https://github.com/nlohmann/json/issues/733) +- Move test blobs to a submodule? [\#732](https://github.com/nlohmann/json/issues/732) +- Support \n symbols in json string. [\#731](https://github.com/nlohmann/json/issues/731) +- Project's name is too generic and hard to search for [\#730](https://github.com/nlohmann/json/issues/730) +- Visual Studio 2015 IntelliTrace problems [\#729](https://github.com/nlohmann/json/issues/729) +- How to erase nested objects inside other objects? [\#728](https://github.com/nlohmann/json/issues/728) +- How to prevent alphabetical sorting of data? [\#727](https://github.com/nlohmann/json/issues/727) +- Serialization for CBOR [\#726](https://github.com/nlohmann/json/issues/726) +- Using json Object as value in a map [\#725](https://github.com/nlohmann/json/issues/725) +- std::regex and nlohmann::json value [\#724](https://github.com/nlohmann/json/issues/724) +- Warnings when compiling with VisualStudio 2015 [\#723](https://github.com/nlohmann/json/issues/723) +- Has this lib the unicode \(wstring\) support? [\#722](https://github.com/nlohmann/json/issues/722) +- When will be 3.0 in master? [\#721](https://github.com/nlohmann/json/issues/721) +- Determine the type from error message. [\#720](https://github.com/nlohmann/json/issues/720) +- Compile-Error C2100 \(MS VS2015\) in line 887 json.hpp [\#719](https://github.com/nlohmann/json/issues/719) +- from\_json not working for boost::optional example [\#718](https://github.com/nlohmann/json/issues/718) +- about from\_json and to\_json function [\#717](https://github.com/nlohmann/json/issues/717) +- How to deserialize array with derived objects [\#716](https://github.com/nlohmann/json/issues/716) +- How to detect parse failure? [\#715](https://github.com/nlohmann/json/issues/715) +- Parse throw std::ios\_base::failure exception when failbit set to true [\#714](https://github.com/nlohmann/json/issues/714) +- Is there a way of format just making a pretty print without changing the key's orders ? [\#713](https://github.com/nlohmann/json/issues/713) +- Serialization of array of not same model items [\#712](https://github.com/nlohmann/json/issues/712) +- pointer to json parse vector [\#711](https://github.com/nlohmann/json/issues/711) +- Gtest SEH Exception [\#709](https://github.com/nlohmann/json/issues/709) +- broken from\_json implementation for pair and tuple [\#707](https://github.com/nlohmann/json/issues/707) +- Unevaluated lambda in assert breaks gcc 7 build [\#705](https://github.com/nlohmann/json/issues/705) +- Issues when adding values to firebase database [\#704](https://github.com/nlohmann/json/issues/704) +- Floating point equality - revisited [\#703](https://github.com/nlohmann/json/issues/703) +- Conversion from valarray\ to json fails to build [\#702](https://github.com/nlohmann/json/issues/702) +- internal compiler error \(gcc7\) [\#701](https://github.com/nlohmann/json/issues/701) +- One build system to rule them all [\#698](https://github.com/nlohmann/json/issues/698) +- Generated nlohmann\_jsonConfig.cmake does not set JSON\_INCLUDE\_DIR [\#695](https://github.com/nlohmann/json/issues/695) +- support the Chinese language in json string [\#694](https://github.com/nlohmann/json/issues/694) +- NaN problem within develop branch [\#693](https://github.com/nlohmann/json/issues/693) +- Please post example of specialization for boost::filesystem [\#692](https://github.com/nlohmann/json/issues/692) +- Impossible to do an array of composite objects [\#691](https://github.com/nlohmann/json/issues/691) +- How to save json to file? [\#690](https://github.com/nlohmann/json/issues/690) +- my simple json parser [\#689](https://github.com/nlohmann/json/issues/689) +- problem with new struct parsing syntax [\#688](https://github.com/nlohmann/json/issues/688) +- Parse error while parse the json string contains UTF 8 encoded document bytes string [\#684](https://github.com/nlohmann/json/issues/684) +- \[question\] how to get a string value by pointer [\#683](https://github.com/nlohmann/json/issues/683) +- create json object from string variable [\#681](https://github.com/nlohmann/json/issues/681) +- adl\_serializer and CRTP [\#680](https://github.com/nlohmann/json/issues/680) +- Is there a way to control the precision of serialized floating point numbers? [\#677](https://github.com/nlohmann/json/issues/677) +- Is there a way to get the path of a value? [\#676](https://github.com/nlohmann/json/issues/676) +- Could the parser locate errors to line? [\#675](https://github.com/nlohmann/json/issues/675) +- There is performance inefficiency found by coverity tool json2.1.1/include/nlohmann/json.hpp [\#673](https://github.com/nlohmann/json/issues/673) +- include problem, when cmake on osx [\#672](https://github.com/nlohmann/json/issues/672) +- Operator= ambiguous in C++1z and GCC 7.1.1 [\#670](https://github.com/nlohmann/json/issues/670) +- should't the cmake install target be to nlohman/json.hpp [\#668](https://github.com/nlohmann/json/issues/668) +- deserialise from `std::vector` [\#667](https://github.com/nlohmann/json/issues/667) +- How to iterate? [\#665](https://github.com/nlohmann/json/issues/665) +- could this json lib work on windows? [\#664](https://github.com/nlohmann/json/issues/664) +- How does from\_json work? [\#662](https://github.com/nlohmann/json/issues/662) +- insert\(or merge\) object should replace same key , not ignore [\#661](https://github.com/nlohmann/json/issues/661) +- Why is an object ordering values by Alphabetical Order? [\#660](https://github.com/nlohmann/json/issues/660) +- Parse method doesn't handle newlines. [\#659](https://github.com/nlohmann/json/issues/659) +- Compilation "note" on GCC 6 ARM [\#658](https://github.com/nlohmann/json/issues/658) +- Adding additional push\_back/operator+= rvalue overloads for JSON object [\#657](https://github.com/nlohmann/json/issues/657) +- dump's parameter "ensure\_ascii" creates too long sequences [\#656](https://github.com/nlohmann/json/issues/656) +- Question: parsing `void \*` [\#655](https://github.com/nlohmann/json/issues/655) +- how should I check a string is valid JSON string ? [\#653](https://github.com/nlohmann/json/issues/653) +- Question: thread safety of read only accesses [\#651](https://github.com/nlohmann/json/issues/651) +- Eclipse: Method 'size' could not be resolved [\#649](https://github.com/nlohmann/json/issues/649) +- Update/Add object fields [\#648](https://github.com/nlohmann/json/issues/648) +- No exception raised for Out Of Range input of numbers [\#647](https://github.com/nlohmann/json/issues/647) +- Package Name [\#646](https://github.com/nlohmann/json/issues/646) +- What is the meaning of operator\[\]\(T\* key\) [\#645](https://github.com/nlohmann/json/issues/645) +- Which is the correct way to json objects as parameters to functions? [\#644](https://github.com/nlohmann/json/issues/644) +- Method to get string representations of values [\#642](https://github.com/nlohmann/json/issues/642) +- CBOR serialization of a given JSON value does not serialize [\#641](https://github.com/nlohmann/json/issues/641) +- Are we forced to use "-fexceptions" flag in android ndk project [\#640](https://github.com/nlohmann/json/issues/640) +- Comparison of objects containing floats [\#639](https://github.com/nlohmann/json/issues/639) +- 'localeconv' is not supported by NDK for SDK \<=20 [\#638](https://github.com/nlohmann/json/issues/638) +- \[Question\] cLion integration [\#637](https://github.com/nlohmann/json/issues/637) +- How to construct an iteratable usage in nlohmann json? [\#636](https://github.com/nlohmann/json/issues/636) +- \[Question\] copy assign json-container to vector [\#635](https://github.com/nlohmann/json/issues/635) +- Get size without .dump\(\) [\#634](https://github.com/nlohmann/json/issues/634) +- Segmentation fault when parsing invalid json file [\#633](https://github.com/nlohmann/json/issues/633) +- How to serialize from json to vector\? [\#632](https://github.com/nlohmann/json/issues/632) +- no member named 'thousands\_sep' in 'lconv' [\#631](https://github.com/nlohmann/json/issues/631) +- \[Question\] Any fork for \(the unsupported\) Visual Studio 2012 version? [\#628](https://github.com/nlohmann/json/issues/628) +- Dependency injection in serializer [\#627](https://github.com/nlohmann/json/issues/627) +- from\_json for std::array [\#625](https://github.com/nlohmann/json/issues/625) +- Discussion: How to structure the parsing function families [\#623](https://github.com/nlohmann/json/issues/623) +- Question: How to erase subtree [\#622](https://github.com/nlohmann/json/issues/622) +- Insertion into nested json field [\#621](https://github.com/nlohmann/json/issues/621) +- \[Question\] When using this as git submodule, will it clone the whole thing include test data and benchmark? [\#620](https://github.com/nlohmann/json/issues/620) +- Question: return static json object from function [\#618](https://github.com/nlohmann/json/issues/618) +- icc16 error [\#617](https://github.com/nlohmann/json/issues/617) +- \[-Wdeprecated-declarations\] in row `j \>\> ss;` in file `json.hpp:7405:26` and FAILED unit tests with MinGWx64! [\#616](https://github.com/nlohmann/json/issues/616) +- to\_json for pairs, tuples [\#614](https://github.com/nlohmann/json/issues/614) +- Using uninitialized memory 'buf' in line 11173 v2.1.1? [\#613](https://github.com/nlohmann/json/issues/613) +- How to parse multiple same Keys of JSON and save them? [\#612](https://github.com/nlohmann/json/issues/612) +- "Multiple declarations" error when using types defined with `typedef` [\#611](https://github.com/nlohmann/json/issues/611) +- 2.1.1+ breaks compilation of shared\_ptr\ == 0 [\#610](https://github.com/nlohmann/json/issues/610) +- a bug of inheritance ? [\#608](https://github.com/nlohmann/json/issues/608) +- std::map key conversion with to\_json [\#607](https://github.com/nlohmann/json/issues/607) +- json.hpp:6384:62: error: wrong number of template arguments \(1, should be 2\) [\#606](https://github.com/nlohmann/json/issues/606) +- Incremental parsing: Where's the push version? [\#605](https://github.com/nlohmann/json/issues/605) +- Is there a way to validate the structure of a json object ? [\#604](https://github.com/nlohmann/json/issues/604) +- \[Question\] Issue when using Appveyor when compiling library [\#603](https://github.com/nlohmann/json/issues/603) +- BOM not skipped when using json:parse\(iterator\) [\#602](https://github.com/nlohmann/json/issues/602) +- Use of the binary type in CBOR and Message Pack [\#601](https://github.com/nlohmann/json/issues/601) +- Newbie issue: how does one convert a map in Json back to std::map? [\#600](https://github.com/nlohmann/json/issues/600) +- Plugin system [\#599](https://github.com/nlohmann/json/issues/599) +- Feature request: Comments [\#597](https://github.com/nlohmann/json/issues/597) +- Using custom types for scalars? [\#596](https://github.com/nlohmann/json/issues/596) +- Issues with the arithmetic in iterator and reverse iterator [\#593](https://github.com/nlohmann/json/issues/593) +- not enough examples [\#592](https://github.com/nlohmann/json/issues/592) +- in-class initialization for type 'const T' is not yet implemented [\#591](https://github.com/nlohmann/json/issues/591) +- compiling with gcc 7 -\> error on bool operator \< [\#590](https://github.com/nlohmann/json/issues/590) +- Parsing from stream leads to an array [\#589](https://github.com/nlohmann/json/issues/589) +- Buggy support for binary string data [\#587](https://github.com/nlohmann/json/issues/587) +- C++17's ambiguous conversion [\#586](https://github.com/nlohmann/json/issues/586) +- How does the messagepack encoding/decoding compare to msgpack-cpp in terms of performance? [\#585](https://github.com/nlohmann/json/issues/585) +- is it possible to check existence of a value deep in hierarchy? [\#584](https://github.com/nlohmann/json/issues/584) +- loading from a stream and exceptions [\#582](https://github.com/nlohmann/json/issues/582) +- Visual Studio seems not to have all min\(\) function versions [\#581](https://github.com/nlohmann/json/issues/581) +- Supporting of the json schema [\#580](https://github.com/nlohmann/json/issues/580) +- Stack-overflow \(OSS-Fuzz 1444\) [\#577](https://github.com/nlohmann/json/issues/577) +- Heap-buffer-overflow \(OSS-Fuzz 1400\) [\#575](https://github.com/nlohmann/json/issues/575) +- JSON escape quotes [\#574](https://github.com/nlohmann/json/issues/574) +- error: static\_assert failed [\#573](https://github.com/nlohmann/json/issues/573) +- Storing floats, and round trip serialisation/deserialisation diffs [\#572](https://github.com/nlohmann/json/issues/572) +- JSON.getLong produces inconsistent results [\#571](https://github.com/nlohmann/json/issues/571) +- Request: Object.at\(\) with default return value [\#570](https://github.com/nlohmann/json/issues/570) +- Internal structure gets corrupted while parsing [\#569](https://github.com/nlohmann/json/issues/569) +- create template \ basic\_json from\_cbor\(Iter begin, Iter end\) [\#568](https://github.com/nlohmann/json/issues/568) +- Need to improve ignores.. [\#567](https://github.com/nlohmann/json/issues/567) +- Conan.io [\#566](https://github.com/nlohmann/json/issues/566) +- contradictory documentation regarding json::find [\#565](https://github.com/nlohmann/json/issues/565) +- Unexpected '\"' in middle of array [\#564](https://github.com/nlohmann/json/issues/564) +- Support parse std::pair to Json object [\#563](https://github.com/nlohmann/json/issues/563) +- json and Microsoft Visual c++ Compiler Nov 2012 CTP [\#562](https://github.com/nlohmann/json/issues/562) +- from\_json declaration order and exceptions [\#561](https://github.com/nlohmann/json/issues/561) +- Tip: Don't upgrade to VS2017 if using json initializer list constructs [\#559](https://github.com/nlohmann/json/issues/559) +- parse error - unexpected end of input [\#558](https://github.com/nlohmann/json/issues/558) +- Cant modify existing numbers inside a json object [\#557](https://github.com/nlohmann/json/issues/557) +- Minimal repository \(current size very large\) [\#556](https://github.com/nlohmann/json/issues/556) +- Better support for SAX style serialize and deserialize in new version? [\#554](https://github.com/nlohmann/json/issues/554) +- Cannot convert from json array to std::array [\#553](https://github.com/nlohmann/json/issues/553) +- Do not define an unnamed namespace in a header file \(DCL59-CPP\) [\#552](https://github.com/nlohmann/json/issues/552) +- Parse error on known good json file [\#551](https://github.com/nlohmann/json/issues/551) +- Warning on Intel compiler \(icc 17\) [\#550](https://github.com/nlohmann/json/issues/550) +- multiple versions of 'vsnprintf' [\#549](https://github.com/nlohmann/json/issues/549) +- illegal indirection [\#548](https://github.com/nlohmann/json/issues/548) +- Ambiguous compare operators with clang-5.0 [\#547](https://github.com/nlohmann/json/issues/547) +- Using tsl::ordered\_map [\#546](https://github.com/nlohmann/json/issues/546) +- Compiler support errors are inconvenient [\#544](https://github.com/nlohmann/json/issues/544) +- Head Elements Sorting [\#543](https://github.com/nlohmann/json/issues/543) +- Duplicate symbols error happens while to\_json/from\_json method implemented inside entity definition header file [\#542](https://github.com/nlohmann/json/issues/542) +- consider adding a bool json::is\_valid\(std::string const&\) non-member function [\#541](https://github.com/nlohmann/json/issues/541) +- Help request [\#539](https://github.com/nlohmann/json/issues/539) +- How to deal with missing keys in `from\_json`? [\#538](https://github.com/nlohmann/json/issues/538) +- recursive from\_msgpack implementation will stack overflow [\#537](https://github.com/nlohmann/json/issues/537) +- Exception objects must be nothrow copy constructible \(ERR60-CPP\) [\#531](https://github.com/nlohmann/json/issues/531) +- Support for multiple root elements [\#529](https://github.com/nlohmann/json/issues/529) +- Port has\_shape from dropbox/json11 [\#528](https://github.com/nlohmann/json/issues/528) +- dump\_float: truncation from ptrdiff\_t to long [\#527](https://github.com/nlohmann/json/issues/527) +- Make exception base class visible in basic\_json [\#525](https://github.com/nlohmann/json/issues/525) +- msgpack unit test failures on ppc64 arch [\#524](https://github.com/nlohmann/json/issues/524) +- How about split the implementation out, and only leave the interface? [\#523](https://github.com/nlohmann/json/issues/523) +- VC++2017 not enough actual parameters for macro 'max' [\#522](https://github.com/nlohmann/json/issues/522) +- crash on empty ifstream [\#521](https://github.com/nlohmann/json/issues/521) +- Suggestion: Support tabs for indentation when serializing to stream. [\#520](https://github.com/nlohmann/json/issues/520) +- Abrt in get\_number \(OSS-Fuzz 885\) [\#519](https://github.com/nlohmann/json/issues/519) +- Abrt on unknown address \(OSS-Fuzz 884\) [\#518](https://github.com/nlohmann/json/issues/518) +- Stack-overflow \(OSS-Fuzz 869\) [\#517](https://github.com/nlohmann/json/issues/517) +- Assertion error \(OSS-Fuzz 868\) [\#516](https://github.com/nlohmann/json/issues/516) +- NaN to json and back [\#515](https://github.com/nlohmann/json/issues/515) +- Comparison of NaN [\#514](https://github.com/nlohmann/json/issues/514) +- why it's not possible to serialize c++11 enums directly [\#513](https://github.com/nlohmann/json/issues/513) +- clang compile error: use of overloaded operator '\<=' is ambiguous with \(nlohmann::json{{"a", 5}}\)\["a"\] \<= 10 [\#512](https://github.com/nlohmann/json/issues/512) +- Why not also look inside the type for \(static\) to\_json and from\_json funtions? [\#511](https://github.com/nlohmann/json/issues/511) +- Parser issues [\#509](https://github.com/nlohmann/json/issues/509) +- I may not understand [\#507](https://github.com/nlohmann/json/issues/507) +- VS2017 min / max problem for 2.1.1 [\#506](https://github.com/nlohmann/json/issues/506) +- CBOR/MessagePack is not read until the end [\#505](https://github.com/nlohmann/json/issues/505) +- Assertion error \(OSS-Fuzz 856\) [\#504](https://github.com/nlohmann/json/issues/504) +- Return position in parse error exceptions [\#503](https://github.com/nlohmann/json/issues/503) +- conversion from/to C array is not supported [\#502](https://github.com/nlohmann/json/issues/502) +- error C2338: could not find to\_json\(\) method in T's namespace [\#501](https://github.com/nlohmann/json/issues/501) +- Test suite fails in en\_GB.UTF-8 [\#500](https://github.com/nlohmann/json/issues/500) +- cannot use operator\[\] with number [\#499](https://github.com/nlohmann/json/issues/499) +- consider using \_\_cpp\_exceptions and/or \_\_EXCEPTIONS to disable/enable exception support [\#498](https://github.com/nlohmann/json/issues/498) +- Stack-overflow \(OSS-Fuzz issue 814\) [\#497](https://github.com/nlohmann/json/issues/497) +- Using in Unreal Engine - handling custom types conversion [\#495](https://github.com/nlohmann/json/issues/495) +- Conversion from vector\ to json fails to build [\#494](https://github.com/nlohmann/json/issues/494) +- fill\_line\_buffer incorrectly tests m\_stream for eof but not fail or bad bits [\#493](https://github.com/nlohmann/json/issues/493) +- Compiling with \_GLIBCXX\_DEBUG yields iterator-comparison warnings during tests [\#492](https://github.com/nlohmann/json/issues/492) +- crapy interface [\#491](https://github.com/nlohmann/json/issues/491) +- Fix Visual Studo 2013 builds. [\#490](https://github.com/nlohmann/json/issues/490) +- Failed to compile with -D\_GLIBCXX\_PARALLEL [\#489](https://github.com/nlohmann/json/issues/489) +- Input several field with the same name [\#488](https://github.com/nlohmann/json/issues/488) +- read in .json file yields strange sizes [\#487](https://github.com/nlohmann/json/issues/487) +- json::value\_t can't be a map's key type in VC++ 2015 [\#486](https://github.com/nlohmann/json/issues/486) +- Using fifo\_map [\#485](https://github.com/nlohmann/json/issues/485) +- Cannot get float pointer for value stored as `0` [\#484](https://github.com/nlohmann/json/issues/484) +- byte string support [\#483](https://github.com/nlohmann/json/issues/483) +- For a header-only library you have to clone 214MB [\#482](https://github.com/nlohmann/json/issues/482) +- https://github.com/nlohmann/json\#execute-unit-tests [\#481](https://github.com/nlohmann/json/issues/481) +- Remove deprecated constructor basic\_json\(std::istream&\) [\#480](https://github.com/nlohmann/json/issues/480) +- writing the binary json file? [\#479](https://github.com/nlohmann/json/issues/479) +- CBOR/MessagePack from uint8\_t \* and size [\#478](https://github.com/nlohmann/json/issues/478) +- Streaming binary representations [\#477](https://github.com/nlohmann/json/issues/477) +- Reuse memory in to\_cbor and to\_msgpack functions [\#476](https://github.com/nlohmann/json/issues/476) +- Error Using JSON Library with arrays C++ [\#475](https://github.com/nlohmann/json/issues/475) +- Moving forward to version 3.0.0 [\#474](https://github.com/nlohmann/json/issues/474) +- Inconsistent behavior in conversion to array type [\#473](https://github.com/nlohmann/json/issues/473) +- Create a \[key:member\_pointer\] map to ease parsing custom types [\#471](https://github.com/nlohmann/json/issues/471) +- MSVC 2015 update 2 [\#469](https://github.com/nlohmann/json/issues/469) +- VS2017 implicit to std::string conversion fix. [\#464](https://github.com/nlohmann/json/issues/464) +- How to make sure a string or string literal is a valid JSON? [\#458](https://github.com/nlohmann/json/issues/458) +- basic\_json templated on a "policy" class [\#456](https://github.com/nlohmann/json/issues/456) +- json::value\(const json\_pointer&, ValueType\) requires exceptions to return the default value. [\#440](https://github.com/nlohmann/json/issues/440) +- is it possible merge two json object [\#428](https://github.com/nlohmann/json/issues/428) +- Is it possible to turn this into a shared library? [\#420](https://github.com/nlohmann/json/issues/420) +- Further thoughts on performance improvements [\#418](https://github.com/nlohmann/json/issues/418) +- nan number stored as null [\#388](https://github.com/nlohmann/json/issues/388) +- Behavior of operator\>\> should more closely resemble that of built-in overloads. [\#367](https://github.com/nlohmann/json/issues/367) +- Request: range-based-for over a json-object to expose .first/.second [\#350](https://github.com/nlohmann/json/issues/350) +- feature wish: JSONPath [\#343](https://github.com/nlohmann/json/issues/343) +- UTF-8/Unicode escape and dump [\#330](https://github.com/nlohmann/json/issues/330) +- Serialized value not always can be parsed. [\#329](https://github.com/nlohmann/json/issues/329) +- Is there a way to forward declare nlohmann::json? [\#314](https://github.com/nlohmann/json/issues/314) +- Exception line [\#301](https://github.com/nlohmann/json/issues/301) +- Do not throw exception when default\_value's type does not match the actual type [\#278](https://github.com/nlohmann/json/issues/278) +- dump\(\) method doesn't work with a custom allocator [\#268](https://github.com/nlohmann/json/issues/268) +- Readme documentation enhancements [\#248](https://github.com/nlohmann/json/issues/248) +- Use user-defined exceptions [\#244](https://github.com/nlohmann/json/issues/244) +- Incorrect C++11 allocator model support [\#161](https://github.com/nlohmann/json/issues/161) + +- :white\_check\_mark: re-added tests for algorithms [\#879](https://github.com/nlohmann/json/pull/879) ([nlohmann](https://github.com/nlohmann)) +- Overworked library toward 3.0.0 release [\#875](https://github.com/nlohmann/json/pull/875) ([nlohmann](https://github.com/nlohmann)) +- :rotating\_light: remove C4996 warnings \#872 [\#873](https://github.com/nlohmann/json/pull/873) ([nlohmann](https://github.com/nlohmann)) +- :boom: throwing an exception in case dump encounters a non-UTF-8 string \#838 [\#870](https://github.com/nlohmann/json/pull/870) ([nlohmann](https://github.com/nlohmann)) +- :memo: fixing documentation \#867 [\#868](https://github.com/nlohmann/json/pull/868) ([nlohmann](https://github.com/nlohmann)) +- iter\_impl template conformance with C++17 [\#860](https://github.com/nlohmann/json/pull/860) ([bogemic](https://github.com/bogemic)) +- Std allocator conformance cpp17 [\#856](https://github.com/nlohmann/json/pull/856) ([bogemic](https://github.com/bogemic)) +- cmake: use BUILD\_INTERFACE/INSTALL\_INTERFACE [\#855](https://github.com/nlohmann/json/pull/855) ([theodelrieu](https://github.com/theodelrieu)) +- to/from\_json: add a MSVC-specific static\_assert to force a stacktrace [\#854](https://github.com/nlohmann/json/pull/854) ([theodelrieu](https://github.com/theodelrieu)) +- Add .natvis for MSVC debug view [\#844](https://github.com/nlohmann/json/pull/844) ([TinyTinni](https://github.com/TinyTinni)) +- Updated hunter package links [\#829](https://github.com/nlohmann/json/pull/829) ([jowr](https://github.com/jowr)) +- Typos README [\#811](https://github.com/nlohmann/json/pull/811) ([Itja](https://github.com/Itja)) +- add forwarding references to json\_ref constructor [\#807](https://github.com/nlohmann/json/pull/807) ([theodelrieu](https://github.com/theodelrieu)) +- Add transparent comparator and perfect forwarding support to find\(\) and count\(\) [\#795](https://github.com/nlohmann/json/pull/795) ([jseward](https://github.com/jseward)) +- Error : 'identifier "size\_t" is undefined' in linux [\#793](https://github.com/nlohmann/json/pull/793) ([sonulohani](https://github.com/sonulohani)) +- Fix Visual Studio 2017 warnings [\#788](https://github.com/nlohmann/json/pull/788) ([jseward](https://github.com/jseward)) +- Fix warning C4706 on Visual Studio 2017 [\#785](https://github.com/nlohmann/json/pull/785) ([jseward](https://github.com/jseward)) +- Set GENERATE\_TAGFILE in Doxyfile [\#783](https://github.com/nlohmann/json/pull/783) ([eld00d](https://github.com/eld00d)) +- using more CMake [\#765](https://github.com/nlohmann/json/pull/765) ([nlohmann](https://github.com/nlohmann)) +- Simplified istream handing \#367 [\#764](https://github.com/nlohmann/json/pull/764) ([pjkundert](https://github.com/pjkundert)) +- Add info for the vcpkg package. [\#753](https://github.com/nlohmann/json/pull/753) ([gregmarr](https://github.com/gregmarr)) +- fix from\_json implementation for pair/tuple [\#708](https://github.com/nlohmann/json/pull/708) ([theodelrieu](https://github.com/theodelrieu)) +- Update json.hpp [\#686](https://github.com/nlohmann/json/pull/686) ([GoWebProd](https://github.com/GoWebProd)) +- Remove duplicate word [\#685](https://github.com/nlohmann/json/pull/685) ([daixtrose](https://github.com/daixtrose)) +- To fix compilation issue for intel OSX compiler [\#682](https://github.com/nlohmann/json/pull/682) ([kbthomp1](https://github.com/kbthomp1)) +- Digraph warning [\#679](https://github.com/nlohmann/json/pull/679) ([traits](https://github.com/traits)) +- massage -\> message [\#678](https://github.com/nlohmann/json/pull/678) ([DmitryKuk](https://github.com/DmitryKuk)) +- Fix "not constraint" grammar in docs [\#674](https://github.com/nlohmann/json/pull/674) ([wincent](https://github.com/wincent)) +- Add documentation for integration with CMake and hunter [\#671](https://github.com/nlohmann/json/pull/671) ([dan-42](https://github.com/dan-42)) +- REFACTOR: rewrite CMakeLists.txt for better inlcude and reuse [\#669](https://github.com/nlohmann/json/pull/669) ([dan-42](https://github.com/dan-42)) +- enable\_testing only if the JSON\_BuildTests is ON [\#666](https://github.com/nlohmann/json/pull/666) ([effolkronium](https://github.com/effolkronium)) +- Support moving from rvalues in std::initializer\_list [\#663](https://github.com/nlohmann/json/pull/663) ([himikof](https://github.com/himikof)) +- add ensure\_ascii parameter to dump. \#330 [\#654](https://github.com/nlohmann/json/pull/654) ([ryanjmulder](https://github.com/ryanjmulder)) +- Rename BuildTests to JSON\_BuildTests [\#652](https://github.com/nlohmann/json/pull/652) ([olegendo](https://github.com/olegendo)) +- Don't include \, use std::make\_shared [\#650](https://github.com/nlohmann/json/pull/650) ([olegendo](https://github.com/olegendo)) +- Refacto/split basic json [\#643](https://github.com/nlohmann/json/pull/643) ([theodelrieu](https://github.com/theodelrieu)) +- fix typo in operator\_\_notequal example [\#630](https://github.com/nlohmann/json/pull/630) ([Chocobo1](https://github.com/Chocobo1)) +- Fix MSVC warning C4819 [\#629](https://github.com/nlohmann/json/pull/629) ([Chocobo1](https://github.com/Chocobo1)) +- \[BugFix\] Add parentheses around std::min [\#626](https://github.com/nlohmann/json/pull/626) ([koemeet](https://github.com/koemeet)) +- add pair/tuple conversions [\#624](https://github.com/nlohmann/json/pull/624) ([theodelrieu](https://github.com/theodelrieu)) +- remove std::pair support [\#615](https://github.com/nlohmann/json/pull/615) ([theodelrieu](https://github.com/theodelrieu)) +- Add pair support, fix CompatibleObject conversions \(fixes \#600\) [\#609](https://github.com/nlohmann/json/pull/609) ([theodelrieu](https://github.com/theodelrieu)) +- \#550 Fix iterator related compiling issues for Intel icc [\#598](https://github.com/nlohmann/json/pull/598) ([HenryRLee](https://github.com/HenryRLee)) +- Issue \#593 Fix the arithmetic operators in the iterator and reverse iterator [\#595](https://github.com/nlohmann/json/pull/595) ([HenryRLee](https://github.com/HenryRLee)) +- fix doxygen error of basic\_json::get\(\) [\#583](https://github.com/nlohmann/json/pull/583) ([zhaohuaxishi](https://github.com/zhaohuaxishi)) +- Fixing assignement for iterator wrapper second, and adding unit test [\#579](https://github.com/nlohmann/json/pull/579) ([Type1J](https://github.com/Type1J)) +- Adding first and second properties to iteration\_proxy\_internal [\#578](https://github.com/nlohmann/json/pull/578) ([Type1J](https://github.com/Type1J)) +- Adding support for Meson. [\#576](https://github.com/nlohmann/json/pull/576) ([Type1J](https://github.com/Type1J)) +- add enum class default conversions [\#545](https://github.com/nlohmann/json/pull/545) ([theodelrieu](https://github.com/theodelrieu)) +- Properly pop diagnostics [\#540](https://github.com/nlohmann/json/pull/540) ([tinloaf](https://github.com/tinloaf)) +- Add Visual Studio 17 image to appveyor build matrix [\#536](https://github.com/nlohmann/json/pull/536) ([vpetrigo](https://github.com/vpetrigo)) +- UTF8 encoding enhancement [\#534](https://github.com/nlohmann/json/pull/534) ([TedLyngmo](https://github.com/TedLyngmo)) +- Fix typo [\#530](https://github.com/nlohmann/json/pull/530) ([berkus](https://github.com/berkus)) +- Make exception base class visible in basic\_json [\#526](https://github.com/nlohmann/json/pull/526) ([krzysztofwos](https://github.com/krzysztofwos)) +- :art: Namespace `uint8\_t` from the C++ stdlib [\#510](https://github.com/nlohmann/json/pull/510) ([alex-weej](https://github.com/alex-weej)) +- add to\_json method for C arrays [\#508](https://github.com/nlohmann/json/pull/508) ([theodelrieu](https://github.com/theodelrieu)) +- Fix -Weffc++ warnings \(GNU 6.3.1\) [\#496](https://github.com/nlohmann/json/pull/496) ([TedLyngmo](https://github.com/TedLyngmo)) + +## [v2.1.1](https://github.com/nlohmann/json/releases/tag/v2.1.1) (2017-02-25) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.1.0...v2.1.1) + +- warning in the library [\#472](https://github.com/nlohmann/json/issues/472) +- How to create an array of Objects? [\#470](https://github.com/nlohmann/json/issues/470) +- \[Bug?\] Cannot get int pointer, but int64\_t works [\#468](https://github.com/nlohmann/json/issues/468) +- Illegal indirection [\#467](https://github.com/nlohmann/json/issues/467) +- in vs can't find linkageId [\#466](https://github.com/nlohmann/json/issues/466) +- Roundtrip error while parsing "1000000000000000010E5" [\#465](https://github.com/nlohmann/json/issues/465) +- C4996 error and warning with Visual Studio [\#463](https://github.com/nlohmann/json/issues/463) +- Support startIndex for from\_cbor/from\_msgpack [\#462](https://github.com/nlohmann/json/issues/462) +- question: monospace font used in feature slideshow? [\#460](https://github.com/nlohmann/json/issues/460) +- Object.keys\(\) [\#459](https://github.com/nlohmann/json/issues/459) +- Use “, “ as delimiter for json-objects. [\#457](https://github.com/nlohmann/json/issues/457) +- Enum -\> string during serialization and vice versa [\#455](https://github.com/nlohmann/json/issues/455) +- doubles are printed as integers [\#454](https://github.com/nlohmann/json/issues/454) +- Warnings with Visual Studio c++ \(VS2015 Update 3\) [\#453](https://github.com/nlohmann/json/issues/453) +- Heap-buffer-overflow \(OSS-Fuzz issue 585\) [\#452](https://github.com/nlohmann/json/issues/452) +- use of undeclared identifier 'UINT8\_MAX' [\#451](https://github.com/nlohmann/json/issues/451) +- Question on the lifetime managment of objects at the lower levels [\#449](https://github.com/nlohmann/json/issues/449) +- Json should not be constructible with 'json\*' [\#448](https://github.com/nlohmann/json/issues/448) +- Move value\_t to namespace scope [\#447](https://github.com/nlohmann/json/issues/447) +- Typo in README.md [\#446](https://github.com/nlohmann/json/issues/446) +- make check compilation is unneccesarily slow [\#445](https://github.com/nlohmann/json/issues/445) +- Problem in dump\(\) in json.h caused by ss.imbue [\#444](https://github.com/nlohmann/json/issues/444) +- I want to create Windows Application in Visual Studio 2015 c++, and i have a problem [\#443](https://github.com/nlohmann/json/issues/443) +- Implicit conversion issues [\#442](https://github.com/nlohmann/json/issues/442) +- Parsing of floats locale dependent [\#302](https://github.com/nlohmann/json/issues/302) + +- Speedup CI builds using cotire [\#461](https://github.com/nlohmann/json/pull/461) ([tusharpm](https://github.com/tusharpm)) +- TurpentineDistillery feature/locale independent str to num [\#450](https://github.com/nlohmann/json/pull/450) ([nlohmann](https://github.com/nlohmann)) +- README: adjust boost::optional example [\#439](https://github.com/nlohmann/json/pull/439) ([jaredgrubb](https://github.com/jaredgrubb)) +- fix \#414 - comparing to 0 literal [\#415](https://github.com/nlohmann/json/pull/415) ([stanmihai4](https://github.com/stanmihai4)) + +## [v2.1.0](https://github.com/nlohmann/json/releases/tag/v2.1.0) (2017-01-28) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.10...v2.1.0) + +- Parsing multiple JSON objects from a string or stream [\#438](https://github.com/nlohmann/json/issues/438) +- Use-of-uninitialized-value \(OSS-Fuzz issue 477\) [\#437](https://github.com/nlohmann/json/issues/437) +- add `reserve` function for array to reserve memory before adding json values into it [\#436](https://github.com/nlohmann/json/issues/436) +- Typo in examples page [\#434](https://github.com/nlohmann/json/issues/434) +- avoid malformed json [\#433](https://github.com/nlohmann/json/issues/433) +- How to add json objects to a map? [\#432](https://github.com/nlohmann/json/issues/432) +- create json instance from raw json \(unsigned char\*\) [\#431](https://github.com/nlohmann/json/issues/431) +- Getting std::invalid\_argument: stream error when following example [\#429](https://github.com/nlohmann/json/issues/429) +- Forward declare-only header? [\#427](https://github.com/nlohmann/json/issues/427) +- Implicit conversion from array to object [\#425](https://github.com/nlohmann/json/issues/425) +- Automatic ordered JSON [\#424](https://github.com/nlohmann/json/issues/424) +- error C4996: 'strerror' when reading file [\#422](https://github.com/nlohmann/json/issues/422) +- Get an error - JSON pointer must be empty or begin with '/' [\#421](https://github.com/nlohmann/json/issues/421) +- size parameter for parse\(\) [\#419](https://github.com/nlohmann/json/issues/419) +- json.hpp forcibly defines GCC\_VERSION [\#417](https://github.com/nlohmann/json/issues/417) +- Use-of-uninitialized-value \(OSS-Fuzz issue 377\) [\#416](https://github.com/nlohmann/json/issues/416) +- comparing to 0 literal [\#414](https://github.com/nlohmann/json/issues/414) +- Single char converted to ASCII code instead of string [\#413](https://github.com/nlohmann/json/issues/413) +- How to know if a string was parsed as utf-8? [\#406](https://github.com/nlohmann/json/issues/406) +- Overloaded += to add objects to an array makes no sense? [\#404](https://github.com/nlohmann/json/issues/404) +- Finding a value in an array [\#399](https://github.com/nlohmann/json/issues/399) +- add release information in static function [\#397](https://github.com/nlohmann/json/issues/397) +- Optimize memory usage of json objects in combination with binary serialization [\#373](https://github.com/nlohmann/json/issues/373) +- Conversion operators not considered [\#369](https://github.com/nlohmann/json/issues/369) +- Append ".0" to serialized floating\_point values that are digits-only. [\#362](https://github.com/nlohmann/json/issues/362) +- Add a customization point for user-defined types [\#328](https://github.com/nlohmann/json/issues/328) +- Conformance report for reference [\#307](https://github.com/nlohmann/json/issues/307) +- Document the best way to serialize/deserialize user defined types to json [\#298](https://github.com/nlohmann/json/issues/298) +- Add StringView template typename to basic\_json [\#297](https://github.com/nlohmann/json/issues/297) +- \[Improvement\] Add option to remove exceptions [\#296](https://github.com/nlohmann/json/issues/296) +- Performance in miloyip/nativejson-benchmark [\#202](https://github.com/nlohmann/json/issues/202) + +- conversion from/to user-defined types [\#435](https://github.com/nlohmann/json/pull/435) ([nlohmann](https://github.com/nlohmann)) +- Fix documentation error [\#430](https://github.com/nlohmann/json/pull/430) ([vjon](https://github.com/vjon)) +- locale-independent num-to-str [\#378](https://github.com/nlohmann/json/pull/378) ([TurpentineDistillery](https://github.com/TurpentineDistillery)) + +## [v2.0.10](https://github.com/nlohmann/json/releases/tag/v2.0.10) (2017-01-02) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.9...v2.0.10) + +- Heap-buffer-overflow \(OSS-Fuzz issue 367\) [\#412](https://github.com/nlohmann/json/issues/412) +- Heap-buffer-overflow \(OSS-Fuzz issue 366\) [\#411](https://github.com/nlohmann/json/issues/411) +- Use-of-uninitialized-value \(OSS-Fuzz issue 347\) [\#409](https://github.com/nlohmann/json/issues/409) +- Heap-buffer-overflow \(OSS-Fuzz issue 344\) [\#408](https://github.com/nlohmann/json/issues/408) +- Heap-buffer-overflow \(OSS-Fuzz issue 343\) [\#407](https://github.com/nlohmann/json/issues/407) +- Heap-buffer-overflow \(OSS-Fuzz issue 342\) [\#405](https://github.com/nlohmann/json/issues/405) +- strerror throwing error in compiler VS2015 [\#403](https://github.com/nlohmann/json/issues/403) +- json::parse of std::string being underlined by Visual Studio [\#402](https://github.com/nlohmann/json/issues/402) +- Explicitly getting string without .dump\(\) [\#401](https://github.com/nlohmann/json/issues/401) +- Possible to speed up json::parse? [\#398](https://github.com/nlohmann/json/issues/398) +- the alphabetic order in the code influence console\_output. [\#396](https://github.com/nlohmann/json/issues/396) +- Execute tests with clang sanitizers [\#394](https://github.com/nlohmann/json/issues/394) +- Check if library can be used with ETL [\#361](https://github.com/nlohmann/json/issues/361) + +- Feature/clang sanitize [\#410](https://github.com/nlohmann/json/pull/410) ([Daniel599](https://github.com/Daniel599)) +- Add Doozer build badge [\#400](https://github.com/nlohmann/json/pull/400) ([andoma](https://github.com/andoma)) + +## [v2.0.9](https://github.com/nlohmann/json/releases/tag/v2.0.9) (2016-12-16) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.8...v2.0.9) + +- \#pragma GCC diagnostic ignored "-Wdocumentation" [\#393](https://github.com/nlohmann/json/issues/393) +- How to parse this json file and write separate sub object as json files? [\#392](https://github.com/nlohmann/json/issues/392) +- Integer-overflow \(OSS-Fuzz issue 267\) [\#389](https://github.com/nlohmann/json/issues/389) +- Implement indefinite-length types from RFC 7049 [\#387](https://github.com/nlohmann/json/issues/387) +- template parameter "T" is not used in declaring the parameter types of function template [\#386](https://github.com/nlohmann/json/issues/386) +- Serializing json instances containing already serialized string values without escaping [\#385](https://github.com/nlohmann/json/issues/385) +- Add test cases from RFC 7049 [\#384](https://github.com/nlohmann/json/issues/384) +- Add a table of contents to the README file [\#383](https://github.com/nlohmann/json/issues/383) +- Update FAQ section in the guidelines for contributing [\#382](https://github.com/nlohmann/json/issues/382) +- Allow for forward declaring nlohmann::json [\#381](https://github.com/nlohmann/json/issues/381) +- Bug in overflow detection when parsing integers [\#380](https://github.com/nlohmann/json/issues/380) +- A unique name to mention the library? [\#377](https://github.com/nlohmann/json/issues/377) +- Support for comments. [\#376](https://github.com/nlohmann/json/issues/376) +- Non-unique keys in objects. [\#375](https://github.com/nlohmann/json/issues/375) +- Request: binary serialization/deserialization [\#358](https://github.com/nlohmann/json/issues/358) + +- Replace class iterator and const\_iterator by using a single template class to reduce code. [\#395](https://github.com/nlohmann/json/pull/395) ([Bosswestfalen](https://github.com/Bosswestfalen)) +- Clang: quiet a warning [\#391](https://github.com/nlohmann/json/pull/391) ([jaredgrubb](https://github.com/jaredgrubb)) +- Fix issue \#380: Signed integer overflow check [\#390](https://github.com/nlohmann/json/pull/390) ([qwename](https://github.com/qwename)) + +## [v2.0.8](https://github.com/nlohmann/json/releases/tag/v2.0.8) (2016-12-02) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.7...v2.0.8) + +- Reading from file [\#374](https://github.com/nlohmann/json/issues/374) +- Compiler warnings? [\#372](https://github.com/nlohmann/json/issues/372) +- docs: how to release a json object in memory? [\#371](https://github.com/nlohmann/json/issues/371) +- crash in dump [\#370](https://github.com/nlohmann/json/issues/370) +- Coverity issue \(FORWARD\_NULL\) in lexer\(std::istream& s\) [\#368](https://github.com/nlohmann/json/issues/368) +- json::parse on failed stream gets stuck [\#366](https://github.com/nlohmann/json/issues/366) +- Performance improvements [\#365](https://github.com/nlohmann/json/issues/365) +- 'to\_string' is not a member of 'std' [\#364](https://github.com/nlohmann/json/issues/364) +- Optional comment support. [\#363](https://github.com/nlohmann/json/issues/363) +- Crash in dump\(\) from a static object [\#359](https://github.com/nlohmann/json/issues/359) +- json::parse\(...\) vs json j; j.parse\(...\) [\#357](https://github.com/nlohmann/json/issues/357) +- Hi, is there any method to dump json to string with the insert order rather than alphabets [\#356](https://github.com/nlohmann/json/issues/356) +- Provide an example of reading from an json with only a key that has an array of strings. [\#354](https://github.com/nlohmann/json/issues/354) +- Request: access with default value. [\#353](https://github.com/nlohmann/json/issues/353) +- {} and \[\] causes parser error. [\#352](https://github.com/nlohmann/json/issues/352) +- Reading a JSON file into a JSON object [\#351](https://github.com/nlohmann/json/issues/351) +- Request: 'emplace\_back' [\#349](https://github.com/nlohmann/json/issues/349) +- Is it possible to stream data through the json parser without storing everything in memory? [\#347](https://github.com/nlohmann/json/issues/347) +- pure virtual conversion operator [\#346](https://github.com/nlohmann/json/issues/346) +- Floating point precision lost [\#345](https://github.com/nlohmann/json/issues/345) +- unit-conversions SIGSEGV on armv7hl [\#303](https://github.com/nlohmann/json/issues/303) +- Coverity scan fails [\#299](https://github.com/nlohmann/json/issues/299) +- Using QString as string type [\#274](https://github.com/nlohmann/json/issues/274) + +## [v2.0.7](https://github.com/nlohmann/json/releases/tag/v2.0.7) (2016-11-02) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.6...v2.0.7) + +- JSON5 [\#348](https://github.com/nlohmann/json/issues/348) +- Check "Parsing JSON is a Minefield" [\#344](https://github.com/nlohmann/json/issues/344) +- Allow hex numbers [\#342](https://github.com/nlohmann/json/issues/342) +- Convert strings to numbers [\#341](https://github.com/nlohmann/json/issues/341) +- ""-operators ignore the length parameter [\#340](https://github.com/nlohmann/json/issues/340) +- JSON into std::tuple [\#339](https://github.com/nlohmann/json/issues/339) +- JSON into vector [\#335](https://github.com/nlohmann/json/issues/335) +- Installing with Homebrew on Mac Errors \(El Capitan\) [\#331](https://github.com/nlohmann/json/issues/331) +- g++ make check results in error [\#312](https://github.com/nlohmann/json/issues/312) +- Cannot convert from 'json' to 'char' [\#276](https://github.com/nlohmann/json/issues/276) +- Please add a Pretty-Print option for arrays to stay always in one line [\#229](https://github.com/nlohmann/json/issues/229) +- Conversion to STL map\\> gives error [\#220](https://github.com/nlohmann/json/issues/220) +- std::unorderd\_map cannot be used as ObjectType [\#164](https://github.com/nlohmann/json/issues/164) + +- fix minor grammar/style issue in README.md [\#336](https://github.com/nlohmann/json/pull/336) ([seeekr](https://github.com/seeekr)) + +## [v2.0.6](https://github.com/nlohmann/json/releases/tag/v2.0.6) (2016-10-15) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.5...v2.0.6) + +- How to handle json files? [\#333](https://github.com/nlohmann/json/issues/333) +- This file requires compiler and library support .... [\#332](https://github.com/nlohmann/json/issues/332) +- Segmentation fault on saving json to file [\#326](https://github.com/nlohmann/json/issues/326) +- parse error - unexpected \ with 2.0.5 [\#325](https://github.com/nlohmann/json/issues/325) +- Add nested object capability to pointers [\#323](https://github.com/nlohmann/json/issues/323) +- Fix usage examples' comments for std::multiset [\#322](https://github.com/nlohmann/json/issues/322) +- json\_unit runs forever when executed in build directory [\#319](https://github.com/nlohmann/json/issues/319) +- Visual studio 2015 update3 true != TRUE [\#317](https://github.com/nlohmann/json/issues/317) +- releasing single header file in compressed format [\#316](https://github.com/nlohmann/json/issues/316) +- json object from std::ifstream [\#315](https://github.com/nlohmann/json/issues/315) + +- make has\_mapped\_type struct friendly [\#324](https://github.com/nlohmann/json/pull/324) ([vpetrigo](https://github.com/vpetrigo)) +- Fix usage examples' comments for std::multiset [\#321](https://github.com/nlohmann/json/pull/321) ([vasild](https://github.com/vasild)) +- Include dir relocation [\#318](https://github.com/nlohmann/json/pull/318) ([ChristophJud](https://github.com/ChristophJud)) +- trivial documentation fix [\#313](https://github.com/nlohmann/json/pull/313) ([5tefan](https://github.com/5tefan)) + +## [v2.0.5](https://github.com/nlohmann/json/releases/tag/v2.0.5) (2016-09-14) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.4...v2.0.5) + +- \[feature request\]: schema validator and comments [\#311](https://github.com/nlohmann/json/issues/311) +- make json\_benchmarks no longer working in 2.0.4 [\#310](https://github.com/nlohmann/json/issues/310) +- Segmentation fault \(core dumped\) [\#309](https://github.com/nlohmann/json/issues/309) +- No matching member function for call to 'get\_impl' [\#308](https://github.com/nlohmann/json/issues/308) + +## [v2.0.4](https://github.com/nlohmann/json/releases/tag/v2.0.4) (2016-09-11) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.3...v2.0.4) + +- Parsing fails without space at end of file [\#306](https://github.com/nlohmann/json/issues/306) +- json schema validator [\#305](https://github.com/nlohmann/json/issues/305) +- Unused variable warning [\#304](https://github.com/nlohmann/json/issues/304) + +## [v2.0.3](https://github.com/nlohmann/json/releases/tag/v2.0.3) (2016-08-31) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.2...v2.0.3) + +- warning C4706: assignment within conditional expression [\#295](https://github.com/nlohmann/json/issues/295) +- Strip comments / Minify [\#294](https://github.com/nlohmann/json/issues/294) +- Q: Is it possible to build json tree from already UTF8 encoded values? [\#293](https://github.com/nlohmann/json/issues/293) +- Equality operator results in array when assigned object [\#292](https://github.com/nlohmann/json/issues/292) +- Support for integers not from the range \[-\(2\*\*53\)+1, \(2\*\*53\)-1\] in parser [\#291](https://github.com/nlohmann/json/issues/291) +- Support for iterator-range parsing [\#290](https://github.com/nlohmann/json/issues/290) +- Horribly inconsistent behavior between const/non-const reference in operator \[\] \(\) [\#289](https://github.com/nlohmann/json/issues/289) +- Silently get numbers into smaller types [\#288](https://github.com/nlohmann/json/issues/288) +- Incorrect parsing of large int64\_t numbers [\#287](https://github.com/nlohmann/json/issues/287) +- \[question\]: macro to disable floating point support [\#284](https://github.com/nlohmann/json/issues/284) + +- unit-constructor1.cpp: Fix floating point truncation warning [\#300](https://github.com/nlohmann/json/pull/300) ([t-b](https://github.com/t-b)) + +## [v2.0.2](https://github.com/nlohmann/json/releases/tag/v2.0.2) (2016-07-31) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.1...v2.0.2) + +- can function dump\(\) return string in the order I push in the json object ? [\#286](https://github.com/nlohmann/json/issues/286) +- Error on the Mac: Undefined symbols for architecture x86\_64 [\#285](https://github.com/nlohmann/json/issues/285) +- value\(\) does not work with \_json\_pointer types [\#283](https://github.com/nlohmann/json/issues/283) +- Build error for std::int64 [\#282](https://github.com/nlohmann/json/issues/282) +- strings can't be accessed after dump\(\)-\>parse\(\) - type is lost [\#281](https://github.com/nlohmann/json/issues/281) +- Easy serialization of classes [\#280](https://github.com/nlohmann/json/issues/280) +- recursive data structures [\#277](https://github.com/nlohmann/json/issues/277) +- hexify\(\) function emits conversion warning [\#270](https://github.com/nlohmann/json/issues/270) + +- let the makefile choose the correct sed [\#279](https://github.com/nlohmann/json/pull/279) ([murinicanor](https://github.com/murinicanor)) +- Update hexify to use array lookup instead of ternary \(\#270\) [\#275](https://github.com/nlohmann/json/pull/275) ([dtoma](https://github.com/dtoma)) + +## [v2.0.1](https://github.com/nlohmann/json/releases/tag/v2.0.1) (2016-06-28) +[Full Changelog](https://github.com/nlohmann/json/compare/v2.0.0...v2.0.1) + +- Compilation error. [\#273](https://github.com/nlohmann/json/issues/273) +- dump\(\) performance degradation in v2 [\#272](https://github.com/nlohmann/json/issues/272) + +- fixed a tiny typo [\#271](https://github.com/nlohmann/json/pull/271) ([feroldi](https://github.com/feroldi)) + +## [v2.0.0](https://github.com/nlohmann/json/releases/tag/v2.0.0) (2016-06-23) +[Full Changelog](https://github.com/nlohmann/json/compare/v1.1.0...v2.0.0) + +- json::diff generates incorrect patch when removing multiple array elements. [\#269](https://github.com/nlohmann/json/issues/269) +- Docs - What does Json\[key\] return? [\#267](https://github.com/nlohmann/json/issues/267) +- Compiler Errors With JSON.hpp [\#265](https://github.com/nlohmann/json/issues/265) +- Ambiguous push\_back and operator+= overloads [\#263](https://github.com/nlohmann/json/issues/263) +- Preseving order of items in json [\#262](https://github.com/nlohmann/json/issues/262) +- '\' char problem in strings [\#261](https://github.com/nlohmann/json/issues/261) +- VS2015 compile fail [\#260](https://github.com/nlohmann/json/issues/260) +- -Wconversion warning [\#259](https://github.com/nlohmann/json/issues/259) +- Maybe a bug [\#258](https://github.com/nlohmann/json/issues/258) +- Few tests failed on Visual C++ 2015 [\#257](https://github.com/nlohmann/json/issues/257) +- Access keys when iteration with new for loop C++11 [\#256](https://github.com/nlohmann/json/issues/256) +- multiline text values [\#255](https://github.com/nlohmann/json/issues/255) +- Error when using json in g++ [\#254](https://github.com/nlohmann/json/issues/254) +- is the release 2.0? [\#253](https://github.com/nlohmann/json/issues/253) +- concatenate objects [\#252](https://github.com/nlohmann/json/issues/252) +- Encoding [\#251](https://github.com/nlohmann/json/issues/251) +- Unable to build example for constructing json object with stringstreams [\#250](https://github.com/nlohmann/json/issues/250) +- Hexadecimal support [\#249](https://github.com/nlohmann/json/issues/249) +- Update long-term goals [\#246](https://github.com/nlohmann/json/issues/246) +- Contribution To This Json Project [\#245](https://github.com/nlohmann/json/issues/245) +- Trouble using parser with initial dictionary [\#243](https://github.com/nlohmann/json/issues/243) +- Unit test fails when doing a CMake out-of-tree build [\#241](https://github.com/nlohmann/json/issues/241) +- -Wconversion warnings [\#239](https://github.com/nlohmann/json/issues/239) +- Additional integration options [\#237](https://github.com/nlohmann/json/issues/237) +- .get\\(\) works for non spaced string but returns as array for spaced/longer strings [\#236](https://github.com/nlohmann/json/issues/236) +- ambiguous overload for 'push\_back' and 'operator+=' [\#235](https://github.com/nlohmann/json/issues/235) +- Can't use basic\_json::iterator as a base iterator for std::move\_iterator [\#233](https://github.com/nlohmann/json/issues/233) +- json object's creation can freezes execution [\#231](https://github.com/nlohmann/json/issues/231) +- Incorrect dumping of parsed numbers with exponents, but without decimal places [\#230](https://github.com/nlohmann/json/issues/230) +- double values are serialized with commas as decimal points [\#228](https://github.com/nlohmann/json/issues/228) +- Move semantics with std::initializer\_list [\#225](https://github.com/nlohmann/json/issues/225) +- replace emplace [\#224](https://github.com/nlohmann/json/issues/224) +- abort during getline in yyfill [\#223](https://github.com/nlohmann/json/issues/223) +- free\(\): invalid pointer error in GCC 5.2.1 [\#221](https://github.com/nlohmann/json/issues/221) +- Error compile Android NDK error: 'strtof' is not a member of 'std' [\#219](https://github.com/nlohmann/json/issues/219) +- Wrong link in the README.md [\#217](https://github.com/nlohmann/json/issues/217) +- Wide character strings not supported [\#216](https://github.com/nlohmann/json/issues/216) +- Memory allocations using range-based for loops [\#214](https://github.com/nlohmann/json/issues/214) +- would you like to support gcc 4.8.1? [\#211](https://github.com/nlohmann/json/issues/211) +- Reading concatenated json's from an istream [\#210](https://github.com/nlohmann/json/issues/210) +- Conflicting typedef of ssize\_t on Windows 32 bit when using Boost.Python [\#204](https://github.com/nlohmann/json/issues/204) +- Inconsistency between operator\[\] and push\_back [\#203](https://github.com/nlohmann/json/issues/203) +- Small bugs in json.hpp \(get\_number\) and unit.cpp \(non-standard integer type test\) [\#199](https://github.com/nlohmann/json/issues/199) +- GCC/clang floating point parsing bug in strtod\(\) [\#195](https://github.com/nlohmann/json/issues/195) +- What is within scope? [\#192](https://github.com/nlohmann/json/issues/192) +- Bugs in miloyip/nativejson-benchmark: roundtrips [\#187](https://github.com/nlohmann/json/issues/187) +- Floating point exceptions [\#181](https://github.com/nlohmann/json/issues/181) +- Integer conversion to unsigned [\#178](https://github.com/nlohmann/json/issues/178) +- map string string fails to compile [\#176](https://github.com/nlohmann/json/issues/176) +- In basic\_json::basic\_json\(const CompatibleArrayType& val\), the requirement of CompatibleArrayType is not strict enough. [\#174](https://github.com/nlohmann/json/issues/174) +- Provide a FAQ [\#163](https://github.com/nlohmann/json/issues/163) +- Implicit assignment to std::string fails [\#144](https://github.com/nlohmann/json/issues/144) + +- Fix Issue \#265 [\#266](https://github.com/nlohmann/json/pull/266) ([06needhamt](https://github.com/06needhamt)) +- Define CMake/CTest tests [\#247](https://github.com/nlohmann/json/pull/247) ([robertmrk](https://github.com/robertmrk)) +- Out of tree builds and a few other miscellaneous CMake cleanups. [\#242](https://github.com/nlohmann/json/pull/242) ([ChrisKitching](https://github.com/ChrisKitching)) +- Implement additional integration options [\#238](https://github.com/nlohmann/json/pull/238) ([robertmrk](https://github.com/robertmrk)) +- make serialization locale-independent [\#232](https://github.com/nlohmann/json/pull/232) ([nlohmann](https://github.com/nlohmann)) +- fixes \#223 by updating README.md [\#227](https://github.com/nlohmann/json/pull/227) ([kevin--](https://github.com/kevin--)) +- Use namespace std for int64\_t and uint64\_t [\#226](https://github.com/nlohmann/json/pull/226) ([lv-zheng](https://github.com/lv-zheng)) +- Added missing cerrno header to fix ERANGE compile error on android [\#222](https://github.com/nlohmann/json/pull/222) ([Teemperor](https://github.com/Teemperor)) +- Corrected readme [\#218](https://github.com/nlohmann/json/pull/218) ([Annihil](https://github.com/Annihil)) +- Create PULL\_REQUEST\_TEMPLATE.md [\#213](https://github.com/nlohmann/json/pull/213) ([whackashoe](https://github.com/whackashoe)) +- fixed noexcept; added constexpr [\#208](https://github.com/nlohmann/json/pull/208) ([nlohmann](https://github.com/nlohmann)) +- Add support for afl-fuzz testing [\#207](https://github.com/nlohmann/json/pull/207) ([mykter](https://github.com/mykter)) +- replaced ssize\_t occurrences with auto \(addresses \#204\) [\#205](https://github.com/nlohmann/json/pull/205) ([nlohmann](https://github.com/nlohmann)) +- Fixed issue \#199 - Small bugs in json.hpp \(get\_number\) and unit.cpp \(non-standard integer type test\) [\#200](https://github.com/nlohmann/json/pull/200) ([twelsby](https://github.com/twelsby)) +- Fix broken link [\#197](https://github.com/nlohmann/json/pull/197) ([vog](https://github.com/vog)) +- Issue \#195 - update Travis to Trusty due to gcc/clang strtod\(\) bug [\#196](https://github.com/nlohmann/json/pull/196) ([twelsby](https://github.com/twelsby)) +- Issue \#178 - Extending support to full uint64\_t/int64\_t range and unsigned type \(updated\) [\#193](https://github.com/nlohmann/json/pull/193) ([twelsby](https://github.com/twelsby)) + +## [v1.1.0](https://github.com/nlohmann/json/releases/tag/v1.1.0) (2016-01-24) +[Full Changelog](https://github.com/nlohmann/json/compare/v1.0.0...v1.1.0) + +- Small error in pull \#185 [\#194](https://github.com/nlohmann/json/issues/194) +- Bugs in miloyip/nativejson-benchmark: floating-point parsing [\#186](https://github.com/nlohmann/json/issues/186) +- Floating point equality [\#185](https://github.com/nlohmann/json/issues/185) +- Unused variables in catch [\#180](https://github.com/nlohmann/json/issues/180) +- Typo in documentation [\#179](https://github.com/nlohmann/json/issues/179) +- JSON performance benchmark comparision [\#177](https://github.com/nlohmann/json/issues/177) +- Since re2c is often ignored in pull requests, it may make sense to make a contributing.md file [\#175](https://github.com/nlohmann/json/issues/175) +- Question about exceptions [\#173](https://github.com/nlohmann/json/issues/173) +- Android? [\#172](https://github.com/nlohmann/json/issues/172) +- Cannot index by key of type static constexpr const char\* [\#171](https://github.com/nlohmann/json/issues/171) +- Add assertions [\#168](https://github.com/nlohmann/json/issues/168) +- MSVC 2015 build fails when attempting to compare object\_t [\#167](https://github.com/nlohmann/json/issues/167) +- Member detector is not portable [\#166](https://github.com/nlohmann/json/issues/166) +- Unnecessary const\_cast [\#162](https://github.com/nlohmann/json/issues/162) +- Question about get\_ref\(\) [\#128](https://github.com/nlohmann/json/issues/128) +- range based for loop for objects [\#83](https://github.com/nlohmann/json/issues/83) +- Consider submitting this to the Boost Library Incubator [\#66](https://github.com/nlohmann/json/issues/66) + +- Fixed Issue \#186 - add strto\(f|d|ld\) overload wrappers, "-0.0" special case and FP trailing zero [\#191](https://github.com/nlohmann/json/pull/191) ([twelsby](https://github.com/twelsby)) +- Issue \#185 - remove approx\(\) and use \#pragma to kill warnings [\#190](https://github.com/nlohmann/json/pull/190) ([twelsby](https://github.com/twelsby)) +- Fixed Issue \#171 - added two extra template overloads of operator\[\] for T\* arguments [\#189](https://github.com/nlohmann/json/pull/189) ([twelsby](https://github.com/twelsby)) +- Fixed issue \#167 - removed operator ValueType\(\) condition for VS2015 [\#188](https://github.com/nlohmann/json/pull/188) ([twelsby](https://github.com/twelsby)) +- Implementation of get\_ref\(\) [\#184](https://github.com/nlohmann/json/pull/184) ([dariomt](https://github.com/dariomt)) +- Fixed some typos in CONTRIBUTING.md [\#182](https://github.com/nlohmann/json/pull/182) ([nibroc](https://github.com/nibroc)) + +## [v1.0.0](https://github.com/nlohmann/json/releases/tag/v1.0.0) (2015-12-27) +[Full Changelog](https://github.com/nlohmann/json/compare/v1.0.0-rc1...v1.0.0) + +- add key name to exception [\#160](https://github.com/nlohmann/json/issues/160) +- Getting member discarding qualifyer [\#159](https://github.com/nlohmann/json/issues/159) +- basic\_json::iterator::value\(\) output includes quotes while basic\_json::iterator::key\(\) doesn't [\#158](https://github.com/nlohmann/json/issues/158) +- Indexing `const basic\_json\<\>` with `const basic\_string\` [\#157](https://github.com/nlohmann/json/issues/157) +- token\_type\_name\(token\_type t\): not all control paths return a value [\#156](https://github.com/nlohmann/json/issues/156) +- prevent json.hpp from emitting compiler warnings [\#154](https://github.com/nlohmann/json/issues/154) +- json::parse\(string\) does not check utf8 bom [\#152](https://github.com/nlohmann/json/issues/152) +- unsigned 64bit values output as signed [\#151](https://github.com/nlohmann/json/issues/151) +- Wish feature: json5 [\#150](https://github.com/nlohmann/json/issues/150) +- Unable to compile on MSVC 2015 with SDL checking enabled: This function or variable may be unsafe. [\#149](https://github.com/nlohmann/json/issues/149) +- "Json Object" type does not keep object order [\#148](https://github.com/nlohmann/json/issues/148) +- dump\(\) convert strings encoded by utf-8 to shift-jis on windows 10. [\#147](https://github.com/nlohmann/json/issues/147) +- Unable to get field names in a json object [\#145](https://github.com/nlohmann/json/issues/145) +- Question: Is the use of incomplete type correct? [\#138](https://github.com/nlohmann/json/issues/138) +- json.hpp:5746:32: error: 'to\_string' is not a member of 'std' [\#136](https://github.com/nlohmann/json/issues/136) +- Bug in basic\_json::operator\[\] const overload [\#135](https://github.com/nlohmann/json/issues/135) +- wrong enable\_if for const pointer \(instead of pointer-to-const\) [\#134](https://github.com/nlohmann/json/issues/134) +- overload of at\(\) with default value [\#133](https://github.com/nlohmann/json/issues/133) +- Splitting source [\#132](https://github.com/nlohmann/json/issues/132) +- Question about get\_ptr\(\) [\#127](https://github.com/nlohmann/json/issues/127) +- Visual Studio 14 Debug assertion failed [\#125](https://github.com/nlohmann/json/issues/125) +- Memory leak in face of exceptions [\#118](https://github.com/nlohmann/json/issues/118) +- Find and Count for arrays [\#117](https://github.com/nlohmann/json/issues/117) +- dynamically constructing an arbitrarily nested object [\#114](https://github.com/nlohmann/json/issues/114) +- Returning any data type [\#113](https://github.com/nlohmann/json/issues/113) +- Compile error with g++ 4.9.3 cygwin 64-bit [\#112](https://github.com/nlohmann/json/issues/112) +- insert json array issue with gcc4.8.2 [\#110](https://github.com/nlohmann/json/issues/110) +- error: unterminated raw string [\#109](https://github.com/nlohmann/json/issues/109) +- vector\ copy constructor really weird [\#108](https://github.com/nlohmann/json/issues/108) +- \[clang-3.6.2\] string/sstream with number to json issue [\#107](https://github.com/nlohmann/json/issues/107) +- maintaining order of keys during iteration [\#106](https://github.com/nlohmann/json/issues/106) +- object field accessors [\#103](https://github.com/nlohmann/json/issues/103) +- v8pp and json [\#95](https://github.com/nlohmann/json/issues/95) +- Wishlist [\#65](https://github.com/nlohmann/json/issues/65) +- Windows/Visual Studio \(through 2013\) is unsupported [\#62](https://github.com/nlohmann/json/issues/62) + +- Replace sprintf with hex function, this fixes \#149 [\#153](https://github.com/nlohmann/json/pull/153) ([whackashoe](https://github.com/whackashoe)) +- Fix character skipping after a surrogate pair [\#146](https://github.com/nlohmann/json/pull/146) ([robertmrk](https://github.com/robertmrk)) +- Detect correctly pointer-to-const [\#137](https://github.com/nlohmann/json/pull/137) ([dariomt](https://github.com/dariomt)) +- disabled "CopyAssignable" test for MSVC in Debug mode, see \#125 [\#131](https://github.com/nlohmann/json/pull/131) ([dariomt](https://github.com/dariomt)) +- removed stream operator for iterator, resolution for \#125 [\#130](https://github.com/nlohmann/json/pull/130) ([dariomt](https://github.com/dariomt)) +- fixed typos in comments for examples [\#129](https://github.com/nlohmann/json/pull/129) ([dariomt](https://github.com/dariomt)) +- Remove superfluous inefficiency [\#126](https://github.com/nlohmann/json/pull/126) ([d-frey](https://github.com/d-frey)) +- remove invalid parameter '-stdlib=libc++' in CMakeLists.txt [\#124](https://github.com/nlohmann/json/pull/124) ([emvivre](https://github.com/emvivre)) +- exception-safe object creation, fixes \#118 [\#122](https://github.com/nlohmann/json/pull/122) ([d-frey](https://github.com/d-frey)) +- Fix small oversight. [\#121](https://github.com/nlohmann/json/pull/121) ([ColinH](https://github.com/ColinH)) +- Overload parse\(\) to accept an rvalue reference [\#120](https://github.com/nlohmann/json/pull/120) ([silverweed](https://github.com/silverweed)) +- Use the right variable name in doc string [\#115](https://github.com/nlohmann/json/pull/115) ([whoshuu](https://github.com/whoshuu)) + +## [v1.0.0-rc1](https://github.com/nlohmann/json/releases/tag/v1.0.0-rc1) (2015-07-26) +- Finish documenting the public interface in Doxygen [\#102](https://github.com/nlohmann/json/issues/102) +- Binary string causes numbers to be dumped as hex [\#101](https://github.com/nlohmann/json/issues/101) +- failed to iterator json object with reverse\_iterator [\#100](https://github.com/nlohmann/json/issues/100) +- 'noexcept' : unknown override specifier [\#99](https://github.com/nlohmann/json/issues/99) +- json float parsing problem [\#98](https://github.com/nlohmann/json/issues/98) +- Adjust wording to JSON RFC [\#97](https://github.com/nlohmann/json/issues/97) +- 17 MB / 90 MB repo size!? [\#96](https://github.com/nlohmann/json/issues/96) +- static analysis warnings [\#94](https://github.com/nlohmann/json/issues/94) +- reverse\_iterator operator inheritance problem [\#93](https://github.com/nlohmann/json/issues/93) +- init error [\#92](https://github.com/nlohmann/json/issues/92) +- access by \(const\) reference [\#91](https://github.com/nlohmann/json/issues/91) +- is\_integer and is\_float tests [\#90](https://github.com/nlohmann/json/issues/90) +- Nonstandard integer type [\#89](https://github.com/nlohmann/json/issues/89) +- static library build [\#84](https://github.com/nlohmann/json/issues/84) +- lexer::get\_number return NAN [\#82](https://github.com/nlohmann/json/issues/82) +- MinGW have no std::to\_string [\#80](https://github.com/nlohmann/json/issues/80) +- Incorrect behaviour of basic\_json::count method [\#78](https://github.com/nlohmann/json/issues/78) +- Invoking is\_array\(\) function creates "null" value [\#77](https://github.com/nlohmann/json/issues/77) +- dump\(\) / parse\(\) not idempotent [\#76](https://github.com/nlohmann/json/issues/76) +- Handle infinity and NaN cases [\#70](https://github.com/nlohmann/json/issues/70) +- errors in g++-4.8.1 [\#68](https://github.com/nlohmann/json/issues/68) +- Keys when iterating over objects [\#67](https://github.com/nlohmann/json/issues/67) +- Compilation results in tons of warnings [\#64](https://github.com/nlohmann/json/issues/64) +- Complete brief documentation [\#61](https://github.com/nlohmann/json/issues/61) +- Double quotation mark is not parsed correctly [\#60](https://github.com/nlohmann/json/issues/60) +- Get coverage back to 100% [\#58](https://github.com/nlohmann/json/issues/58) +- erase elements using iterators [\#57](https://github.com/nlohmann/json/issues/57) +- Removing item from array [\#56](https://github.com/nlohmann/json/issues/56) +- Serialize/Deserialize like PHP? [\#55](https://github.com/nlohmann/json/issues/55) +- Numbers as keys [\#54](https://github.com/nlohmann/json/issues/54) +- Why are elements alphabetized on key while iterating? [\#53](https://github.com/nlohmann/json/issues/53) +- Document erase, count, and iterators key and value [\#52](https://github.com/nlohmann/json/issues/52) +- Do not use std::to\_string [\#51](https://github.com/nlohmann/json/issues/51) +- Supported compilers [\#50](https://github.com/nlohmann/json/issues/50) +- Confused about iterating through json objects [\#49](https://github.com/nlohmann/json/issues/49) +- Use non-member begin/end [\#48](https://github.com/nlohmann/json/issues/48) +- Erase key [\#47](https://github.com/nlohmann/json/issues/47) +- Key iterator [\#46](https://github.com/nlohmann/json/issues/46) +- Add count member function [\#45](https://github.com/nlohmann/json/issues/45) +- Problem getting vector \(array\) of strings [\#44](https://github.com/nlohmann/json/issues/44) +- Compilation error due to assuming that private=public [\#43](https://github.com/nlohmann/json/issues/43) +- Use of deprecated implicit copy constructor [\#42](https://github.com/nlohmann/json/issues/42) +- Printing attribute names [\#39](https://github.com/nlohmann/json/issues/39) +- dumping a small number\_float just outputs 0.000000 [\#37](https://github.com/nlohmann/json/issues/37) +- find is error [\#32](https://github.com/nlohmann/json/issues/32) +- Avoid using spaces when encoding without pretty print [\#31](https://github.com/nlohmann/json/issues/31) +- Cannot encode long numbers [\#30](https://github.com/nlohmann/json/issues/30) +- segmentation fault when iterating over empty arrays/objects [\#28](https://github.com/nlohmann/json/issues/28) +- Creating an empty array [\#27](https://github.com/nlohmann/json/issues/27) +- Custom allocator support [\#25](https://github.com/nlohmann/json/issues/25) +- make the type of the used string container customizable [\#20](https://github.com/nlohmann/json/issues/20) +- Improper parsing of JSON string "\\" [\#17](https://github.com/nlohmann/json/issues/17) +- create a header-only version [\#16](https://github.com/nlohmann/json/issues/16) +- Don't return "const values" [\#15](https://github.com/nlohmann/json/issues/15) +- Add to\_string overload for indentation [\#13](https://github.com/nlohmann/json/issues/13) +- string parser does not recognize uncompliant strings [\#12](https://github.com/nlohmann/json/issues/12) +- possible double-free in find function [\#11](https://github.com/nlohmann/json/issues/11) +- UTF-8 encoding/deconding/testing [\#10](https://github.com/nlohmann/json/issues/10) +- move code into namespace [\#9](https://github.com/nlohmann/json/issues/9) +- free functions for explicit objects and arrays in initializer lists [\#8](https://github.com/nlohmann/json/issues/8) +- unique\_ptr for ownership [\#7](https://github.com/nlohmann/json/issues/7) +- Add unit tests [\#4](https://github.com/nlohmann/json/issues/4) +- Drop C++98 support [\#3](https://github.com/nlohmann/json/issues/3) +- Test case coverage [\#2](https://github.com/nlohmann/json/issues/2) +- Runtime error in Travis job [\#1](https://github.com/nlohmann/json/issues/1) + +- Keyword 'inline' is useless when member functions are defined in headers [\#87](https://github.com/nlohmann/json/pull/87) ([ahamez](https://github.com/ahamez)) +- Remove useless typename [\#86](https://github.com/nlohmann/json/pull/86) ([ahamez](https://github.com/ahamez)) +- Avoid warning with Xcode's clang [\#85](https://github.com/nlohmann/json/pull/85) ([ahamez](https://github.com/ahamez)) +- Fix typos [\#73](https://github.com/nlohmann/json/pull/73) ([aqnouch](https://github.com/aqnouch)) +- Replace `default\_callback` function with `nullptr` and check for null… [\#72](https://github.com/nlohmann/json/pull/72) ([aburgh](https://github.com/aburgh)) +- support enum [\#71](https://github.com/nlohmann/json/pull/71) ([likebeta](https://github.com/likebeta)) +- Fix performance regression introduced with the parsing callback feature. [\#69](https://github.com/nlohmann/json/pull/69) ([aburgh](https://github.com/aburgh)) +- Improve the implementations of the comparission-operators [\#63](https://github.com/nlohmann/json/pull/63) ([Florianjw](https://github.com/Florianjw)) +- Fix compilation of json\_unit with GCC 5 [\#59](https://github.com/nlohmann/json/pull/59) ([dkopecek](https://github.com/dkopecek)) +- Parse streams incrementally. [\#40](https://github.com/nlohmann/json/pull/40) ([aburgh](https://github.com/aburgh)) +- Feature/small float serialization [\#38](https://github.com/nlohmann/json/pull/38) ([jrandall](https://github.com/jrandall)) +- template version with re2c scanner [\#36](https://github.com/nlohmann/json/pull/36) ([nlohmann](https://github.com/nlohmann)) +- more descriptive documentation in example [\#33](https://github.com/nlohmann/json/pull/33) ([luxe](https://github.com/luxe)) +- Fix string conversion under Clang [\#26](https://github.com/nlohmann/json/pull/26) ([wancw](https://github.com/wancw)) +- Fixed dumping of strings [\#24](https://github.com/nlohmann/json/pull/24) ([Teemperor](https://github.com/Teemperor)) +- Added a remark to the readme that coverage is GCC only for now [\#23](https://github.com/nlohmann/json/pull/23) ([Teemperor](https://github.com/Teemperor)) +- Unicode escaping [\#22](https://github.com/nlohmann/json/pull/22) ([Teemperor](https://github.com/Teemperor)) +- Implemented the JSON spec for string parsing for everything but the \uXXXX escaping [\#21](https://github.com/nlohmann/json/pull/21) ([Teemperor](https://github.com/Teemperor)) +- add the std iterator typedefs to iterator and const\_iterator [\#19](https://github.com/nlohmann/json/pull/19) ([kirkshoop](https://github.com/kirkshoop)) +- Fixed escaped quotes [\#18](https://github.com/nlohmann/json/pull/18) ([Teemperor](https://github.com/Teemperor)) +- Fix double delete on std::bad\_alloc exception [\#14](https://github.com/nlohmann/json/pull/14) ([elliotgoodrich](https://github.com/elliotgoodrich)) +- Added CMake and lcov [\#6](https://github.com/nlohmann/json/pull/6) ([Teemperor](https://github.com/Teemperor)) +- Version 2.0 [\#5](https://github.com/nlohmann/json/pull/5) ([nlohmann](https://github.com/nlohmann)) + + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/libraries/cppdap/third_party/json/LICENSE.MIT b/libraries/cppdap/third_party/json/LICENSE.MIT new file mode 100644 index 000000000..db73c5f7d --- /dev/null +++ b/libraries/cppdap/third_party/json/LICENSE.MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2019 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libraries/cppdap/third_party/json/Makefile b/libraries/cppdap/third_party/json/Makefile new file mode 100644 index 000000000..4fb303fcb --- /dev/null +++ b/libraries/cppdap/third_party/json/Makefile @@ -0,0 +1,629 @@ +.PHONY: pretty clean ChangeLog.md release + +########################################################################## +# configuration +########################################################################## + +# directory to recent compiler binaries +COMPILER_DIR=/Users/niels/Documents/projects/compilers/local/bin + +# find GNU sed to use `-i` parameter +SED:=$(shell command -v gsed || which sed) + + +########################################################################## +# source files +########################################################################## + +# the list of sources in the include folder +SRCS=$(shell find include -type f | sort) + +# the single header (amalgamated from the source files) +AMALGAMATED_FILE=single_include/nlohmann/json.hpp + + +########################################################################## +# documentation of the Makefile's targets +########################################################################## + +# main target +all: + @echo "amalgamate - amalgamate file single_include/nlohmann/json.hpp from the include/nlohmann sources" + @echo "ChangeLog.md - generate ChangeLog file" + @echo "check - compile and execute test suite" + @echo "check-amalgamation - check whether sources have been amalgamated" + @echo "check-fast - compile and execute test suite (skip long-running tests)" + @echo "clean - remove built files" + @echo "coverage - create coverage information with lcov" + @echo "coverage-fast - create coverage information with fastcov" + @echo "cppcheck - analyze code with cppcheck" + @echo "cpplint - analyze code with cpplint" + @echo "clang_tidy - analyze code with Clang-Tidy" + @echo "clang_analyze - analyze code with Clang-Analyzer" + @echo "doctest - compile example files and check their output" + @echo "fuzz_testing - prepare fuzz testing of the JSON parser" + @echo "fuzz_testing_bson - prepare fuzz testing of the BSON parser" + @echo "fuzz_testing_cbor - prepare fuzz testing of the CBOR parser" + @echo "fuzz_testing_msgpack - prepare fuzz testing of the MessagePack parser" + @echo "fuzz_testing_ubjson - prepare fuzz testing of the UBJSON parser" + @echo "json_unit - create single-file test executable" + @echo "pedantic_clang - run Clang with maximal warning flags" + @echo "pedantic_gcc - run GCC with maximal warning flags" + @echo "pretty - beautify code with Artistic Style" + @echo "run_benchmarks - build and run benchmarks" + + +########################################################################## +# unit tests +########################################################################## + +# build unit tests +json_unit: + @$(MAKE) json_unit -C test + +# run unit tests +check: + $(MAKE) check -C test + +# run unit tests and skip expensive tests +check-fast: + $(MAKE) check -C test TEST_PATTERN="" + + +########################################################################## +# coverage +########################################################################## + +coverage: + rm -fr build_coverage + mkdir build_coverage + cd build_coverage ; CXX=g++-8 cmake .. -GNinja -DJSON_Coverage=ON -DJSON_MultipleHeaders=ON + cd build_coverage ; ninja + cd build_coverage ; ctest -E '.*_default' -j10 + cd build_coverage ; ninja lcov_html + open build_coverage/test/html/index.html + +coverage-fast: + rm -fr build_coverage + mkdir build_coverage + cd build_coverage ; CXX=g++-9 cmake .. -GNinja -DJSON_Coverage=ON -DJSON_MultipleHeaders=ON + cd build_coverage ; ninja + cd build_coverage ; ctest -E '.*_default' -j10 + cd build_coverage ; ninja fastcov_html + open build_coverage/test/html/index.html + +########################################################################## +# documentation tests +########################################################################## + +# compile example files and check output +doctest: + $(MAKE) check_output -C doc + + +########################################################################## +# warning detector +########################################################################## + +# calling Clang with all warnings, except: +# -Wno-c++2a-compat: u8 literals will behave differently in C++20... +# -Wno-deprecated-declarations: the library deprecated some functions +# -Wno-documentation-unknown-command: code uses user-defined commands like @complexity +# -Wno-exit-time-destructors: warning in json code triggered by NLOHMANN_JSON_SERIALIZE_ENUM +# -Wno-float-equal: not all comparisons in the tests can be replaced by Approx +# -Wno-keyword-macro: unit-tests use "#define private public" +# -Wno-padded: padding is nothing to warn about +# -Wno-range-loop-analysis: items tests "for(const auto i...)" +# -Wno-switch-enum -Wno-covered-switch-default: pedantic/contradicting warnings about switches +# -Wno-weak-vtables: exception class is defined inline, but has virtual method +pedantic_clang: + $(MAKE) json_unit CXX=c++ CXXFLAGS=" \ + -std=c++11 -Wno-c++98-compat -Wno-c++98-compat-pedantic \ + -Werror \ + -Weverything \ + -Wno-c++2a-compat \ + -Wno-deprecated-declarations \ + -Wno-documentation-unknown-command \ + -Wno-exit-time-destructors \ + -Wno-float-equal \ + -Wno-keyword-macro \ + -Wno-padded \ + -Wno-range-loop-analysis \ + -Wno-switch-enum -Wno-covered-switch-default \ + -Wno-weak-vtables" + +# calling GCC with most warnings +pedantic_gcc: + $(MAKE) json_unit CXX=/usr/local/bin/g++-9 CXXFLAGS=" \ + -std=c++11 \ + -Waddress \ + -Waddress-of-packed-member \ + -Waggressive-loop-optimizations \ + -Waligned-new=all \ + -Wall \ + -Walloc-zero \ + -Walloca \ + -Warray-bounds \ + -Warray-bounds=2 \ + -Wattribute-alias=2 \ + -Wattribute-warning \ + -Wattributes \ + -Wbool-compare \ + -Wbool-operation \ + -Wbuiltin-declaration-mismatch \ + -Wbuiltin-macro-redefined \ + -Wcannot-profile \ + -Wcast-align \ + -Wcast-function-type \ + -Wcast-qual \ + -Wcatch-value=3 \ + -Wchar-subscripts \ + -Wclass-conversion \ + -Wclass-memaccess \ + -Wclobbered \ + -Wcomment \ + -Wcomments \ + -Wconditionally-supported \ + -Wconversion \ + -Wconversion-null \ + -Wcoverage-mismatch \ + -Wcpp \ + -Wctor-dtor-privacy \ + -Wdangling-else \ + -Wdate-time \ + -Wdelete-incomplete \ + -Wdelete-non-virtual-dtor \ + -Wdeprecated \ + -Wdeprecated-copy \ + -Wdeprecated-copy-dtor \ + -Wdeprecated-declarations \ + -Wdisabled-optimization \ + -Wdiv-by-zero \ + -Wdouble-promotion \ + -Wduplicated-branches \ + -Wduplicated-cond \ + -Weffc++ \ + -Wempty-body \ + -Wendif-labels \ + -Wenum-compare \ + -Wexpansion-to-defined \ + -Werror \ + -Wextra \ + -Wextra-semi \ + -Wfloat-conversion \ + -Wformat \ + -Wformat-contains-nul \ + -Wformat-extra-args \ + -Wformat-nonliteral \ + -Wformat-overflow=2 \ + -Wformat-security \ + -Wformat-signedness \ + -Wformat-truncation=2 \ + -Wformat-y2k \ + -Wformat-zero-length \ + -Wformat=2 \ + -Wframe-address \ + -Wfree-nonheap-object \ + -Whsa \ + -Wif-not-aligned \ + -Wignored-attributes \ + -Wignored-qualifiers \ + -Wimplicit-fallthrough=5 \ + -Winherited-variadic-ctor \ + -Winit-list-lifetime \ + -Winit-self \ + -Winline \ + -Wint-in-bool-context \ + -Wint-to-pointer-cast \ + -Winvalid-memory-model \ + -Winvalid-offsetof \ + -Winvalid-pch \ + -Wliteral-suffix \ + -Wlogical-not-parentheses \ + -Wlogical-op \ + -Wlto-type-mismatch \ + -Wmain \ + -Wmaybe-uninitialized \ + -Wmemset-elt-size \ + -Wmemset-transposed-args \ + -Wmisleading-indentation \ + -Wmissing-attributes \ + -Wmissing-braces \ + -Wmissing-declarations \ + -Wmissing-field-initializers \ + -Wmissing-format-attribute \ + -Wmissing-include-dirs \ + -Wmissing-noreturn \ + -Wmissing-profile \ + -Wmultichar \ + -Wmultiple-inheritance \ + -Wmultistatement-macros \ + -Wnarrowing \ + -Wno-deprecated-declarations \ + -Wno-float-equal \ + -Wno-long-long \ + -Wno-namespaces \ + -Wno-padded \ + -Wno-switch-enum \ + -Wno-system-headers \ + -Wno-templates \ + -Wno-undef \ + -Wnoexcept \ + -Wnoexcept-type \ + -Wnon-template-friend \ + -Wnon-virtual-dtor \ + -Wnonnull \ + -Wnonnull-compare \ + -Wnonportable-cfstrings \ + -Wnormalized \ + -Wnull-dereference \ + -Wodr \ + -Wold-style-cast \ + -Wopenmp-simd \ + -Woverflow \ + -Woverlength-strings \ + -Woverloaded-virtual \ + -Wpacked \ + -Wpacked-bitfield-compat \ + -Wpacked-not-aligned \ + -Wparentheses \ + -Wpedantic \ + -Wpessimizing-move \ + -Wplacement-new=2 \ + -Wpmf-conversions \ + -Wpointer-arith \ + -Wpointer-compare \ + -Wpragmas \ + -Wprio-ctor-dtor \ + -Wpsabi \ + -Wredundant-decls \ + -Wredundant-move \ + -Wregister \ + -Wreorder \ + -Wrestrict \ + -Wreturn-local-addr \ + -Wreturn-type \ + -Wscalar-storage-order \ + -Wsequence-point \ + -Wshadow \ + -Wshadow-compatible-local \ + -Wshadow-local \ + -Wshadow=compatible-local \ + -Wshadow=global \ + -Wshadow=local \ + -Wshift-count-negative \ + -Wshift-count-overflow \ + -Wshift-negative-value \ + -Wshift-overflow=2 \ + -Wsign-compare \ + -Wsign-conversion \ + -Wsign-promo \ + -Wsized-deallocation \ + -Wsizeof-array-argument \ + -Wsizeof-pointer-div \ + -Wsizeof-pointer-memaccess \ + -Wstack-protector \ + -Wstrict-aliasing=3 \ + -Wstrict-null-sentinel \ + -Wstrict-overflow=5 \ + -Wstringop-overflow=4 \ + -Wstringop-truncation \ + -Wsubobject-linkage \ + -Wsuggest-attribute=cold \ + -Wsuggest-attribute=const \ + -Wsuggest-attribute=format \ + -Wsuggest-attribute=malloc \ + -Wsuggest-attribute=noreturn \ + -Wsuggest-attribute=pure \ + -Wsuggest-final-methods \ + -Wsuggest-final-types \ + -Wsuggest-override \ + -Wswitch \ + -Wswitch-bool \ + -Wswitch-default \ + -Wswitch-unreachable \ + -Wsync-nand \ + -Wsynth \ + -Wtautological-compare \ + -Wterminate \ + -Wtrampolines \ + -Wtrigraphs \ + -Wtype-limits \ + -Wuninitialized \ + -Wunknown-pragmas \ + -Wunreachable-code \ + -Wunsafe-loop-optimizations \ + -Wunused \ + -Wunused-but-set-parameter \ + -Wunused-but-set-variable \ + -Wunused-const-variable=2 \ + -Wunused-function \ + -Wunused-label \ + -Wunused-local-typedefs \ + -Wunused-macros \ + -Wunused-parameter \ + -Wunused-result \ + -Wunused-value \ + -Wunused-variable \ + -Wuseless-cast \ + -Wvarargs \ + -Wvariadic-macros \ + -Wvector-operation-performance \ + -Wvirtual-inheritance \ + -Wvirtual-move-assign \ + -Wvla \ + -Wvolatile-register-var \ + -Wwrite-strings \ + -Wzero-as-null-pointer-constant \ + " + +########################################################################## +# benchmarks +########################################################################## + +run_benchmarks: + rm -fr build_benchmarks + mkdir build_benchmarks + cd build_benchmarks ; cmake ../benchmarks -GNinja -DCMAKE_BUILD_TYPE=Release + cd build_benchmarks ; ninja + cd build_benchmarks ; ./json_benchmarks + +########################################################################## +# fuzzing +########################################################################## + +# the overall fuzz testing target +fuzz_testing: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_afl_fuzzer -C test CXX=afl-clang++ + mv test/parse_afl_fuzzer fuzz-testing/fuzzer + find test/data/json_tests -size -5k -name *json | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + +fuzz_testing_bson: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_bson_fuzzer -C test CXX=afl-clang++ + mv test/parse_bson_fuzzer fuzz-testing/fuzzer + find test/data -size -5k -name *.bson | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + +fuzz_testing_cbor: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_cbor_fuzzer -C test CXX=afl-clang++ + mv test/parse_cbor_fuzzer fuzz-testing/fuzzer + find test/data -size -5k -name *.cbor | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + +fuzz_testing_msgpack: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_msgpack_fuzzer -C test CXX=afl-clang++ + mv test/parse_msgpack_fuzzer fuzz-testing/fuzzer + find test/data -size -5k -name *.msgpack | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + +fuzz_testing_ubjson: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_ubjson_fuzzer -C test CXX=afl-clang++ + mv test/parse_ubjson_fuzzer fuzz-testing/fuzzer + find test/data -size -5k -name *.ubjson | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + +fuzzing-start: + afl-fuzz -S fuzzer1 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -S fuzzer2 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -S fuzzer3 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -S fuzzer4 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -S fuzzer5 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -S fuzzer6 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -S fuzzer7 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer > /dev/null & + afl-fuzz -M fuzzer0 -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer + +fuzzing-stop: + -killall fuzzer + -killall afl-fuzz + + +########################################################################## +# Static analysis +########################################################################## + +# call cppcheck +# Note: this target is called by Travis +cppcheck: + cppcheck --enable=warning --inline-suppr --inconclusive --force --std=c++11 $(AMALGAMATED_FILE) --error-exitcode=1 + +# call Clang Static Analyzer +clang_analyze: + rm -fr clang_analyze_build + mkdir clang_analyze_build + cd clang_analyze_build ; CCC_CXX=$(COMPILER_DIR)/clang++ CXX=$(COMPILER_DIR)/clang++ $(COMPILER_DIR)/scan-build cmake .. -GNinja + cd clang_analyze_build ; \ + $(COMPILER_DIR)/scan-build \ + -enable-checker alpha.core.BoolAssignment,alpha.core.CallAndMessageUnInitRefArg,alpha.core.CastSize,alpha.core.CastToStruct,alpha.core.Conversion,alpha.core.DynamicTypeChecker,alpha.core.FixedAddr,alpha.core.PointerArithm,alpha.core.PointerSub,alpha.core.SizeofPtr,alpha.core.StackAddressAsyncEscape,alpha.core.TestAfterDivZero,alpha.deadcode.UnreachableCode,core.builtin.BuiltinFunctions,core.builtin.NoReturnFunctions,core.CallAndMessage,core.DivideZero,core.DynamicTypePropagation,core.NonnilStringConstants,core.NonNullParamChecker,core.NullDereference,core.StackAddressEscape,core.UndefinedBinaryOperatorResult,core.uninitialized.ArraySubscript,core.uninitialized.Assign,core.uninitialized.Branch,core.uninitialized.CapturedBlockVariable,core.uninitialized.UndefReturn,core.VLASize,cplusplus.InnerPointer,cplusplus.Move,cplusplus.NewDelete,cplusplus.NewDeleteLeaks,cplusplus.SelfAssignment,deadcode.DeadStores,nullability.NullableDereferenced,nullability.NullablePassedToNonnull,nullability.NullableReturnedFromNonnull,nullability.NullPassedToNonnull,nullability.NullReturnedFromNonnull \ + --use-c++=$(COMPILER_DIR)/clang++ -analyze-headers -o report ninja + open clang_analyze_build/report/*/index.html + +# call cpplint +# Note: some errors expected due to false positives +cpplint: + third_party/cpplint/cpplint.py \ + --filter=-whitespace,-legal,-readability/alt_tokens,-runtime/references,-runtime/explicit \ + --quiet --recursive $(SRCS) + +# call Clang-Tidy +clang_tidy: + $(COMPILER_DIR)/clang-tidy $(AMALGAMATED_FILE) -- -Iinclude -std=c++11 + +# call PVS-Studio Analyzer +pvs_studio: + rm -fr pvs_studio_build + mkdir pvs_studio_build + cd pvs_studio_build ; cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=On + cd pvs_studio_build ; pvs-studio-analyzer analyze -j 10 + cd pvs_studio_build ; plog-converter -a'GA:1,2;64:1;CS' -t fullhtml PVS-Studio.log -o pvs + open pvs_studio_build/pvs/index.html + +# call Infer static analyzer +infer: + rm -fr infer_build + mkdir infer_build + cd infer_build ; infer compile -- cmake .. ; infer run -- make -j 4 + +# call OCLint static analyzer +oclint: + oclint $(SRCS) -report-type html -enable-global-analysis -o oclint_report.html -max-priority-1=10000 -max-priority-2=10000 -max-priority-3=10000 -- -std=c++11 -Iinclude + open oclint_report.html + +# execute the test suite with Clang sanitizers (address and undefined behavior) +clang_sanitize: + rm -fr clang_sanitize_build + mkdir clang_sanitize_build + cd clang_sanitize_build ; CXX=$(COMPILER_DIR)/clang++ cmake .. -DJSON_Sanitizer=On -DJSON_MultipleHeaders=ON -GNinja + cd clang_sanitize_build ; ninja + cd clang_sanitize_build ; ctest -E '.*_default' -j10 + + +########################################################################## +# Code format and source amalgamation +########################################################################## + +# call the Artistic Style pretty printer on all source files +pretty: + astyle \ + --style=allman \ + --indent=spaces=4 \ + --indent-modifiers \ + --indent-switches \ + --indent-preproc-block \ + --indent-preproc-define \ + --indent-col1-comments \ + --pad-oper \ + --pad-header \ + --align-pointer=type \ + --align-reference=type \ + --add-brackets \ + --convert-tabs \ + --close-templates \ + --lineend=linux \ + --preserve-date \ + --suffix=none \ + --formatted \ + $(SRCS) $(AMALGAMATED_FILE) test/src/*.cpp benchmarks/src/benchmarks.cpp doc/examples/*.cpp + +# create single header file +amalgamate: $(AMALGAMATED_FILE) + +# call the amalgamation tool and pretty print +$(AMALGAMATED_FILE): $(SRCS) + third_party/amalgamate/amalgamate.py -c third_party/amalgamate/config.json -s . --verbose=yes + $(MAKE) pretty + +# check if file single_include/nlohmann/json.hpp has been amalgamated from the nlohmann sources +# Note: this target is called by Travis +check-amalgamation: + @mv $(AMALGAMATED_FILE) $(AMALGAMATED_FILE)~ + @$(MAKE) amalgamate + @diff $(AMALGAMATED_FILE) $(AMALGAMATED_FILE)~ || (echo "===================================================================\n Amalgamation required! Please read the contribution guidelines\n in file .github/CONTRIBUTING.md.\n===================================================================" ; mv $(AMALGAMATED_FILE)~ $(AMALGAMATED_FILE) ; false) + @mv $(AMALGAMATED_FILE)~ $(AMALGAMATED_FILE) + +# check if every header in nlohmann includes sufficient headers to be compiled individually +check-single-includes: + @for x in $(SRCS); do \ + echo "Checking self-sufficiency of $$x..." ; \ + echo "#include <$$x>\nint main() {}\n" | sed 's|include/||' > single_include_test.cpp; \ + $(CXX) $(CXXFLAGS) -Iinclude -std=c++11 single_include_test.cpp -o single_include_test; \ + rm -f single_include_test.cpp single_include_test; \ + done + + +########################################################################## +# CMake +########################################################################## + +# grep "^option" CMakeLists.txt test/CMakeLists.txt | sed 's/(/ /' | awk '{print $2}' | xargs + +# check if all flags of our CMake files work +check_cmake_flags_do: + $(CMAKE_BINARY) --version + for flag in '' JSON_BuildTests JSON_Install JSON_MultipleHeaders JSON_Sanitizer JSON_Valgrind JSON_NoExceptions JSON_Coverage; do \ + rm -fr cmake_build; \ + mkdir cmake_build; \ + echo "$(CMAKE_BINARY) .. -D$$flag=On" ; \ + cd cmake_build ; \ + CXX=g++-8 $(CMAKE_BINARY) .. -D$$flag=On -DCMAKE_CXX_COMPILE_FEATURES="cxx_std_11;cxx_range_for" -DCMAKE_CXX_FLAGS="-std=gnu++11" ; \ + test -f Makefile || exit 1 ; \ + cd .. ; \ + done; + +# call target `check_cmake_flags_do` twice: once for minimal required CMake version 3.1.0 and once for the installed version +check_cmake_flags: + wget https://github.com/Kitware/CMake/releases/download/v3.1.0/cmake-3.1.0-Darwin64.tar.gz + tar xfz cmake-3.1.0-Darwin64.tar.gz + CMAKE_BINARY=$(abspath cmake-3.1.0-Darwin64/CMake.app/Contents/bin/cmake) $(MAKE) check_cmake_flags_do + CMAKE_BINARY=$(shell which cmake) $(MAKE) check_cmake_flags_do + + +########################################################################## +# ChangeLog +########################################################################## + +# Create a ChangeLog based on the git log using the GitHub Changelog Generator +# (). + +# variable to control the diffs between the last released version and the current repository state +NEXT_VERSION ?= "unreleased" + +ChangeLog.md: + github_changelog_generator -o ChangeLog.md --simple-list --release-url https://github.com/nlohmann/json/releases/tag/%s --future-release $(NEXT_VERSION) + $(SED) -i 's|https://github.com/nlohmann/json/releases/tag/HEAD|https://github.com/nlohmann/json/tree/HEAD|' ChangeLog.md + $(SED) -i '2i All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).' ChangeLog.md + + +########################################################################## +# Release files +########################################################################## + +# Create the files for a release and add signatures and hashes. We use `-X` to make the resulting ZIP file +# reproducible, see . + +release: + rm -fr release_files + mkdir release_files + zip -9 --recurse-paths -X include.zip $(SRCS) + gpg --armor --detach-sig include.zip + mv include.zip include.zip.asc release_files + gpg --armor --detach-sig $(AMALGAMATED_FILE) + cp $(AMALGAMATED_FILE) release_files + mv $(AMALGAMATED_FILE).asc release_files + cd release_files ; shasum -a 256 json.hpp > hashes.txt + cd release_files ; shasum -a 256 include.zip >> hashes.txt + + +########################################################################## +# Maintenance +########################################################################## + +# clean up +clean: + rm -fr json_unit json_benchmarks fuzz fuzz-testing *.dSYM test/*.dSYM oclint_report.html + rm -fr benchmarks/files/numbers/*.json + rm -fr cmake-3.1.0-Darwin64.tar.gz cmake-3.1.0-Darwin64 + rm -fr build_coverage build_benchmarks fuzz-testing clang_analyze_build pvs_studio_build infer_build clang_sanitize_build cmake_build + $(MAKE) clean -Cdoc + $(MAKE) clean -Ctest + +########################################################################## +# Thirdparty code +########################################################################## + +update_hedley: + rm -f include/nlohmann/thirdparty/hedley/hedley.hpp include/nlohmann/thirdparty/hedley/hedley_undef.hpp + curl https://raw.githubusercontent.com/nemequ/hedley/master/hedley.h -o include/nlohmann/thirdparty/hedley/hedley.hpp + gsed -i 's/HEDLEY_/JSON_HEDLEY_/g' include/nlohmann/thirdparty/hedley/hedley.hpp + grep "[[:blank:]]*#[[:blank:]]*undef" include/nlohmann/thirdparty/hedley/hedley.hpp | grep -v "__" | sort | uniq | gsed 's/ //g' | gsed 's/undef/undef /g' > include/nlohmann/thirdparty/hedley/hedley_undef.hpp + $(MAKE) amalgamate diff --git a/libraries/cppdap/third_party/json/README.md b/libraries/cppdap/third_party/json/README.md new file mode 100644 index 000000000..9d395c064 --- /dev/null +++ b/libraries/cppdap/third_party/json/README.md @@ -0,0 +1,1371 @@ +[![JSON for Modern C++](https://raw.githubusercontent.com/nlohmann/json/master/doc/json.gif)](https://github.com/nlohmann/json/releases) + +[![Build Status](https://travis-ci.org/nlohmann/json.svg?branch=master)](https://travis-ci.org/nlohmann/json) +[![Build Status](https://ci.appveyor.com/api/projects/status/1acb366xfyg3qybk/branch/develop?svg=true)](https://ci.appveyor.com/project/nlohmann/json) +[![Build Status](https://circleci.com/gh/nlohmann/json.svg?style=svg)](https://circleci.com/gh/nlohmann/json) +[![Coverage Status](https://img.shields.io/coveralls/nlohmann/json.svg)](https://coveralls.io/r/nlohmann/json) +[![Coverity Scan Build Status](https://scan.coverity.com/projects/5550/badge.svg)](https://scan.coverity.com/projects/nlohmann-json) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/f3732b3327e34358a0e9d1fe9f661f08)](https://www.codacy.com/app/nlohmann/json?utm_source=github.com&utm_medium=referral&utm_content=nlohmann/json&utm_campaign=Badge_Grade) +[![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/nlohmann/json.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nlohmann/json/context:cpp) +[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/json.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:json) +[![Try online](https://img.shields.io/badge/try-online-blue.svg)](https://wandbox.org/permlink/TarF5pPn9NtHQjhf) +[![Documentation](https://img.shields.io/badge/docs-doxygen-blue.svg)](http://nlohmann.github.io/json) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nlohmann/json/master/LICENSE.MIT) +[![GitHub Releases](https://img.shields.io/github/release/nlohmann/json.svg)](https://github.com/nlohmann/json/releases) +[![GitHub Issues](https://img.shields.io/github/issues/nlohmann/json.svg)](http://github.com/nlohmann/json/issues) +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/nlohmann/json.svg)](http://isitmaintained.com/project/nlohmann/json "Average time to resolve an issue") +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/289/badge)](https://bestpractices.coreinfrastructure.org/projects/289) + +- [Design goals](#design-goals) +- [Integration](#integration) + - [CMake](#cmake) + - [Package Managers](#package-managers) +- [Examples](#examples) + - [JSON as first-class data type](#json-as-first-class-data-type) + - [Serialization / Deserialization](#serialization--deserialization) + - [STL-like access](#stl-like-access) + - [Conversion from STL containers](#conversion-from-stl-containers) + - [JSON Pointer and JSON Patch](#json-pointer-and-json-patch) + - [JSON Merge Patch](#json-merge-patch) + - [Implicit conversions](#implicit-conversions) + - [Conversions to/from arbitrary types](#arbitrary-types-conversions) + - [Specializing enum conversion](#specializing-enum-conversion) + - [Binary formats (BSON, CBOR, MessagePack, and UBJSON)](#binary-formats-bson-cbor-messagepack-and-ubjson) +- [Supported compilers](#supported-compilers) +- [License](#license) +- [Contact](#contact) +- [Thanks](#thanks) +- [Used third-party tools](#used-third-party-tools) +- [Projects using JSON for Modern C++](#projects-using-json-for-modern-c) +- [Notes](#notes) +- [Execute unit tests](#execute-unit-tests) + +## Design goals + +There are myriads of [JSON](http://json.org) libraries out there, and each may even have its reason to exist. Our class had these design goals: + +- **Intuitive syntax**. In languages such as Python, JSON feels like a first class data type. We used all the operator magic of modern C++ to achieve the same feeling in your code. Check out the [examples below](#examples) and you'll know what I mean. + +- **Trivial integration**. Our whole code consists of a single header file [`json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp). That's it. No library, no subproject, no dependencies, no complex build system. The class is written in vanilla C++11. All in all, everything should require no adjustment of your compiler flags or project settings. + +- **Serious testing**. Our class is heavily [unit-tested](https://github.com/nlohmann/json/tree/develop/test/src) and covers [100%](https://coveralls.io/r/nlohmann/json) of the code, including all exceptional behavior. Furthermore, we checked with [Valgrind](http://valgrind.org) and the [Clang Sanitizers](https://clang.llvm.org/docs/index.html) that there are no memory leaks. [Google OSS-Fuzz](https://github.com/google/oss-fuzz/tree/master/projects/json) additionally runs fuzz tests against all parsers 24/7, effectively executing billions of tests so far. To maintain high quality, the project is following the [Core Infrastructure Initiative (CII) best practices](https://bestpractices.coreinfrastructure.org/projects/289). + +Other aspects were not so important to us: + +- **Memory efficiency**. Each JSON object has an overhead of one pointer (the maximal size of a union) and one enumeration element (1 byte). The default generalization uses the following C++ data types: `std::string` for strings, `int64_t`, `uint64_t` or `double` for numbers, `std::map` for objects, `std::vector` for arrays, and `bool` for Booleans. However, you can template the generalized class `basic_json` to your needs. + +- **Speed**. There are certainly [faster JSON libraries](https://github.com/miloyip/nativejson-benchmark#parsing-time) out there. However, if your goal is to speed up your development by adding JSON support with a single header, then this library is the way to go. If you know how to use a `std::vector` or `std::map`, you are already set. + +See the [contribution guidelines](https://github.com/nlohmann/json/blob/master/.github/CONTRIBUTING.md#please-dont) for more information. + + +## Integration + +[`json.hpp`](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp) is the single required file in `single_include/nlohmann` or [released here](https://github.com/nlohmann/json/releases). You need to add + +```cpp +#include + +// for convenience +using json = nlohmann::json; +``` + +to the files you want to process JSON and set the necessary switches to enable C++11 (e.g., `-std=c++11` for GCC and Clang). + +You can further use file [`include/nlohmann/json_fwd.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/json_fwd.hpp) for forward-declarations. The installation of json_fwd.hpp (as part of cmake's install step), can be achieved by setting `-DJSON_MultipleHeaders=ON`. + +### CMake + +You can also use the `nlohmann_json::nlohmann_json` interface target in CMake. This target populates the appropriate usage requirements for `INTERFACE_INCLUDE_DIRECTORIES` to point to the appropriate include directories and `INTERFACE_COMPILE_FEATURES` for the necessary C++11 flags. + +#### External + +To use this library from a CMake project, you can locate it directly with `find_package()` and use the namespaced imported target from the generated package configuration: + +```cmake +# CMakeLists.txt +find_package(nlohmann_json 3.2.0 REQUIRED) +... +add_library(foo ...) +... +target_link_libraries(foo PRIVATE nlohmann_json::nlohmann_json) +``` + +The package configuration file, `nlohmann_jsonConfig.cmake`, can be used either from an install tree or directly out of the build tree. + +#### Embedded + +To embed the library directly into an existing CMake project, place the entire source tree in a subdirectory and call `add_subdirectory()` in your `CMakeLists.txt` file: + +```cmake +# Typically you don't care so much for a third party library's tests to be +# run from your own project's code. +set(JSON_BuildTests OFF CACHE INTERNAL "") + +# If you only include this third party in PRIVATE source files, you do not +# need to install it when your main project gets installed. +# set(JSON_Install OFF CACHE INTERNAL "") + +# Don't use include(nlohmann_json/CMakeLists.txt) since that carries with it +# unintended consequences that will break the build. It's generally +# discouraged (although not necessarily well documented as such) to use +# include(...) for pulling in other CMake projects anyways. +add_subdirectory(nlohmann_json) +... +add_library(foo ...) +... +target_link_libraries(foo PRIVATE nlohmann_json::nlohmann_json) +``` + +#### Supporting Both + +To allow your project to support either an externally supplied or an embedded JSON library, you can use a pattern akin to the following: + +``` cmake +# Top level CMakeLists.txt +project(FOO) +... +option(FOO_USE_EXTERNAL_JSON "Use an external JSON library" OFF) +... +add_subdirectory(thirdparty) +... +add_library(foo ...) +... +# Note that the namespaced target will always be available regardless of the +# import method +target_link_libraries(foo PRIVATE nlohmann_json::nlohmann_json) +``` +```cmake +# thirdparty/CMakeLists.txt +... +if(FOO_USE_EXTERNAL_JSON) + find_package(nlohmann_json 3.2.0 REQUIRED) +else() + set(JSON_BuildTests OFF CACHE INTERNAL "") + add_subdirectory(nlohmann_json) +endif() +... +``` + +`thirdparty/nlohmann_json` is then a complete copy of this source tree. + +### Package Managers + +:beer: If you are using OS X and [Homebrew](http://brew.sh), just type `brew tap nlohmann/json` and `brew install nlohmann-json` and you're set. If you want the bleeding edge rather than the latest release, use `brew install nlohmann-json --HEAD`. + +If you are using the [Meson Build System](http://mesonbuild.com), then you can get a wrap file by downloading it from [Meson WrapDB](https://wrapdb.mesonbuild.com/nlohmann_json), or simply use `meson wrap install nlohmann_json`. + +If you are using [Conan](https://www.conan.io/) to manage your dependencies, merely add `jsonformoderncpp/x.y.z@vthiery/stable` to your `conanfile.py`'s requires, where `x.y.z` is the release version you want to use. Please file issues [here](https://github.com/vthiery/conan-jsonformoderncpp/issues) if you experience problems with the packages. + +If you are using [Spack](https://www.spack.io/) to manage your dependencies, you can use the [`nlohmann-json` package](https://spack.readthedocs.io/en/latest/package_list.html#nlohmann-json). Please see the [spack project](https://github.com/spack/spack) for any issues regarding the packaging. + +If you are using [hunter](https://github.com/ruslo/hunter/) on your project for external dependencies, then you can use the [nlohmann_json package](https://docs.hunter.sh/en/latest/packages/pkg/nlohmann_json.html). Please see the hunter project for any issues regarding the packaging. + +If you are using [Buckaroo](https://buckaroo.pm), you can install this library's module with `buckaroo add github.com/buckaroo-pm/nlohmann-json`. Please file issues [here](https://github.com/buckaroo-pm/nlohmann-json). There is a demo repo [here](https://github.com/njlr/buckaroo-nholmann-json-example). + +If you are using [vcpkg](https://github.com/Microsoft/vcpkg/) on your project for external dependencies, then you can use the [nlohmann-json package](https://github.com/Microsoft/vcpkg/tree/master/ports/nlohmann-json). Please see the vcpkg project for any issues regarding the packaging. + +If you are using [cget](http://cget.readthedocs.io/en/latest/), you can install the latest development version with `cget install nlohmann/json`. A specific version can be installed with `cget install nlohmann/json@v3.1.0`. Also, the multiple header version can be installed by adding the `-DJSON_MultipleHeaders=ON` flag (i.e., `cget install nlohmann/json -DJSON_MultipleHeaders=ON`). + +If you are using [CocoaPods](https://cocoapods.org), you can use the library by adding pod `"nlohmann_json", '~>3.1.2'` to your podfile (see [an example](https://bitbucket.org/benman/nlohmann_json-cocoapod/src/master/)). Please file issues [here](https://bitbucket.org/benman/nlohmann_json-cocoapod/issues?status=new&status=open). + +If you are using [NuGet](https://www.nuget.org), you can use the package [nlohmann.json](https://www.nuget.org/packages/nlohmann.json/). Please check [this extensive description](https://github.com/nlohmann/json/issues/1132#issuecomment-452250255) on how to use the package. Please files issues [here](https://github.com/hnkb/nlohmann-json-nuget/issues). + +If you are using [conda](https://conda.io/), you can use the package [nlohmann_json](https://github.com/conda-forge/nlohmann_json-feedstock) from [conda-forge](https://conda-forge.org) executing `conda install -c conda-forge nlohmann_json`. Please file issues [here](https://github.com/conda-forge/nlohmann_json-feedstock/issues). + +If you are using [MSYS2](http://www.msys2.org/), your can use the [mingw-w64-nlohmann_json](https://packages.msys2.org/base/mingw-w64-nlohmann_json) package, just type `pacman -S mingw-w64-i686-nlohmann_json` or `pacman -S mingw-w64-x86_64-nlohmann_json` for installation. Please file issues [here](https://github.com/msys2/MINGW-packages/issues/new?title=%5Bnlohmann_json%5D) if you experience problems with the packages. + +## Examples + +Beside the examples below, you may want to check the [documentation](https://nlohmann.github.io/json/) where each function contains a separate code example (e.g., check out [`emplace()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a5338e282d1d02bed389d852dd670d98d.html#a5338e282d1d02bed389d852dd670d98d)). All [example files](https://github.com/nlohmann/json/tree/develop/doc/examples) can be compiled and executed on their own (e.g., file [emplace.cpp](https://github.com/nlohmann/json/blob/develop/doc/examples/emplace.cpp)). + +### JSON as first-class data type + +Here are some examples to give you an idea how to use the class. + +Assume you want to create the JSON object + +```json +{ + "pi": 3.141, + "happy": true, + "name": "Niels", + "nothing": null, + "answer": { + "everything": 42 + }, + "list": [1, 0, 2], + "object": { + "currency": "USD", + "value": 42.99 + } +} +``` + +With this library, you could write: + +```cpp +// create an empty structure (null) +json j; + +// add a number that is stored as double (note the implicit conversion of j to an object) +j["pi"] = 3.141; + +// add a Boolean that is stored as bool +j["happy"] = true; + +// add a string that is stored as std::string +j["name"] = "Niels"; + +// add another null object by passing nullptr +j["nothing"] = nullptr; + +// add an object inside the object +j["answer"]["everything"] = 42; + +// add an array that is stored as std::vector (using an initializer list) +j["list"] = { 1, 0, 2 }; + +// add another object (using an initializer list of pairs) +j["object"] = { {"currency", "USD"}, {"value", 42.99} }; + +// instead, you could also write (which looks very similar to the JSON above) +json j2 = { + {"pi", 3.141}, + {"happy", true}, + {"name", "Niels"}, + {"nothing", nullptr}, + {"answer", { + {"everything", 42} + }}, + {"list", {1, 0, 2}}, + {"object", { + {"currency", "USD"}, + {"value", 42.99} + }} +}; +``` + +Note that in all these cases, you never need to "tell" the compiler which JSON value type you want to use. If you want to be explicit or express some edge cases, the functions [`json::array()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a9ad7ec0bc1082ed09d10900fbb20a21f.html#a9ad7ec0bc1082ed09d10900fbb20a21f) and [`json::object()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_aaf509a7c029100d292187068f61c99b8.html#aaf509a7c029100d292187068f61c99b8) will help: + +```cpp +// a way to express the empty array [] +json empty_array_explicit = json::array(); + +// ways to express the empty object {} +json empty_object_implicit = json({}); +json empty_object_explicit = json::object(); + +// a way to express an _array_ of key/value pairs [["currency", "USD"], ["value", 42.99]] +json array_not_object = json::array({ {"currency", "USD"}, {"value", 42.99} }); +``` + +### Serialization / Deserialization + +#### To/from strings + +You can create a JSON value (deserialization) by appending `_json` to a string literal: + +```cpp +// create object from string literal +json j = "{ \"happy\": true, \"pi\": 3.141 }"_json; + +// or even nicer with a raw string literal +auto j2 = R"( + { + "happy": true, + "pi": 3.141 + } +)"_json; +``` + +Note that without appending the `_json` suffix, the passed string literal is not parsed, but just used as JSON string value. That is, `json j = "{ \"happy\": true, \"pi\": 3.141 }"` would just store the string `"{ "happy": true, "pi": 3.141 }"` rather than parsing the actual object. + +The above example can also be expressed explicitly using [`json::parse()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_afd4ef1ac8ad50a5894a9afebca69140a.html#afd4ef1ac8ad50a5894a9afebca69140a): + +```cpp +// parse explicitly +auto j3 = json::parse("{ \"happy\": true, \"pi\": 3.141 }"); +``` + +You can also get a string representation of a JSON value (serialize): + +```cpp +// explicit conversion to string +std::string s = j.dump(); // {\"happy\":true,\"pi\":3.141} + +// serialization with pretty printing +// pass in the amount of spaces to indent +std::cout << j.dump(4) << std::endl; +// { +// "happy": true, +// "pi": 3.141 +// } +``` + +Note the difference between serialization and assignment: + +```cpp +// store a string in a JSON value +json j_string = "this is a string"; + +// retrieve the string value +auto cpp_string = j_string.get(); +// retrieve the string value (alternative when an variable already exists) +std::string cpp_string2; +j_string.get_to(cpp_string2); + +// retrieve the serialized value (explicit JSON serialization) +std::string serialized_string = j_string.dump(); + +// output of original string +std::cout << cpp_string << " == " << cpp_string2 << " == " << j_string.get() << '\n'; +// output of serialized value +std::cout << j_string << " == " << serialized_string << std::endl; +``` + +[`.dump()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a50ec80b02d0f3f51130d4abb5d1cfdc5.html#a50ec80b02d0f3f51130d4abb5d1cfdc5) always returns the serialized value, and [`.get()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_aa6602bb24022183ab989439e19345d08.html#aa6602bb24022183ab989439e19345d08) returns the originally stored string value. + +Note the library only supports UTF-8. When you store strings with different encodings in the library, calling [`dump()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a50ec80b02d0f3f51130d4abb5d1cfdc5.html#a50ec80b02d0f3f51130d4abb5d1cfdc5) may throw an exception unless `json::error_handler_t::replace` or `json::error_handler_t::ignore` are used as error handlers. + +#### To/from streams (e.g. files, string streams) + +You can also use streams to serialize and deserialize: + +```cpp +// deserialize from standard input +json j; +std::cin >> j; + +// serialize to standard output +std::cout << j; + +// the setw manipulator was overloaded to set the indentation for pretty printing +std::cout << std::setw(4) << j << std::endl; +``` + +These operators work for any subclasses of `std::istream` or `std::ostream`. Here is the same example with files: + +```cpp +// read a JSON file +std::ifstream i("file.json"); +json j; +i >> j; + +// write prettified JSON to another file +std::ofstream o("pretty.json"); +o << std::setw(4) << j << std::endl; +``` + +Please note that setting the exception bit for `failbit` is inappropriate for this use case. It will result in program termination due to the `noexcept` specifier in use. + +#### Read from iterator range + +You can also parse JSON from an iterator range; that is, from any container accessible by iterators whose content is stored as contiguous byte sequence, for instance a `std::vector`: + +```cpp +std::vector v = {'t', 'r', 'u', 'e'}; +json j = json::parse(v.begin(), v.end()); +``` + +You may leave the iterators for the range [begin, end): + +```cpp +std::vector v = {'t', 'r', 'u', 'e'}; +json j = json::parse(v); +``` + +#### SAX interface + +The library uses a SAX-like interface with the following functions: + +```cpp +// called when null is parsed +bool null(); + +// called when a boolean is parsed; value is passed +bool boolean(bool val); + +// called when a signed or unsigned integer number is parsed; value is passed +bool number_integer(number_integer_t val); +bool number_unsigned(number_unsigned_t val); + +// called when a floating-point number is parsed; value and original string is passed +bool number_float(number_float_t val, const string_t& s); + +// called when a string is parsed; value is passed and can be safely moved away +bool string(string_t& val); + +// called when an object or array begins or ends, resp. The number of elements is passed (or -1 if not known) +bool start_object(std::size_t elements); +bool end_object(); +bool start_array(std::size_t elements); +bool end_array(); +// called when an object key is parsed; value is passed and can be safely moved away +bool key(string_t& val); + +// called when a parse error occurs; byte position, the last token, and an exception is passed +bool parse_error(std::size_t position, const std::string& last_token, const detail::exception& ex); +``` + +The return value of each function determines whether parsing should proceed. + +To implement your own SAX handler, proceed as follows: + +1. Implement the SAX interface in a class. You can use class `nlohmann::json_sax` as base class, but you can also use any class where the functions described above are implemented and public. +2. Create an object of your SAX interface class, e.g. `my_sax`. +3. Call `bool json::sax_parse(input, &my_sax)`; where the first parameter can be any input like a string or an input stream and the second parameter is a pointer to your SAX interface. + +Note the `sax_parse` function only returns a `bool` indicating the result of the last executed SAX event. It does not return a `json` value - it is up to you to decide what to do with the SAX events. Furthermore, no exceptions are thrown in case of a parse error - it is up to you what to do with the exception object passed to your `parse_error` implementation. Internally, the SAX interface is used for the DOM parser (class `json_sax_dom_parser`) as well as the acceptor (`json_sax_acceptor`), see file [`json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp). + +### STL-like access + +We designed the JSON class to behave just like an STL container. In fact, it satisfies the [**ReversibleContainer**](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer) requirement. + +```cpp +// create an array using push_back +json j; +j.push_back("foo"); +j.push_back(1); +j.push_back(true); + +// also use emplace_back +j.emplace_back(1.78); + +// iterate the array +for (json::iterator it = j.begin(); it != j.end(); ++it) { + std::cout << *it << '\n'; +} + +// range-based for +for (auto& element : j) { + std::cout << element << '\n'; +} + +// getter/setter +const auto tmp = j[0].get(); +j[1] = 42; +bool foo = j.at(2); + +// comparison +j == "[\"foo\", 1, true]"_json; // true + +// other stuff +j.size(); // 3 entries +j.empty(); // false +j.type(); // json::value_t::array +j.clear(); // the array is empty again + +// convenience type checkers +j.is_null(); +j.is_boolean(); +j.is_number(); +j.is_object(); +j.is_array(); +j.is_string(); + +// create an object +json o; +o["foo"] = 23; +o["bar"] = false; +o["baz"] = 3.141; + +// also use emplace +o.emplace("weather", "sunny"); + +// special iterator member functions for objects +for (json::iterator it = o.begin(); it != o.end(); ++it) { + std::cout << it.key() << " : " << it.value() << "\n"; +} + +// the same code as range for +for (auto& el : o.items()) { + std::cout << el.key() << " : " << el.value() << "\n"; +} + +// even easier with structured bindings (C++17) +for (auto& [key, value] : o.items()) { + std::cout << key << " : " << value << "\n"; +} + +// find an entry +if (o.find("foo") != o.end()) { + // there is an entry with key "foo" +} + +// or simpler using count() +int foo_present = o.count("foo"); // 1 +int fob_present = o.count("fob"); // 0 + +// delete an entry +o.erase("foo"); +``` + + +### Conversion from STL containers + +Any sequence container (`std::array`, `std::vector`, `std::deque`, `std::forward_list`, `std::list`) whose values can be used to construct JSON values (e.g., integers, floating point numbers, Booleans, string types, or again STL containers described in this section) can be used to create a JSON array. The same holds for similar associative containers (`std::set`, `std::multiset`, `std::unordered_set`, `std::unordered_multiset`), but in these cases the order of the elements of the array depends on how the elements are ordered in the respective STL container. + +```cpp +std::vector c_vector {1, 2, 3, 4}; +json j_vec(c_vector); +// [1, 2, 3, 4] + +std::deque c_deque {1.2, 2.3, 3.4, 5.6}; +json j_deque(c_deque); +// [1.2, 2.3, 3.4, 5.6] + +std::list c_list {true, true, false, true}; +json j_list(c_list); +// [true, true, false, true] + +std::forward_list c_flist {12345678909876, 23456789098765, 34567890987654, 45678909876543}; +json j_flist(c_flist); +// [12345678909876, 23456789098765, 34567890987654, 45678909876543] + +std::array c_array {{1, 2, 3, 4}}; +json j_array(c_array); +// [1, 2, 3, 4] + +std::set c_set {"one", "two", "three", "four", "one"}; +json j_set(c_set); // only one entry for "one" is used +// ["four", "one", "three", "two"] + +std::unordered_set c_uset {"one", "two", "three", "four", "one"}; +json j_uset(c_uset); // only one entry for "one" is used +// maybe ["two", "three", "four", "one"] + +std::multiset c_mset {"one", "two", "one", "four"}; +json j_mset(c_mset); // both entries for "one" are used +// maybe ["one", "two", "one", "four"] + +std::unordered_multiset c_umset {"one", "two", "one", "four"}; +json j_umset(c_umset); // both entries for "one" are used +// maybe ["one", "two", "one", "four"] +``` + +Likewise, any associative key-value containers (`std::map`, `std::multimap`, `std::unordered_map`, `std::unordered_multimap`) whose keys can construct an `std::string` and whose values can be used to construct JSON values (see examples above) can be used to create a JSON object. Note that in case of multimaps only one key is used in the JSON object and the value depends on the internal order of the STL container. + +```cpp +std::map c_map { {"one", 1}, {"two", 2}, {"three", 3} }; +json j_map(c_map); +// {"one": 1, "three": 3, "two": 2 } + +std::unordered_map c_umap { {"one", 1.2}, {"two", 2.3}, {"three", 3.4} }; +json j_umap(c_umap); +// {"one": 1.2, "two": 2.3, "three": 3.4} + +std::multimap c_mmap { {"one", true}, {"two", true}, {"three", false}, {"three", true} }; +json j_mmap(c_mmap); // only one entry for key "three" is used +// maybe {"one": true, "two": true, "three": true} + +std::unordered_multimap c_ummap { {"one", true}, {"two", true}, {"three", false}, {"three", true} }; +json j_ummap(c_ummap); // only one entry for key "three" is used +// maybe {"one": true, "two": true, "three": true} +``` + +### JSON Pointer and JSON Patch + +The library supports **JSON Pointer** ([RFC 6901](https://tools.ietf.org/html/rfc6901)) as alternative means to address structured values. On top of this, **JSON Patch** ([RFC 6902](https://tools.ietf.org/html/rfc6902)) allows to describe differences between two JSON values - effectively allowing patch and diff operations known from Unix. + +```cpp +// a JSON value +json j_original = R"({ + "baz": ["one", "two", "three"], + "foo": "bar" +})"_json; + +// access members with a JSON pointer (RFC 6901) +j_original["/baz/1"_json_pointer]; +// "two" + +// a JSON patch (RFC 6902) +json j_patch = R"([ + { "op": "replace", "path": "/baz", "value": "boo" }, + { "op": "add", "path": "/hello", "value": ["world"] }, + { "op": "remove", "path": "/foo"} +])"_json; + +// apply the patch +json j_result = j_original.patch(j_patch); +// { +// "baz": "boo", +// "hello": ["world"] +// } + +// calculate a JSON patch from two JSON values +json::diff(j_result, j_original); +// [ +// { "op":" replace", "path": "/baz", "value": ["one", "two", "three"] }, +// { "op": "remove","path": "/hello" }, +// { "op": "add", "path": "/foo", "value": "bar" } +// ] +``` + +### JSON Merge Patch + +The library supports **JSON Merge Patch** ([RFC 7386](https://tools.ietf.org/html/rfc7386)) as a patch format. Instead of using JSON Pointer (see above) to specify values to be manipulated, it describes the changes using a syntax that closely mimics the document being modified. + +```cpp +// a JSON value +json j_document = R"({ + "a": "b", + "c": { + "d": "e", + "f": "g" + } +})"_json; + +// a patch +json j_patch = R"({ + "a":"z", + "c": { + "f": null + } +})"_json; + +// apply the patch +j_document.merge_patch(j_patch); +// { +// "a": "z", +// "c": { +// "d": "e" +// } +// } +``` + +### Implicit conversions + +Supported types can be implicitly converted to JSON values. + +It is recommended to **NOT USE** implicit conversions **FROM** a JSON value. +You can find more details about this recommendation [here](https://www.github.com/nlohmann/json/issues/958). + +```cpp +// strings +std::string s1 = "Hello, world!"; +json js = s1; +auto s2 = js.get(); +// NOT RECOMMENDED +std::string s3 = js; +std::string s4; +s4 = js; + +// Booleans +bool b1 = true; +json jb = b1; +auto b2 = jb.get(); +// NOT RECOMMENDED +bool b3 = jb; +bool b4; +b4 = jb; + +// numbers +int i = 42; +json jn = i; +auto f = jn.get(); +// NOT RECOMMENDED +double f2 = jb; +double f3; +f3 = jb; + +// etc. +``` + +Note that `char` types are not automatically converted to JSON strings, but to integer numbers. A conversion to a string must be specified explicitly: + +```cpp +char ch = 'A'; // ASCII value 65 +json j_default = ch; // stores integer number 65 +json j_string = std::string(1, ch); // stores string "A" +``` + +### Arbitrary types conversions + +Every type can be serialized in JSON, not just STL containers and scalar types. Usually, you would do something along those lines: + +```cpp +namespace ns { + // a simple struct to model a person + struct person { + std::string name; + std::string address; + int age; + }; +} + +ns::person p = {"Ned Flanders", "744 Evergreen Terrace", 60}; + +// convert to JSON: copy each value into the JSON object +json j; +j["name"] = p.name; +j["address"] = p.address; +j["age"] = p.age; + +// ... + +// convert from JSON: copy each value from the JSON object +ns::person p { + j["name"].get(), + j["address"].get(), + j["age"].get() +}; +``` + +It works, but that's quite a lot of boilerplate... Fortunately, there's a better way: + +```cpp +// create a person +ns::person p {"Ned Flanders", "744 Evergreen Terrace", 60}; + +// conversion: person -> json +json j = p; + +std::cout << j << std::endl; +// {"address":"744 Evergreen Terrace","age":60,"name":"Ned Flanders"} + +// conversion: json -> person +auto p2 = j.get(); + +// that's it +assert(p == p2); +``` + +#### Basic usage + +To make this work with one of your types, you only need to provide two functions: + +```cpp +using nlohmann::json; + +namespace ns { + void to_json(json& j, const person& p) { + j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}}; + } + + void from_json(const json& j, person& p) { + j.at("name").get_to(p.name); + j.at("address").get_to(p.address); + j.at("age").get_to(p.age); + } +} // namespace ns +``` + +That's all! When calling the `json` constructor with your type, your custom `to_json` method will be automatically called. +Likewise, when calling `get()` or `get_to(your_type&)`, the `from_json` method will be called. + +Some important things: + +* Those methods **MUST** be in your type's namespace (which can be the global namespace), or the library will not be able to locate them (in this example, they are in namespace `ns`, where `person` is defined). +* Those methods **MUST** be available (e.g., proper headers must be included) everywhere you use these conversions. Look at [issue 1108](https://github.com/nlohmann/json/issues/1108) for errors that may occur otherwise. +* When using `get()`, `your_type` **MUST** be [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible). (There is a way to bypass this requirement described later.) +* In function `from_json`, use function [`at()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a93403e803947b86f4da2d1fb3345cf2c.html#a93403e803947b86f4da2d1fb3345cf2c) to access the object values rather than `operator[]`. In case a key does not exist, `at` throws an exception that you can handle, whereas `operator[]` exhibits undefined behavior. +* You do not need to add serializers or deserializers for STL types like `std::vector`: the library already implements these. + + +#### How do I convert third-party types? + +This requires a bit more advanced technique. But first, let's see how this conversion mechanism works: + +The library uses **JSON Serializers** to convert types to json. +The default serializer for `nlohmann::json` is `nlohmann::adl_serializer` (ADL means [Argument-Dependent Lookup](https://en.cppreference.com/w/cpp/language/adl)). + +It is implemented like this (simplified): + +```cpp +template +struct adl_serializer { + static void to_json(json& j, const T& value) { + // calls the "to_json" method in T's namespace + } + + static void from_json(const json& j, T& value) { + // same thing, but with the "from_json" method + } +}; +``` + +This serializer works fine when you have control over the type's namespace. However, what about `boost::optional` or `std::filesystem::path` (C++17)? Hijacking the `boost` namespace is pretty bad, and it's illegal to add something other than template specializations to `std`... + +To solve this, you need to add a specialization of `adl_serializer` to the `nlohmann` namespace, here's an example: + +```cpp +// partial specialization (full specialization works too) +namespace nlohmann { + template + struct adl_serializer> { + static void to_json(json& j, const boost::optional& opt) { + if (opt == boost::none) { + j = nullptr; + } else { + j = *opt; // this will call adl_serializer::to_json which will + // find the free function to_json in T's namespace! + } + } + + static void from_json(const json& j, boost::optional& opt) { + if (j.is_null()) { + opt = boost::none; + } else { + opt = j.get(); // same as above, but with + // adl_serializer::from_json + } + } + }; +} +``` + +#### How can I use `get()` for non-default constructible/non-copyable types? + +There is a way, if your type is [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible). You will need to specialize the `adl_serializer` as well, but with a special `from_json` overload: + +```cpp +struct move_only_type { + move_only_type() = delete; + move_only_type(int ii): i(ii) {} + move_only_type(const move_only_type&) = delete; + move_only_type(move_only_type&&) = default; + + int i; +}; + +namespace nlohmann { + template <> + struct adl_serializer { + // note: the return type is no longer 'void', and the method only takes + // one argument + static move_only_type from_json(const json& j) { + return {j.get()}; + } + + // Here's the catch! You must provide a to_json method! Otherwise you + // will not be able to convert move_only_type to json, since you fully + // specialized adl_serializer on that type + static void to_json(json& j, move_only_type t) { + j = t.i; + } + }; +} +``` + +#### Can I write my own serializer? (Advanced use) + +Yes. You might want to take a look at [`unit-udt.cpp`](https://github.com/nlohmann/json/blob/develop/test/src/unit-udt.cpp) in the test suite, to see a few examples. + +If you write your own serializer, you'll need to do a few things: + +- use a different `basic_json` alias than `nlohmann::json` (the last template parameter of `basic_json` is the `JSONSerializer`) +- use your `basic_json` alias (or a template parameter) in all your `to_json`/`from_json` methods +- use `nlohmann::to_json` and `nlohmann::from_json` when you need ADL + +Here is an example, without simplifications, that only accepts types with a size <= 32, and uses ADL. + +```cpp +// You should use void as a second template argument +// if you don't need compile-time checks on T +template::type> +struct less_than_32_serializer { + template + static void to_json(BasicJsonType& j, T value) { + // we want to use ADL, and call the correct to_json overload + using nlohmann::to_json; // this method is called by adl_serializer, + // this is where the magic happens + to_json(j, value); + } + + template + static void from_json(const BasicJsonType& j, T& value) { + // same thing here + using nlohmann::from_json; + from_json(j, value); + } +}; +``` + +Be **very** careful when reimplementing your serializer, you can stack overflow if you don't pay attention: + +```cpp +template +struct bad_serializer +{ + template + static void to_json(BasicJsonType& j, const T& value) { + // this calls BasicJsonType::json_serializer::to_json(j, value); + // if BasicJsonType::json_serializer == bad_serializer ... oops! + j = value; + } + + template + static void to_json(const BasicJsonType& j, T& value) { + // this calls BasicJsonType::json_serializer::from_json(j, value); + // if BasicJsonType::json_serializer == bad_serializer ... oops! + value = j.template get(); // oops! + } +}; +``` + +### Specializing enum conversion + +By default, enum values are serialized to JSON as integers. In some cases this could result in undesired behavior. If an enum is modified or re-ordered after data has been serialized to JSON, the later de-serialized JSON data may be undefined or a different enum value than was originally intended. + +It is possible to more precisely specify how a given enum is mapped to and from JSON as shown below: + +```cpp +// example enum type declaration +enum TaskState { + TS_STOPPED, + TS_RUNNING, + TS_COMPLETED, + TS_INVALID=-1, +}; + +// map TaskState values to JSON as strings +NLOHMANN_JSON_SERIALIZE_ENUM( TaskState, { + {TS_INVALID, nullptr}, + {TS_STOPPED, "stopped"}, + {TS_RUNNING, "running"}, + {TS_COMPLETED, "completed"}, +}) +``` + +The `NLOHMANN_JSON_SERIALIZE_ENUM()` macro declares a set of `to_json()` / `from_json()` functions for type `TaskState` while avoiding repetition and boilerplate serialization code. + +**Usage:** + +```cpp +// enum to JSON as string +json j = TS_STOPPED; +assert(j == "stopped"); + +// json string to enum +json j3 = "running"; +assert(j3.get() == TS_RUNNING); + +// undefined json value to enum (where the first map entry above is the default) +json jPi = 3.14; +assert(jPi.get() == TS_INVALID ); +``` + +Just as in [Arbitrary Type Conversions](#arbitrary-types-conversions) above, +- `NLOHMANN_JSON_SERIALIZE_ENUM()` MUST be declared in your enum type's namespace (which can be the global namespace), or the library will not be able to locate it and it will default to integer serialization. +- It MUST be available (e.g., proper headers must be included) everywhere you use the conversions. + +Other Important points: +- When using `get()`, undefined JSON values will default to the first pair specified in your map. Select this default pair carefully. +- If an enum or JSON value is specified more than once in your map, the first matching occurrence from the top of the map will be returned when converting to or from JSON. + +### Binary formats (BSON, CBOR, MessagePack, and UBJSON) + +Though JSON is a ubiquitous data format, it is not a very compact format suitable for data exchange, for instance over a network. Hence, the library supports [BSON](http://bsonspec.org) (Binary JSON), [CBOR](http://cbor.io) (Concise Binary Object Representation), [MessagePack](http://msgpack.org), and [UBJSON](http://ubjson.org) (Universal Binary JSON Specification) to efficiently encode JSON values to byte vectors and to decode such vectors. + +```cpp +// create a JSON value +json j = R"({"compact": true, "schema": 0})"_json; + +// serialize to BSON +std::vector v_bson = json::to_bson(j); + +// 0x1B, 0x00, 0x00, 0x00, 0x08, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0x00, 0x01, 0x10, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + +// roundtrip +json j_from_bson = json::from_bson(v_bson); + +// serialize to CBOR +std::vector v_cbor = json::to_cbor(j); + +// 0xA2, 0x67, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0xF5, 0x66, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x00 + +// roundtrip +json j_from_cbor = json::from_cbor(v_cbor); + +// serialize to MessagePack +std::vector v_msgpack = json::to_msgpack(j); + +// 0x82, 0xA7, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0xC3, 0xA6, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x00 + +// roundtrip +json j_from_msgpack = json::from_msgpack(v_msgpack); + +// serialize to UBJSON +std::vector v_ubjson = json::to_ubjson(j); + +// 0x7B, 0x69, 0x07, 0x63, 0x6F, 0x6D, 0x70, 0x61, 0x63, 0x74, 0x54, 0x69, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6D, 0x61, 0x69, 0x00, 0x7D + +// roundtrip +json j_from_ubjson = json::from_ubjson(v_ubjson); +``` + + +## Supported compilers + +Though it's 2019 already, the support for C++11 is still a bit sparse. Currently, the following compilers are known to work: + +- GCC 4.8 - 9.0 (and possibly later) +- Clang 3.4 - 8.0 (and possibly later) +- Intel C++ Compiler 17.0.2 (and possibly later) +- Microsoft Visual C++ 2015 / Build Tools 14.0.25123.0 (and possibly later) +- Microsoft Visual C++ 2017 / Build Tools 15.5.180.51428 (and possibly later) + +I would be happy to learn about other compilers/versions. + +Please note: + +- GCC 4.8 has a bug [57824](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57824)): multiline raw strings cannot be the arguments to macros. Don't use multiline raw strings directly in macros with this compiler. +- Android defaults to using very old compilers and C++ libraries. To fix this, add the following to your `Application.mk`. This will switch to the LLVM C++ library, the Clang compiler, and enable C++11 and other features disabled by default. + + ``` + APP_STL := c++_shared + NDK_TOOLCHAIN_VERSION := clang3.6 + APP_CPPFLAGS += -frtti -fexceptions + ``` + + The code compiles successfully with [Android NDK](https://developer.android.com/ndk/index.html?hl=ml), Revision 9 - 11 (and possibly later) and [CrystaX's Android NDK](https://www.crystax.net/en/android/ndk) version 10. + +- For GCC running on MinGW or Android SDK, the error `'to_string' is not a member of 'std'` (or similarly, for `strtod`) may occur. Note this is not an issue with the code, but rather with the compiler itself. On Android, see above to build with a newer environment. For MinGW, please refer to [this site](http://tehsausage.com/mingw-to-string) and [this discussion](https://github.com/nlohmann/json/issues/136) for information on how to fix this bug. For Android NDK using `APP_STL := gnustl_static`, please refer to [this discussion](https://github.com/nlohmann/json/issues/219). + +- Unsupported versions of GCC and Clang are rejected by `#error` directives. This can be switched off by defining `JSON_SKIP_UNSUPPORTED_COMPILER_CHECK`. Note that you can expect no support in this case. + +The following compilers are currently used in continuous integration at [Travis](https://travis-ci.org/nlohmann/json), [AppVeyor](https://ci.appveyor.com/project/nlohmann/json), and [Doozer](https://doozer.io): + +| Compiler | Operating System | Version String | +|-----------------------|------------------------------|----------------| +| GCC 4.8.5 | Ubuntu 14.04.5 LTS | g++-4.8 (Ubuntu 4.8.5-2ubuntu1~14.04.2) 4.8.5 | +| GCC 4.8.5 | CentOS Release-7-6.1810.2.el7.centos.x86_64 | g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36) | +| GCC 4.9.2 (armv7l) | Raspbian GNU/Linux 8 (jessie) | g++ (Raspbian 4.9.2-10+deb8u2) 4.9.2 | +| GCC 4.9.4 | Ubuntu 14.04.1 LTS | g++-4.9 (Ubuntu 4.9.4-2ubuntu1~14.04.1) 4.9.4 | +| GCC 5.3.1 (armv7l) | Ubuntu 16.04 LTS | g++ (Ubuntu/Linaro 5.3.1-14ubuntu2) 5.3.1 20160413 | +| GCC 5.5.0 | Ubuntu 14.04.1 LTS | g++-5 (Ubuntu 5.5.0-12ubuntu1~14.04) 5.5.0 20171010 | +| GCC 6.3.1 | Fedora release 24 (Twenty Four) | g++ (GCC) 6.3.1 20161221 (Red Hat 6.3.1-1) | +| GCC 6.4.0 | Ubuntu 14.04.1 LTS | g++-6 (Ubuntu 6.4.0-17ubuntu1~14.04) 6.4.0 20180424 | +| GCC 7.3.0 | Ubuntu 14.04.1 LTS | g++-7 (Ubuntu 7.3.0-21ubuntu1~14.04) 7.3.0 | +| GCC 7.3.0 | Windows Server 2012 R2 (x64) | g++ (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 7.3.0 | +| GCC 8.1.0 | Ubuntu 14.04.1 LTS | g++-8 (Ubuntu 8.1.0-5ubuntu1~14.04) 8.1.0 | +| Clang 3.5.0 | Ubuntu 14.04.1 LTS | clang version 3.5.0-4ubuntu2~trusty2 (tags/RELEASE_350/final) (based on LLVM 3.5.0) | +| Clang 3.6.2 | Ubuntu 14.04.1 LTS | clang version 3.6.2-svn240577-1~exp1 (branches/release_36) (based on LLVM 3.6.2) | +| Clang 3.7.1 | Ubuntu 14.04.1 LTS | clang version 3.7.1-svn253571-1~exp1 (branches/release_37) (based on LLVM 3.7.1) | +| Clang 3.8.0 | Ubuntu 14.04.1 LTS | clang version 3.8.0-2ubuntu3~trusty5 (tags/RELEASE_380/final) | +| Clang 3.9.1 | Ubuntu 14.04.1 LTS | clang version 3.9.1-4ubuntu3~14.04.3 (tags/RELEASE_391/rc2) | +| Clang 4.0.1 | Ubuntu 14.04.1 LTS | clang version 4.0.1-svn305264-1~exp1 (branches/release_40) | +| Clang 5.0.2 | Ubuntu 14.04.1 LTS | clang version 5.0.2-svn328729-1~exp1~20180509123505.100 (branches/release_50) | +| Clang 6.0.1 | Ubuntu 14.04.1 LTS | clang version 6.0.1-svn334776-1~exp1~20180726133705.85 (branches/release_60) | +| Clang 7.0.1 | Ubuntu 14.04.1 LTS | clang version 7.0.1-svn348686-1~exp1~20181213084532.54 (branches/release_70) | +| Clang Xcode 8.3 | OSX 10.11.6 | Apple LLVM version 8.1.0 (clang-802.0.38) | +| Clang Xcode 9.0 | OSX 10.12.6 | Apple LLVM version 9.0.0 (clang-900.0.37) | +| Clang Xcode 9.1 | OSX 10.12.6 | Apple LLVM version 9.0.0 (clang-900.0.38) | +| Clang Xcode 9.2 | OSX 10.13.3 | Apple LLVM version 9.1.0 (clang-902.0.39.1) | +| Clang Xcode 9.3 | OSX 10.13.3 | Apple LLVM version 9.1.0 (clang-902.0.39.2) | +| Clang Xcode 10.0 | OSX 10.13.3 | Apple LLVM version 10.0.0 (clang-1000.11.45.2) | +| Clang Xcode 10.1 | OSX 10.13.3 | Apple LLVM version 10.0.0 (clang-1000.11.45.5) | +| Visual Studio 14 2015 | Windows Server 2012 R2 (x64) | Microsoft (R) Build Engine version 14.0.25420.1, MSVC 19.0.24215.1 | +| Visual Studio 2017 | Windows Server 2016 | Microsoft (R) Build Engine version 15.7.180.61344, MSVC 19.14.26433.0 | + +## License + + + +The class is licensed under the [MIT License](http://opensource.org/licenses/MIT): + +Copyright © 2013-2019 [Niels Lohmann](http://nlohmann.me) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Softwareâ€), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS ISâ€, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +* * * + +The class contains the UTF-8 Decoder from Bjoern Hoehrmann which is licensed under the [MIT License](http://opensource.org/licenses/MIT) (see above). Copyright © 2008-2009 [Björn Hoehrmann](http://bjoern.hoehrmann.de/) + +The class contains a slightly modified version of the Grisu2 algorithm from Florian Loitsch which is licensed under the [MIT License](http://opensource.org/licenses/MIT) (see above). Copyright © 2009 [Florian Loitsch](http://florian.loitsch.com/) + +The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Evan Nemerson which is licensed as [CC0-1.0](http://creativecommons.org/publicdomain/zero/1.0/). + +## Contact + +If you have questions regarding the library, I would like to invite you to [open an issue at GitHub](https://github.com/nlohmann/json/issues/new/choose). Please describe your request, problem, or question as detailed as possible, and also mention the version of the library you are using as well as the version of your compiler and operating system. Opening an issue at GitHub allows other users and contributors to this library to collaborate. For instance, I have little experience with MSVC, and most issues in this regard have been solved by a growing community. If you have a look at the [closed issues](https://github.com/nlohmann/json/issues?q=is%3Aissue+is%3Aclosed), you will see that we react quite timely in most cases. + +Only if your request would contain confidential information, please [send me an email](mailto:mail@nlohmann.me). For encrypted messages, please use [this key](https://keybase.io/nlohmann/pgp_keys.asc). + +## Security + +[Commits by Niels Lohmann](https://github.com/nlohmann/json/commits) and [releases](https://github.com/nlohmann/json/releases) are signed with this [PGP Key](https://keybase.io/nlohmann/pgp_keys.asc?fingerprint=797167ae41c0a6d9232e48457f3cea63ae251b69). + +## Thanks + +I deeply appreciate the help of the following people. + + + +- [Teemperor](https://github.com/Teemperor) implemented CMake support and lcov integration, realized escape and Unicode handling in the string parser, and fixed the JSON serialization. +- [elliotgoodrich](https://github.com/elliotgoodrich) fixed an issue with double deletion in the iterator classes. +- [kirkshoop](https://github.com/kirkshoop) made the iterators of the class composable to other libraries. +- [wancw](https://github.com/wanwc) fixed a bug that hindered the class to compile with Clang. +- Tomas Ã…blad found a bug in the iterator implementation. +- [Joshua C. Randall](https://github.com/jrandall) fixed a bug in the floating-point serialization. +- [Aaron Burghardt](https://github.com/aburgh) implemented code to parse streams incrementally. Furthermore, he greatly improved the parser class by allowing the definition of a filter function to discard undesired elements while parsing. +- [Daniel KopeÄek](https://github.com/dkopecek) fixed a bug in the compilation with GCC 5.0. +- [Florian Weber](https://github.com/Florianjw) fixed a bug in and improved the performance of the comparison operators. +- [Eric Cornelius](https://github.com/EricMCornelius) pointed out a bug in the handling with NaN and infinity values. He also improved the performance of the string escaping. +- [易æ€é¾™](https://github.com/likebeta) implemented a conversion from anonymous enums. +- [kepkin](https://github.com/kepkin) patiently pushed forward the support for Microsoft Visual studio. +- [gregmarr](https://github.com/gregmarr) simplified the implementation of reverse iterators and helped with numerous hints and improvements. In particular, he pushed forward the implementation of user-defined types. +- [Caio Luppi](https://github.com/caiovlp) fixed a bug in the Unicode handling. +- [dariomt](https://github.com/dariomt) fixed some typos in the examples. +- [Daniel Frey](https://github.com/d-frey) cleaned up some pointers and implemented exception-safe memory allocation. +- [Colin Hirsch](https://github.com/ColinH) took care of a small namespace issue. +- [Huu Nguyen](https://github.com/whoshuu) correct a variable name in the documentation. +- [Silverweed](https://github.com/silverweed) overloaded `parse()` to accept an rvalue reference. +- [dariomt](https://github.com/dariomt) fixed a subtlety in MSVC type support and implemented the `get_ref()` function to get a reference to stored values. +- [ZahlGraf](https://github.com/ZahlGraf) added a workaround that allows compilation using Android NDK. +- [whackashoe](https://github.com/whackashoe) replaced a function that was marked as unsafe by Visual Studio. +- [406345](https://github.com/406345) fixed two small warnings. +- [Glen Fernandes](https://github.com/glenfe) noted a potential portability problem in the `has_mapped_type` function. +- [Corbin Hughes](https://github.com/nibroc) fixed some typos in the contribution guidelines. +- [twelsby](https://github.com/twelsby) fixed the array subscript operator, an issue that failed the MSVC build, and floating-point parsing/dumping. He further added support for unsigned integer numbers and implemented better roundtrip support for parsed numbers. +- [Volker Diels-Grabsch](https://github.com/vog) fixed a link in the README file. +- [msm-](https://github.com/msm-) added support for American Fuzzy Lop. +- [Annihil](https://github.com/Annihil) fixed an example in the README file. +- [Themercee](https://github.com/Themercee) noted a wrong URL in the README file. +- [Lv Zheng](https://github.com/lv-zheng) fixed a namespace issue with `int64_t` and `uint64_t`. +- [abc100m](https://github.com/abc100m) analyzed the issues with GCC 4.8 and proposed a [partial solution](https://github.com/nlohmann/json/pull/212). +- [zewt](https://github.com/zewt) added useful notes to the README file about Android. +- [Róbert Márki](https://github.com/robertmrk) added a fix to use move iterators and improved the integration via CMake. +- [Chris Kitching](https://github.com/ChrisKitching) cleaned up the CMake files. +- [Tom Needham](https://github.com/06needhamt) fixed a subtle bug with MSVC 2015 which was also proposed by [Michael K.](https://github.com/Epidal). +- [Mário Feroldi](https://github.com/thelostt) fixed a small typo. +- [duncanwerner](https://github.com/duncanwerner) found a really embarrassing performance regression in the 2.0.0 release. +- [Damien](https://github.com/dtoma) fixed one of the last conversion warnings. +- [Thomas Braun](https://github.com/t-b) fixed a warning in a test case. +- [Théo DELRIEU](https://github.com/theodelrieu) patiently and constructively oversaw the long way toward [iterator-range parsing](https://github.com/nlohmann/json/issues/290). He also implemented the magic behind the serialization/deserialization of user-defined types and split the single header file into smaller chunks. +- [Stefan](https://github.com/5tefan) fixed a minor issue in the documentation. +- [Vasil Dimov](https://github.com/vasild) fixed the documentation regarding conversions from `std::multiset`. +- [ChristophJud](https://github.com/ChristophJud) overworked the CMake files to ease project inclusion. +- [Vladimir Petrigo](https://github.com/vpetrigo) made a SFINAE hack more readable and added Visual Studio 17 to the build matrix. +- [Denis Andrejew](https://github.com/seeekr) fixed a grammar issue in the README file. +- [Pierre-Antoine Lacaze](https://github.com/palacaze) found a subtle bug in the `dump()` function. +- [TurpentineDistillery](https://github.com/TurpentineDistillery) pointed to [`std::locale::classic()`](https://en.cppreference.com/w/cpp/locale/locale/classic) to avoid too much locale joggling, found some nice performance improvements in the parser, improved the benchmarking code, and realized locale-independent number parsing and printing. +- [cgzones](https://github.com/cgzones) had an idea how to fix the Coverity scan. +- [Jared Grubb](https://github.com/jaredgrubb) silenced a nasty documentation warning. +- [Yixin Zhang](https://github.com/qwename) fixed an integer overflow check. +- [Bosswestfalen](https://github.com/Bosswestfalen) merged two iterator classes into a smaller one. +- [Daniel599](https://github.com/Daniel599) helped to get Travis execute the tests with Clang's sanitizers. +- [Jonathan Lee](https://github.com/vjon) fixed an example in the README file. +- [gnzlbg](https://github.com/gnzlbg) supported the implementation of user-defined types. +- [Alexej Harm](https://github.com/qis) helped to get the user-defined types working with Visual Studio. +- [Jared Grubb](https://github.com/jaredgrubb) supported the implementation of user-defined types. +- [EnricoBilla](https://github.com/EnricoBilla) noted a typo in an example. +- [Martin HoÅ™eňovský](https://github.com/horenmar) found a way for a 2x speedup for the compilation time of the test suite. +- [ukhegg](https://github.com/ukhegg) found proposed an improvement for the examples section. +- [rswanson-ihi](https://github.com/rswanson-ihi) noted a typo in the README. +- [Mihai Stan](https://github.com/stanmihai4) fixed a bug in the comparison with `nullptr`s. +- [Tushar Maheshwari](https://github.com/tusharpm) added [cotire](https://github.com/sakra/cotire) support to speed up the compilation. +- [TedLyngmo](https://github.com/TedLyngmo) noted a typo in the README, removed unnecessary bit arithmetic, and fixed some `-Weffc++` warnings. +- [Krzysztof WoÅ›](https://github.com/krzysztofwos) made exceptions more visible. +- [ftillier](https://github.com/ftillier) fixed a compiler warning. +- [tinloaf](https://github.com/tinloaf) made sure all pushed warnings are properly popped. +- [Fytch](https://github.com/Fytch) found a bug in the documentation. +- [Jay Sistar](https://github.com/Type1J) implemented a Meson build description. +- [Henry Lee](https://github.com/HenryRLee) fixed a warning in ICC and improved the iterator implementation. +- [Vincent Thiery](https://github.com/vthiery) maintains a package for the Conan package manager. +- [Steffen](https://github.com/koemeet) fixed a potential issue with MSVC and `std::min`. +- [Mike Tzou](https://github.com/Chocobo1) fixed some typos. +- [amrcode](https://github.com/amrcode) noted a misleading documentation about comparison of floats. +- [Oleg Endo](https://github.com/olegendo) reduced the memory consumption by replacing `` with ``. +- [dan-42](https://github.com/dan-42) cleaned up the CMake files to simplify including/reusing of the library. +- [Nikita Ofitserov](https://github.com/himikof) allowed for moving values from initializer lists. +- [Greg Hurrell](https://github.com/wincent) fixed a typo. +- [Dmitry Kukovinets](https://github.com/DmitryKuk) fixed a typo. +- [kbthomp1](https://github.com/kbthomp1) fixed an issue related to the Intel OSX compiler. +- [Markus Werle](https://github.com/daixtrose) fixed a typo. +- [WebProdPP](https://github.com/WebProdPP) fixed a subtle error in a precondition check. +- [Alex](https://github.com/leha-bot) noted an error in a code sample. +- [Tom de Geus](https://github.com/tdegeus) reported some warnings with ICC and helped fixing them. +- [Perry Kundert](https://github.com/pjkundert) simplified reading from input streams. +- [Sonu Lohani](https://github.com/sonulohani) fixed a small compilation error. +- [Jamie Seward](https://github.com/jseward) fixed all MSVC warnings. +- [Nate Vargas](https://github.com/eld00d) added a Doxygen tag file. +- [pvleuven](https://github.com/pvleuven) helped fixing a warning in ICC. +- [Pavel](https://github.com/crea7or) helped fixing some warnings in MSVC. +- [Jamie Seward](https://github.com/jseward) avoided unnecessary string copies in `find()` and `count()`. +- [Mitja](https://github.com/Itja) fixed some typos. +- [Jorrit Wronski](https://github.com/jowr) updated the Hunter package links. +- [Matthias Möller](https://github.com/TinyTinni) added a `.natvis` for the MSVC debug view. +- [bogemic](https://github.com/bogemic) fixed some C++17 deprecation warnings. +- [Eren Okka](https://github.com/erengy) fixed some MSVC warnings. +- [abolz](https://github.com/abolz) integrated the Grisu2 algorithm for proper floating-point formatting, allowing more roundtrip checks to succeed. +- [Vadim Evard](https://github.com/Pipeliner) fixed a Markdown issue in the README. +- [zerodefect](https://github.com/zerodefect) fixed a compiler warning. +- [Kert](https://github.com/kaidokert) allowed to template the string type in the serialization and added the possibility to override the exceptional behavior. +- [mark-99](https://github.com/mark-99) helped fixing an ICC error. +- [Patrik Huber](https://github.com/patrikhuber) fixed links in the README file. +- [johnfb](https://github.com/johnfb) found a bug in the implementation of CBOR's indefinite length strings. +- [Paul Fultz II](https://github.com/pfultz2) added a note on the cget package manager. +- [Wilson Lin](https://github.com/wla80) made the integration section of the README more concise. +- [RalfBielig](https://github.com/ralfbielig) detected and fixed a memory leak in the parser callback. +- [agrianius](https://github.com/agrianius) allowed to dump JSON to an alternative string type. +- [Kevin Tonon](https://github.com/ktonon) overworked the C++11 compiler checks in CMake. +- [Axel Huebl](https://github.com/ax3l) simplified a CMake check and added support for the [Spack package manager](https://spack.io). +- [Carlos O'Ryan](https://github.com/coryan) fixed a typo. +- [James Upjohn](https://github.com/jammehcow) fixed a version number in the compilers section. +- [Chuck Atkins](https://github.com/chuckatkins) adjusted the CMake files to the CMake packaging guidelines and provided documentation for the CMake integration. +- [Jan Schöppach](https://github.com/dns13) fixed a typo. +- [martin-mfg](https://github.com/martin-mfg) fixed a typo. +- [Matthias Möller](https://github.com/TinyTinni) removed the dependency from `std::stringstream`. +- [agrianius](https://github.com/agrianius) added code to use alternative string implementations. +- [Daniel599](https://github.com/Daniel599) allowed to use more algorithms with the `items()` function. +- [Julius Rakow](https://github.com/jrakow) fixed the Meson include directory and fixed the links to [cppreference.com](cppreference.com). +- [Sonu Lohani](https://github.com/sonulohani) fixed the compilation with MSVC 2015 in debug mode. +- [grembo](https://github.com/grembo) fixed the test suite and re-enabled several test cases. +- [Hyeon Kim](https://github.com/simnalamburt) introduced the macro `JSON_INTERNAL_CATCH` to control the exception handling inside the library. +- [thyu](https://github.com/thyu) fixed a compiler warning. +- [David Guthrie](https://github.com/LEgregius) fixed a subtle compilation error with Clang 3.4.2. +- [Dennis Fischer](https://github.com/dennisfischer) allowed to call `find_package` without installing the library. +- [Hyeon Kim](https://github.com/simnalamburt) fixed an issue with a double macro definition. +- [Ben Berman](https://github.com/rivertam) made some error messages more understandable. +- [zakalibit](https://github.com/zakalibit) fixed a compilation problem with the Intel C++ compiler. +- [mandreyel](https://github.com/mandreyel) fixed a compilation problem. +- [Kostiantyn Ponomarenko](https://github.com/koponomarenko) added version and license information to the Meson build file. +- [Henry Schreiner](https://github.com/henryiii) added support for GCC 4.8. +- [knilch](https://github.com/knilch0r) made sure the test suite does not stall when run in the wrong directory. +- [Antonio Borondo](https://github.com/antonioborondo) fixed an MSVC 2017 warning. +- [Dan Gendreau](https://github.com/dgendreau) implemented the `NLOHMANN_JSON_SERIALIZE_ENUM` macro to quickly define a enum/JSON mapping. +- [efp](https://github.com/efp) added line and column information to parse errors. +- [julian-becker](https://github.com/julian-becker) added BSON support. +- [Pratik Chowdhury](https://github.com/pratikpc) added support for structured bindings. +- [David Avedissian](https://github.com/davedissian) added support for Clang 5.0.1 (PS4 version). +- [Jonathan Dumaresq](https://github.com/dumarjo) implemented an input adapter to read from `FILE*`. +- [kjpus](https://github.com/kjpus) fixed a link in the documentation. +- [Manvendra Singh](https://github.com/manu-chroma) fixed a typo in the documentation. +- [ziggurat29](https://github.com/ziggurat29) fixed an MSVC warning. +- [Sylvain Corlay](https://github.com/SylvainCorlay) added code to avoid an issue with MSVC. +- [mefyl](https://github.com/mefyl) fixed a bug when JSON was parsed from an input stream. +- [Millian Poquet](https://github.com/mpoquet) allowed to install the library via Meson. +- [Michael Behrns-Miller](https://github.com/moodboom) found an issue with a missing namespace. +- [Nasztanovics Ferenc](https://github.com/naszta) fixed a compilation issue with libc 2.12. +- [Andreas Schwab](https://github.com/andreas-schwab) fixed the endian conversion. +- [Mark-Dunning](https://github.com/Mark-Dunning) fixed a warning in MSVC. +- [Gareth Sylvester-Bradley](https://github.com/garethsb-sony) added `operator/` for JSON Pointers. +- [John-Mark](https://github.com/johnmarkwayve) noted a missing header. +- [Vitaly Zaitsev](https://github.com/xvitaly) fixed compilation with GCC 9.0. +- [Laurent Stacul](https://github.com/stac47) fixed compilation with GCC 9.0. +- [Ivor Wanders](https://github.com/iwanders) helped reducing the CMake requirement to version 3.1. +- [njlr](https://github.com/njlr) updated the Buckaroo instructions. +- [Lion](https://github.com/lieff) fixed a compilation issue with GCC 7 on CentOS. +- [Isaac Nickaein](https://github.com/nickaein) improved the integer serialization performance and implemented the `contains()` function. +- [past-due](https://github.com/past-due) suppressed an unfixable warning. +- [Elvis Oric](https://github.com/elvisoric) improved Meson support. +- [MatÄ›j Plch](https://github.com/Afforix) fixed an example in the README. +- [Mark Beckwith](https://github.com/wythe) fixed a typo. +- [scinart](https://github.com/scinart) fixed bug in the serializer. +- [Patrick Boettcher](https://github.com/pboettch) implemented `push_back()` and `pop_back()` for JSON Pointers. +- [Bruno Oliveira](https://github.com/nicoddemus) added support for Conda. +- [Michele Caini](https://github.com/skypjack) fixed links in the README. +- [Hani](https://github.com/hnkb) documented how to install the library with NuGet. +- [Mark Beckwith](https://github.com/wythe) fixed a typo. +- [yann-morin-1998](https://github.com/yann-morin-1998) helped reducing the CMake requirement to version 3.1. +- [Konstantin Podsvirov](https://github.com/podsvirov) maintains a package for the MSYS2 software distro. +- [remyabel](https://github.com/remyabel) added GNUInstallDirs to the CMake files. +- [Taylor Howard](https://github.com/taylorhoward92) fixed a unit test. +- [Gabe Ron](https://github.com/Macr0Nerd) implemented the `to_string` method. +- [Watal M. Iwasaki](https://github.com/heavywatal) fixed a Clang warning. +- [Viktor Kirilov](https://github.com/onqtam) switched the unit tests from [Catch](https://github.com/philsquared/Catch) to [doctest](https://github.com/onqtam/doctest) + +Thanks a lot for helping out! Please [let me know](mailto:mail@nlohmann.me) if I forgot someone. + + +## Used third-party tools + +The library itself consists of a single header file licensed under the MIT license. However, it is built, tested, documented, and whatnot using a lot of third-party tools and services. Thanks a lot! + +- [**amalgamate.py - Amalgamate C source and header files**](https://github.com/edlund/amalgamate) to create a single header file +- [**American fuzzy lop**](http://lcamtuf.coredump.cx/afl/) for fuzz testing +- [**AppVeyor**](https://www.appveyor.com) for [continuous integration](https://ci.appveyor.com/project/nlohmann/json) on Windows +- [**Artistic Style**](http://astyle.sourceforge.net) for automatic source code indentation +- [**CircleCI**](http://circleci.com) for [continuous integration](https://circleci.com/gh/nlohmann/json). +- [**Clang**](http://clang.llvm.org) for compilation with code sanitizers +- [**CMake**](https://cmake.org) for build automation +- [**Codacity**](https://www.codacy.com) for further [code analysis](https://www.codacy.com/app/nlohmann/json) +- [**Coveralls**](https://coveralls.io) to measure [code coverage](https://coveralls.io/github/nlohmann/json) +- [**Coverity Scan**](https://scan.coverity.com) for [static analysis](https://scan.coverity.com/projects/nlohmann-json) +- [**cppcheck**](http://cppcheck.sourceforge.net) for static analysis +- [**doctest**](https://github.com/onqtam/doctest) for the unit tests +- [**Doozer**](https://doozer.io) for [continuous integration](https://doozer.io/nlohmann/json) on Linux (CentOS, Raspbian, Fedora) +- [**Doxygen**](http://www.stack.nl/~dimitri/doxygen/) to generate [documentation](https://nlohmann.github.io/json/) +- [**fastcov**](https://github.com/RPGillespie6/fastcov) to process coverage information +- [**git-update-ghpages**](https://github.com/rstacruz/git-update-ghpages) to upload the documentation to gh-pages +- [**GitHub Changelog Generator**](https://github.com/skywinder/github-changelog-generator) to generate the [ChangeLog](https://github.com/nlohmann/json/blob/develop/ChangeLog.md) +- [**Google Benchmark**](https://github.com/google/benchmark) to implement the benchmarks +- [**Hedley**](https://nemequ.github.io/hedley/) to avoid re-inventing several compiler-agnostic feature macros +- [**lcov**](http://ltp.sourceforge.net/coverage/lcov.php) to process coverage information and create a HTML view +- [**libFuzzer**](http://llvm.org/docs/LibFuzzer.html) to implement fuzz testing for OSS-Fuzz +- [**OSS-Fuzz**](https://github.com/google/oss-fuzz) for continuous fuzz testing of the library ([project repository](https://github.com/google/oss-fuzz/tree/master/projects/json)) +- [**Probot**](https://probot.github.io) for automating maintainer tasks such as closing stale issues, requesting missing information, or detecting toxic comments. +- [**send_to_wandbox**](https://github.com/nlohmann/json/blob/develop/doc/scripts/send_to_wandbox.py) to send code examples to [Wandbox](http://melpon.org/wandbox) +- [**Travis**](https://travis-ci.org) for [continuous integration](https://travis-ci.org/nlohmann/json) on Linux and macOS +- [**Valgrind**](http://valgrind.org) to check for correct memory management +- [**Wandbox**](http://melpon.org/wandbox) for [online examples](https://wandbox.org/permlink/TarF5pPn9NtHQjhf) + + +## Projects using JSON for Modern C++ + +The library is currently used in Apple macOS Sierra and iOS 10. I am not sure what they are using the library for, but I am happy that it runs on so many devices. + + +## Notes + +### Character encoding + +The library supports **Unicode input** as follows: + +- Only **UTF-8** encoded input is supported which is the default encoding for JSON according to [RFC 8259](https://tools.ietf.org/html/rfc8259.html#section-8.1). +- `std::u16string` and `std::u32string` can be parsed, assuming UTF-16 and UTF-32 encoding, respectively. These encodings are not supported when reading from files or other input containers. +- Other encodings such as Latin-1 or ISO 8859-1 are **not** supported and will yield parse or serialization errors. +- [Unicode noncharacters](http://www.unicode.org/faq/private_use.html#nonchar1) will not be replaced by the library. +- Invalid surrogates (e.g., incomplete pairs such as `\uDEAD`) will yield parse errors. +- The strings stored in the library are UTF-8 encoded. When using the default string type (`std::string`), note that its length/size functions return the number of stored bytes rather than the number of characters or glyphs. +- When you store strings with different encodings in the library, calling [`dump()`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a50ec80b02d0f3f51130d4abb5d1cfdc5.html#a50ec80b02d0f3f51130d4abb5d1cfdc5) may throw an exception unless `json::error_handler_t::replace` or `json::error_handler_t::ignore` are used as error handlers. + +### Comments in JSON + +This library does not support comments. It does so for three reasons: + +1. Comments are not part of the [JSON specification](https://tools.ietf.org/html/rfc8259). You may argue that `//` or `/* */` are allowed in JavaScript, but JSON is not JavaScript. +2. This was not an oversight: Douglas Crockford [wrote on this](https://plus.google.com/118095276221607585885/posts/RK8qyGVaGSr) in May 2012: + + > I removed comments from JSON because I saw people were using them to hold parsing directives, a practice which would have destroyed interoperability. I know that the lack of comments makes some people sad, but it shouldn't. + + > Suppose you are using JSON to keep configuration files, which you would like to annotate. Go ahead and insert all the comments you like. Then pipe it through JSMin before handing it to your JSON parser. + +3. It is dangerous for interoperability if some libraries would add comment support while others don't. Please check [The Harmful Consequences of the Robustness Principle](https://tools.ietf.org/html/draft-iab-protocol-maintenance-01) on this. + +This library will not support comments in the future. If you wish to use comments, I see three options: + +1. Strip comments before using this library. +2. Use a different JSON library with comment support. +3. Use a format that natively supports comments (e.g., YAML or JSON5). + +### Order of object keys + +By default, the library does not preserve the **insertion order of object elements**. This is standards-compliant, as the [JSON standard](https://tools.ietf.org/html/rfc8259.html) defines objects as "an unordered collection of zero or more name/value pairs". If you do want to preserve the insertion order, you can specialize the object type with containers like [`tsl::ordered_map`](https://github.com/Tessil/ordered-map) ([integration](https://github.com/nlohmann/json/issues/546#issuecomment-304447518)) or [`nlohmann::fifo_map`](https://github.com/nlohmann/fifo_map) ([integration](https://github.com/nlohmann/json/issues/485#issuecomment-333652309)). + +### Further notes + +- The code contains numerous debug **assertions** which can be switched off by defining the preprocessor macro `NDEBUG`, see the [documentation of `assert`](https://en.cppreference.com/w/cpp/error/assert). In particular, note [`operator[]`](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a233b02b0839ef798942dd46157cc0fe6.html#a233b02b0839ef798942dd46157cc0fe6) implements **unchecked access** for const objects: If the given key is not present, the behavior is undefined (think of a dereferenced null pointer) and yields an [assertion failure](https://github.com/nlohmann/json/issues/289) if assertions are switched on. If you are not sure whether an element in an object exists, use checked access with the [`at()` function](https://nlohmann.github.io/json/classnlohmann_1_1basic__json_a73ae333487310e3302135189ce8ff5d8.html#a73ae333487310e3302135189ce8ff5d8). +- As the exact type of a number is not defined in the [JSON specification](https://tools.ietf.org/html/rfc8259.html), this library tries to choose the best fitting C++ number type automatically. As a result, the type `double` may be used to store numbers which may yield [**floating-point exceptions**](https://github.com/nlohmann/json/issues/181) in certain rare situations if floating-point exceptions have been unmasked in the calling code. These exceptions are not caused by the library and need to be fixed in the calling code, such as by re-masking the exceptions prior to calling library functions. +- The code can be compiled without C++ **runtime type identification** features; that is, you can use the `-fno-rtti` compiler flag. +- **Exceptions** are used widely within the library. They can, however, be switched off with either using the compiler flag `-fno-exceptions` or by defining the symbol `JSON_NOEXCEPTION`. In this case, exceptions are replaced by `abort()` calls. You can further control this behavior by defining `JSON_THROW_USER´` (overriding `throw`), `JSON_TRY_USER` (overriding `try`), and `JSON_CATCH_USER` (overriding `catch`). Note that `JSON_THROW_USER` should leave the current scope (e.g., by throwing or aborting), as continuing after it may yield undefined behavior. + +## Execute unit tests + +To compile and run the tests, you need to execute + +```sh +$ mkdir build +$ cd build +$ cmake .. +$ cmake --build . +$ ctest --output-on-failure +``` + +For more information, have a look at the file [.travis.yml](https://github.com/nlohmann/json/blob/master/.travis.yml). diff --git a/libraries/cppdap/third_party/json/appveyor.yml b/libraries/cppdap/third_party/json/appveyor.yml new file mode 100644 index 000000000..0a92a6c9a --- /dev/null +++ b/libraries/cppdap/third_party/json/appveyor.yml @@ -0,0 +1,111 @@ +version: '{build}' + +environment: + matrix: + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + configuration: Debug + platform: x86 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 14 2015 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Debug + platform: x86 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 15 2017 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + configuration: Debug + COMPILER: mingw + platform: x86 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Ninja + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + configuration: Release + COMPILER: mingw + platform: x86 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Ninja + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + configuration: Release + platform: x86 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 14 2015 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + configuration: Release + platform: x86 + name: with_win_header + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 14 2015 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Release + platform: x86 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 15 2017 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Release + platform: x86 + CXX_FLAGS: "/permissive- /std:c++latest /utf-8" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 15 2017 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + configuration: Release + platform: x64 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 14 2015 Win64 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Release + platform: x64 + CXX_FLAGS: "" + LINKER_FLAGS: "" + GENERATOR: Visual Studio 15 2017 Win64 + + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + configuration: Release + platform: x64 + CXX_FLAGS: "/permissive- /std:c++latest /utf-8 /F4000000" + LINKER_FLAGS: "/STACK:4000000" + GENERATOR: Visual Studio 15 2017 Win64 + +init: + - cmake --version + - msbuild /version + +install: + - if "%COMPILER%"=="mingw" appveyor DownloadFile https://github.com/ninja-build/ninja/releases/download/v1.6.0/ninja-win.zip -FileName ninja.zip + - if "%COMPILER%"=="mingw" 7z x ninja.zip -oC:\projects\deps\ninja > nul + - if "%COMPILER%"=="mingw" set PATH=C:\projects\deps\ninja;%PATH% + - if "%COMPILER%"=="mingw" set PATH=C:\mingw-w64\x86_64-7.3.0-posix-seh-rt_v5-rev0\mingw64\bin;%PATH% + - if "%COMPILER%"=="mingw" g++ --version + +before_build: + # for with_win_header build, inject the inclusion of Windows.h to the single-header library + - ps: if ($env:name -Eq "with_win_header") { $header_path = "single_include\nlohmann\json.hpp" } + - ps: if ($env:name -Eq "with_win_header") { "#include `n" + (Get-Content $header_path | Out-String) | Set-Content $header_path } + - cmake . -G "%GENERATOR%" -DCMAKE_BUILD_TYPE="%configuration%" -DCMAKE_CXX_FLAGS="%CXX_FLAGS%" -DCMAKE_EXE_LINKER_FLAGS="%LINKER_FLAGS%" -DCMAKE_IGNORE_PATH="C:/Program Files/Git/usr/bin" + +build_script: + - cmake --build . --config "%configuration%" + +test_script: + - if "%configuration%"=="Release" ctest -C "%configuration%" -V -j + # On Debug builds, skip test-unicode_all + # as it is extremely slow to run and cause + # occasional timeouts on AppVeyor. + # More info: https://github.com/nlohmann/json/pull/1570 + - if "%configuration%"=="Debug" ctest --exclude-regex "test-unicode_all" -C "%configuration%" -V -j diff --git a/libraries/cppdap/third_party/json/cmake/config.cmake.in b/libraries/cppdap/third_party/json/cmake/config.cmake.in new file mode 100644 index 000000000..9a17a7d7b --- /dev/null +++ b/libraries/cppdap/third_party/json/cmake/config.cmake.in @@ -0,0 +1,15 @@ +include(FindPackageHandleStandardArgs) +set(${CMAKE_FIND_PACKAGE_NAME}_CONFIG ${CMAKE_CURRENT_LIST_FILE}) +find_package_handle_standard_args(@PROJECT_NAME@ CONFIG_MODE) + +if(NOT TARGET @PROJECT_NAME@::@NLOHMANN_JSON_TARGET_NAME@) + include("${CMAKE_CURRENT_LIST_DIR}/@NLOHMANN_JSON_TARGETS_EXPORT_NAME@.cmake") + if((NOT TARGET @NLOHMANN_JSON_TARGET_NAME@) AND + (NOT @PROJECT_NAME@_FIND_VERSION OR + @PROJECT_NAME@_FIND_VERSION VERSION_LESS 3.2.0)) + add_library(@NLOHMANN_JSON_TARGET_NAME@ INTERFACE IMPORTED) + set_target_properties(@NLOHMANN_JSON_TARGET_NAME@ PROPERTIES + INTERFACE_LINK_LIBRARIES @PROJECT_NAME@::@NLOHMANN_JSON_TARGET_NAME@ + ) + endif() +endif() diff --git a/libraries/cppdap/third_party/json/include/nlohmann/adl_serializer.hpp b/libraries/cppdap/third_party/json/include/nlohmann/adl_serializer.hpp new file mode 100644 index 000000000..eeaa14257 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/adl_serializer.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +namespace nlohmann +{ + +template +struct adl_serializer +{ + /*! + @brief convert a JSON value to any value type + + This function is usually called by the `get()` function of the + @ref basic_json class (either explicit or via conversion operators). + + @param[in] j JSON value to read from + @param[in,out] val value to write to + */ + template + static auto from_json(BasicJsonType&& j, ValueType& val) noexcept( + noexcept(::nlohmann::from_json(std::forward(j), val))) + -> decltype(::nlohmann::from_json(std::forward(j), val), void()) + { + ::nlohmann::from_json(std::forward(j), val); + } + + /*! + @brief convert any value type to a JSON value + + This function is usually called by the constructors of the @ref basic_json + class. + + @param[in,out] j JSON value to write to + @param[in] val value to read from + */ + template + static auto to_json(BasicJsonType& j, ValueType&& val) noexcept( + noexcept(::nlohmann::to_json(j, std::forward(val)))) + -> decltype(::nlohmann::to_json(j, std::forward(val)), void()) + { + ::nlohmann::to_json(j, std::forward(val)); + } +}; + +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/from_json.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/from_json.hpp new file mode 100644 index 000000000..224fb33f9 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/from_json.hpp @@ -0,0 +1,389 @@ +#pragma once + +#include // transform +#include // array +#include // and, not +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray + +#include +#include +#include +#include +#include + +namespace nlohmann +{ +namespace detail +{ +template +void from_json(const BasicJsonType& j, typename std::nullptr_t& n) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_null())) + { + JSON_THROW(type_error::create(302, "type must be null, but is " + std::string(j.type_name()))); + } + n = nullptr; +} + +// overloads for basic_json template parameters +template::value and + not std::is_same::value, + int> = 0> +void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) +{ + switch (static_cast(j)) + { + case value_t::number_unsigned: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_integer: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_float: + { + val = static_cast(*j.template get_ptr()); + break; + } + + default: + JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name()))); + } +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_boolean())) + { + JSON_THROW(type_error::create(302, "type must be boolean, but is " + std::string(j.type_name()))); + } + b = *j.template get_ptr(); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& s) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_string())) + { + JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()))); + } + s = *j.template get_ptr(); +} + +template < + typename BasicJsonType, typename ConstructibleStringType, + enable_if_t < + is_constructible_string_type::value and + not std::is_same::value, + int > = 0 > +void from_json(const BasicJsonType& j, ConstructibleStringType& s) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_string())) + { + JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()))); + } + + s = *j.template get_ptr(); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::number_float_t& val) +{ + get_arithmetic_value(j, val); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) +{ + get_arithmetic_value(j, val); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::number_integer_t& val) +{ + get_arithmetic_value(j, val); +} + +template::value, int> = 0> +void from_json(const BasicJsonType& j, EnumType& e) +{ + typename std::underlying_type::type val; + get_arithmetic_value(j, val); + e = static_cast(val); +} + +// forward_list doesn't have an insert method +template::value, int> = 0> +void from_json(const BasicJsonType& j, std::forward_list& l) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); + } + l.clear(); + std::transform(j.rbegin(), j.rend(), + std::front_inserter(l), [](const BasicJsonType & i) + { + return i.template get(); + }); +} + +// valarray doesn't have an insert method +template::value, int> = 0> +void from_json(const BasicJsonType& j, std::valarray& l) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); + } + l.resize(j.size()); + std::copy(j.m_value.array->begin(), j.m_value.array->end(), std::begin(l)); +} + +template +auto from_json(const BasicJsonType& j, T (&arr)[N]) +-> decltype(j.template get(), void()) +{ + for (std::size_t i = 0; i < N; ++i) + { + arr[i] = j.at(i).template get(); + } +} + +template +void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType::array_t& arr, priority_tag<3> /*unused*/) +{ + arr = *j.template get_ptr(); +} + +template +auto from_json_array_impl(const BasicJsonType& j, std::array& arr, + priority_tag<2> /*unused*/) +-> decltype(j.template get(), void()) +{ + for (std::size_t i = 0; i < N; ++i) + { + arr[i] = j.at(i).template get(); + } +} + +template +auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, priority_tag<1> /*unused*/) +-> decltype( + arr.reserve(std::declval()), + j.template get(), + void()) +{ + using std::end; + + ConstructibleArrayType ret; + ret.reserve(j.size()); + std::transform(j.begin(), j.end(), + std::inserter(ret, end(ret)), [](const BasicJsonType & i) + { + // get() returns *this, this won't call a from_json + // method when value_type is BasicJsonType + return i.template get(); + }); + arr = std::move(ret); +} + +template +void from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, + priority_tag<0> /*unused*/) +{ + using std::end; + + ConstructibleArrayType ret; + std::transform( + j.begin(), j.end(), std::inserter(ret, end(ret)), + [](const BasicJsonType & i) + { + // get() returns *this, this won't call a from_json + // method when value_type is BasicJsonType + return i.template get(); + }); + arr = std::move(ret); +} + +template ::value and + not is_constructible_object_type::value and + not is_constructible_string_type::value and + not is_basic_json::value, + int > = 0 > + +auto from_json(const BasicJsonType& j, ConstructibleArrayType& arr) +-> decltype(from_json_array_impl(j, arr, priority_tag<3> {}), +j.template get(), +void()) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + + std::string(j.type_name()))); + } + + from_json_array_impl(j, arr, priority_tag<3> {}); +} + +template::value, int> = 0> +void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_object())) + { + JSON_THROW(type_error::create(302, "type must be object, but is " + std::string(j.type_name()))); + } + + ConstructibleObjectType ret; + auto inner_object = j.template get_ptr(); + using value_type = typename ConstructibleObjectType::value_type; + std::transform( + inner_object->begin(), inner_object->end(), + std::inserter(ret, ret.begin()), + [](typename BasicJsonType::object_t::value_type const & p) + { + return value_type(p.first, p.second.template get()); + }); + obj = std::move(ret); +} + +// overload for arithmetic types, not chosen for basic_json template arguments +// (BooleanType, etc..); note: Is it really necessary to provide explicit +// overloads for boolean_t etc. in case of a custom BooleanType which is not +// an arithmetic type? +template::value and + not std::is_same::value and + not std::is_same::value and + not std::is_same::value and + not std::is_same::value, + int> = 0> +void from_json(const BasicJsonType& j, ArithmeticType& val) +{ + switch (static_cast(j)) + { + case value_t::number_unsigned: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_integer: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_float: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::boolean: + { + val = static_cast(*j.template get_ptr()); + break; + } + + default: + JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name()))); + } +} + +template +void from_json(const BasicJsonType& j, std::pair& p) +{ + p = {j.at(0).template get(), j.at(1).template get()}; +} + +template +void from_json_tuple_impl(const BasicJsonType& j, Tuple& t, index_sequence /*unused*/) +{ + t = std::make_tuple(j.at(Idx).template get::type>()...); +} + +template +void from_json(const BasicJsonType& j, std::tuple& t) +{ + from_json_tuple_impl(j, t, index_sequence_for {}); +} + +template ::value>> +void from_json(const BasicJsonType& j, std::map& m) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); + } + m.clear(); + for (const auto& p : j) + { + if (JSON_HEDLEY_UNLIKELY(not p.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name()))); + } + m.emplace(p.at(0).template get(), p.at(1).template get()); + } +} + +template ::value>> +void from_json(const BasicJsonType& j, std::unordered_map& m) +{ + if (JSON_HEDLEY_UNLIKELY(not j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()))); + } + m.clear(); + for (const auto& p : j) + { + if (JSON_HEDLEY_UNLIKELY(not p.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name()))); + } + m.emplace(p.at(0).template get(), p.at(1).template get()); + } +} + +struct from_json_fn +{ + template + auto operator()(const BasicJsonType& j, T& val) const + noexcept(noexcept(from_json(j, val))) + -> decltype(from_json(j, val), void()) + { + return from_json(j, val); + } +}; +} // namespace detail + +/// namespace to hold default `from_json` function +/// to see why this is required: +/// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html +namespace +{ +constexpr const auto& from_json = detail::static_const::value; +} // namespace +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_chars.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_chars.hpp new file mode 100644 index 000000000..d99703a54 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_chars.hpp @@ -0,0 +1,1106 @@ +#pragma once + +#include // array +#include // assert +#include // or, and, not +#include // signbit, isfinite +#include // intN_t, uintN_t +#include // memcpy, memmove +#include // numeric_limits +#include // conditional +#include + +namespace nlohmann +{ +namespace detail +{ + +/*! +@brief implements the Grisu2 algorithm for binary to decimal floating-point +conversion. + +This implementation is a slightly modified version of the reference +implementation which may be obtained from +http://florian.loitsch.com/publications (bench.tar.gz). + +The code is distributed under the MIT license, Copyright (c) 2009 Florian Loitsch. + +For a detailed description of the algorithm see: + +[1] Loitsch, "Printing Floating-Point Numbers Quickly and Accurately with + Integers", Proceedings of the ACM SIGPLAN 2010 Conference on Programming + Language Design and Implementation, PLDI 2010 +[2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and Accurately", + Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language + Design and Implementation, PLDI 1996 +*/ +namespace dtoa_impl +{ + +template +Target reinterpret_bits(const Source source) +{ + static_assert(sizeof(Target) == sizeof(Source), "size mismatch"); + + Target target; + std::memcpy(&target, &source, sizeof(Source)); + return target; +} + +struct diyfp // f * 2^e +{ + static constexpr int kPrecision = 64; // = q + + std::uint64_t f = 0; + int e = 0; + + constexpr diyfp(std::uint64_t f_, int e_) noexcept : f(f_), e(e_) {} + + /*! + @brief returns x - y + @pre x.e == y.e and x.f >= y.f + */ + static diyfp sub(const diyfp& x, const diyfp& y) noexcept + { + assert(x.e == y.e); + assert(x.f >= y.f); + + return {x.f - y.f, x.e}; + } + + /*! + @brief returns x * y + @note The result is rounded. (Only the upper q bits are returned.) + */ + static diyfp mul(const diyfp& x, const diyfp& y) noexcept + { + static_assert(kPrecision == 64, "internal error"); + + // Computes: + // f = round((x.f * y.f) / 2^q) + // e = x.e + y.e + q + + // Emulate the 64-bit * 64-bit multiplication: + // + // p = u * v + // = (u_lo + 2^32 u_hi) (v_lo + 2^32 v_hi) + // = (u_lo v_lo ) + 2^32 ((u_lo v_hi ) + (u_hi v_lo )) + 2^64 (u_hi v_hi ) + // = (p0 ) + 2^32 ((p1 ) + (p2 )) + 2^64 (p3 ) + // = (p0_lo + 2^32 p0_hi) + 2^32 ((p1_lo + 2^32 p1_hi) + (p2_lo + 2^32 p2_hi)) + 2^64 (p3 ) + // = (p0_lo ) + 2^32 (p0_hi + p1_lo + p2_lo ) + 2^64 (p1_hi + p2_hi + p3) + // = (p0_lo ) + 2^32 (Q ) + 2^64 (H ) + // = (p0_lo ) + 2^32 (Q_lo + 2^32 Q_hi ) + 2^64 (H ) + // + // (Since Q might be larger than 2^32 - 1) + // + // = (p0_lo + 2^32 Q_lo) + 2^64 (Q_hi + H) + // + // (Q_hi + H does not overflow a 64-bit int) + // + // = p_lo + 2^64 p_hi + + const std::uint64_t u_lo = x.f & 0xFFFFFFFFu; + const std::uint64_t u_hi = x.f >> 32u; + const std::uint64_t v_lo = y.f & 0xFFFFFFFFu; + const std::uint64_t v_hi = y.f >> 32u; + + const std::uint64_t p0 = u_lo * v_lo; + const std::uint64_t p1 = u_lo * v_hi; + const std::uint64_t p2 = u_hi * v_lo; + const std::uint64_t p3 = u_hi * v_hi; + + const std::uint64_t p0_hi = p0 >> 32u; + const std::uint64_t p1_lo = p1 & 0xFFFFFFFFu; + const std::uint64_t p1_hi = p1 >> 32u; + const std::uint64_t p2_lo = p2 & 0xFFFFFFFFu; + const std::uint64_t p2_hi = p2 >> 32u; + + std::uint64_t Q = p0_hi + p1_lo + p2_lo; + + // The full product might now be computed as + // + // p_hi = p3 + p2_hi + p1_hi + (Q >> 32) + // p_lo = p0_lo + (Q << 32) + // + // But in this particular case here, the full p_lo is not required. + // Effectively we only need to add the highest bit in p_lo to p_hi (and + // Q_hi + 1 does not overflow). + + Q += std::uint64_t{1} << (64u - 32u - 1u); // round, ties up + + const std::uint64_t h = p3 + p2_hi + p1_hi + (Q >> 32u); + + return {h, x.e + y.e + 64}; + } + + /*! + @brief normalize x such that the significand is >= 2^(q-1) + @pre x.f != 0 + */ + static diyfp normalize(diyfp x) noexcept + { + assert(x.f != 0); + + while ((x.f >> 63u) == 0) + { + x.f <<= 1u; + x.e--; + } + + return x; + } + + /*! + @brief normalize x such that the result has the exponent E + @pre e >= x.e and the upper e - x.e bits of x.f must be zero. + */ + static diyfp normalize_to(const diyfp& x, const int target_exponent) noexcept + { + const int delta = x.e - target_exponent; + + assert(delta >= 0); + assert(((x.f << delta) >> delta) == x.f); + + return {x.f << delta, target_exponent}; + } +}; + +struct boundaries +{ + diyfp w; + diyfp minus; + diyfp plus; +}; + +/*! +Compute the (normalized) diyfp representing the input number 'value' and its +boundaries. + +@pre value must be finite and positive +*/ +template +boundaries compute_boundaries(FloatType value) +{ + assert(std::isfinite(value)); + assert(value > 0); + + // Convert the IEEE representation into a diyfp. + // + // If v is denormal: + // value = 0.F * 2^(1 - bias) = ( F) * 2^(1 - bias - (p-1)) + // If v is normalized: + // value = 1.F * 2^(E - bias) = (2^(p-1) + F) * 2^(E - bias - (p-1)) + + static_assert(std::numeric_limits::is_iec559, + "internal error: dtoa_short requires an IEEE-754 floating-point implementation"); + + constexpr int kPrecision = std::numeric_limits::digits; // = p (includes the hidden bit) + constexpr int kBias = std::numeric_limits::max_exponent - 1 + (kPrecision - 1); + constexpr int kMinExp = 1 - kBias; + constexpr std::uint64_t kHiddenBit = std::uint64_t{1} << (kPrecision - 1); // = 2^(p-1) + + using bits_type = typename std::conditional::type; + + const std::uint64_t bits = reinterpret_bits(value); + const std::uint64_t E = bits >> (kPrecision - 1); + const std::uint64_t F = bits & (kHiddenBit - 1); + + const bool is_denormal = E == 0; + const diyfp v = is_denormal + ? diyfp(F, kMinExp) + : diyfp(F + kHiddenBit, static_cast(E) - kBias); + + // Compute the boundaries m- and m+ of the floating-point value + // v = f * 2^e. + // + // Determine v- and v+, the floating-point predecessor and successor if v, + // respectively. + // + // v- = v - 2^e if f != 2^(p-1) or e == e_min (A) + // = v - 2^(e-1) if f == 2^(p-1) and e > e_min (B) + // + // v+ = v + 2^e + // + // Let m- = (v- + v) / 2 and m+ = (v + v+) / 2. All real numbers _strictly_ + // between m- and m+ round to v, regardless of how the input rounding + // algorithm breaks ties. + // + // ---+-------------+-------------+-------------+-------------+--- (A) + // v- m- v m+ v+ + // + // -----------------+------+------+-------------+-------------+--- (B) + // v- m- v m+ v+ + + const bool lower_boundary_is_closer = F == 0 and E > 1; + const diyfp m_plus = diyfp(2 * v.f + 1, v.e - 1); + const diyfp m_minus = lower_boundary_is_closer + ? diyfp(4 * v.f - 1, v.e - 2) // (B) + : diyfp(2 * v.f - 1, v.e - 1); // (A) + + // Determine the normalized w+ = m+. + const diyfp w_plus = diyfp::normalize(m_plus); + + // Determine w- = m- such that e_(w-) = e_(w+). + const diyfp w_minus = diyfp::normalize_to(m_minus, w_plus.e); + + return {diyfp::normalize(v), w_minus, w_plus}; +} + +// Given normalized diyfp w, Grisu needs to find a (normalized) cached +// power-of-ten c, such that the exponent of the product c * w = f * 2^e lies +// within a certain range [alpha, gamma] (Definition 3.2 from [1]) +// +// alpha <= e = e_c + e_w + q <= gamma +// +// or +// +// f_c * f_w * 2^alpha <= f_c 2^(e_c) * f_w 2^(e_w) * 2^q +// <= f_c * f_w * 2^gamma +// +// Since c and w are normalized, i.e. 2^(q-1) <= f < 2^q, this implies +// +// 2^(q-1) * 2^(q-1) * 2^alpha <= c * w * 2^q < 2^q * 2^q * 2^gamma +// +// or +// +// 2^(q - 2 + alpha) <= c * w < 2^(q + gamma) +// +// The choice of (alpha,gamma) determines the size of the table and the form of +// the digit generation procedure. Using (alpha,gamma)=(-60,-32) works out well +// in practice: +// +// The idea is to cut the number c * w = f * 2^e into two parts, which can be +// processed independently: An integral part p1, and a fractional part p2: +// +// f * 2^e = ( (f div 2^-e) * 2^-e + (f mod 2^-e) ) * 2^e +// = (f div 2^-e) + (f mod 2^-e) * 2^e +// = p1 + p2 * 2^e +// +// The conversion of p1 into decimal form requires a series of divisions and +// modulos by (a power of) 10. These operations are faster for 32-bit than for +// 64-bit integers, so p1 should ideally fit into a 32-bit integer. This can be +// achieved by choosing +// +// -e >= 32 or e <= -32 := gamma +// +// In order to convert the fractional part +// +// p2 * 2^e = p2 / 2^-e = d[-1] / 10^1 + d[-2] / 10^2 + ... +// +// into decimal form, the fraction is repeatedly multiplied by 10 and the digits +// d[-i] are extracted in order: +// +// (10 * p2) div 2^-e = d[-1] +// (10 * p2) mod 2^-e = d[-2] / 10^1 + ... +// +// The multiplication by 10 must not overflow. It is sufficient to choose +// +// 10 * p2 < 16 * p2 = 2^4 * p2 <= 2^64. +// +// Since p2 = f mod 2^-e < 2^-e, +// +// -e <= 60 or e >= -60 := alpha + +constexpr int kAlpha = -60; +constexpr int kGamma = -32; + +struct cached_power // c = f * 2^e ~= 10^k +{ + std::uint64_t f; + int e; + int k; +}; + +/*! +For a normalized diyfp w = f * 2^e, this function returns a (normalized) cached +power-of-ten c = f_c * 2^e_c, such that the exponent of the product w * c +satisfies (Definition 3.2 from [1]) + + alpha <= e_c + e + q <= gamma. +*/ +inline cached_power get_cached_power_for_binary_exponent(int e) +{ + // Now + // + // alpha <= e_c + e + q <= gamma (1) + // ==> f_c * 2^alpha <= c * 2^e * 2^q + // + // and since the c's are normalized, 2^(q-1) <= f_c, + // + // ==> 2^(q - 1 + alpha) <= c * 2^(e + q) + // ==> 2^(alpha - e - 1) <= c + // + // If c were an exact power of ten, i.e. c = 10^k, one may determine k as + // + // k = ceil( log_10( 2^(alpha - e - 1) ) ) + // = ceil( (alpha - e - 1) * log_10(2) ) + // + // From the paper: + // "In theory the result of the procedure could be wrong since c is rounded, + // and the computation itself is approximated [...]. In practice, however, + // this simple function is sufficient." + // + // For IEEE double precision floating-point numbers converted into + // normalized diyfp's w = f * 2^e, with q = 64, + // + // e >= -1022 (min IEEE exponent) + // -52 (p - 1) + // -52 (p - 1, possibly normalize denormal IEEE numbers) + // -11 (normalize the diyfp) + // = -1137 + // + // and + // + // e <= +1023 (max IEEE exponent) + // -52 (p - 1) + // -11 (normalize the diyfp) + // = 960 + // + // This binary exponent range [-1137,960] results in a decimal exponent + // range [-307,324]. One does not need to store a cached power for each + // k in this range. For each such k it suffices to find a cached power + // such that the exponent of the product lies in [alpha,gamma]. + // This implies that the difference of the decimal exponents of adjacent + // table entries must be less than or equal to + // + // floor( (gamma - alpha) * log_10(2) ) = 8. + // + // (A smaller distance gamma-alpha would require a larger table.) + + // NB: + // Actually this function returns c, such that -60 <= e_c + e + 64 <= -34. + + constexpr int kCachedPowersMinDecExp = -300; + constexpr int kCachedPowersDecStep = 8; + + static constexpr std::array kCachedPowers = + { + { + { 0xAB70FE17C79AC6CA, -1060, -300 }, + { 0xFF77B1FCBEBCDC4F, -1034, -292 }, + { 0xBE5691EF416BD60C, -1007, -284 }, + { 0x8DD01FAD907FFC3C, -980, -276 }, + { 0xD3515C2831559A83, -954, -268 }, + { 0x9D71AC8FADA6C9B5, -927, -260 }, + { 0xEA9C227723EE8BCB, -901, -252 }, + { 0xAECC49914078536D, -874, -244 }, + { 0x823C12795DB6CE57, -847, -236 }, + { 0xC21094364DFB5637, -821, -228 }, + { 0x9096EA6F3848984F, -794, -220 }, + { 0xD77485CB25823AC7, -768, -212 }, + { 0xA086CFCD97BF97F4, -741, -204 }, + { 0xEF340A98172AACE5, -715, -196 }, + { 0xB23867FB2A35B28E, -688, -188 }, + { 0x84C8D4DFD2C63F3B, -661, -180 }, + { 0xC5DD44271AD3CDBA, -635, -172 }, + { 0x936B9FCEBB25C996, -608, -164 }, + { 0xDBAC6C247D62A584, -582, -156 }, + { 0xA3AB66580D5FDAF6, -555, -148 }, + { 0xF3E2F893DEC3F126, -529, -140 }, + { 0xB5B5ADA8AAFF80B8, -502, -132 }, + { 0x87625F056C7C4A8B, -475, -124 }, + { 0xC9BCFF6034C13053, -449, -116 }, + { 0x964E858C91BA2655, -422, -108 }, + { 0xDFF9772470297EBD, -396, -100 }, + { 0xA6DFBD9FB8E5B88F, -369, -92 }, + { 0xF8A95FCF88747D94, -343, -84 }, + { 0xB94470938FA89BCF, -316, -76 }, + { 0x8A08F0F8BF0F156B, -289, -68 }, + { 0xCDB02555653131B6, -263, -60 }, + { 0x993FE2C6D07B7FAC, -236, -52 }, + { 0xE45C10C42A2B3B06, -210, -44 }, + { 0xAA242499697392D3, -183, -36 }, + { 0xFD87B5F28300CA0E, -157, -28 }, + { 0xBCE5086492111AEB, -130, -20 }, + { 0x8CBCCC096F5088CC, -103, -12 }, + { 0xD1B71758E219652C, -77, -4 }, + { 0x9C40000000000000, -50, 4 }, + { 0xE8D4A51000000000, -24, 12 }, + { 0xAD78EBC5AC620000, 3, 20 }, + { 0x813F3978F8940984, 30, 28 }, + { 0xC097CE7BC90715B3, 56, 36 }, + { 0x8F7E32CE7BEA5C70, 83, 44 }, + { 0xD5D238A4ABE98068, 109, 52 }, + { 0x9F4F2726179A2245, 136, 60 }, + { 0xED63A231D4C4FB27, 162, 68 }, + { 0xB0DE65388CC8ADA8, 189, 76 }, + { 0x83C7088E1AAB65DB, 216, 84 }, + { 0xC45D1DF942711D9A, 242, 92 }, + { 0x924D692CA61BE758, 269, 100 }, + { 0xDA01EE641A708DEA, 295, 108 }, + { 0xA26DA3999AEF774A, 322, 116 }, + { 0xF209787BB47D6B85, 348, 124 }, + { 0xB454E4A179DD1877, 375, 132 }, + { 0x865B86925B9BC5C2, 402, 140 }, + { 0xC83553C5C8965D3D, 428, 148 }, + { 0x952AB45CFA97A0B3, 455, 156 }, + { 0xDE469FBD99A05FE3, 481, 164 }, + { 0xA59BC234DB398C25, 508, 172 }, + { 0xF6C69A72A3989F5C, 534, 180 }, + { 0xB7DCBF5354E9BECE, 561, 188 }, + { 0x88FCF317F22241E2, 588, 196 }, + { 0xCC20CE9BD35C78A5, 614, 204 }, + { 0x98165AF37B2153DF, 641, 212 }, + { 0xE2A0B5DC971F303A, 667, 220 }, + { 0xA8D9D1535CE3B396, 694, 228 }, + { 0xFB9B7CD9A4A7443C, 720, 236 }, + { 0xBB764C4CA7A44410, 747, 244 }, + { 0x8BAB8EEFB6409C1A, 774, 252 }, + { 0xD01FEF10A657842C, 800, 260 }, + { 0x9B10A4E5E9913129, 827, 268 }, + { 0xE7109BFBA19C0C9D, 853, 276 }, + { 0xAC2820D9623BF429, 880, 284 }, + { 0x80444B5E7AA7CF85, 907, 292 }, + { 0xBF21E44003ACDD2D, 933, 300 }, + { 0x8E679C2F5E44FF8F, 960, 308 }, + { 0xD433179D9C8CB841, 986, 316 }, + { 0x9E19DB92B4E31BA9, 1013, 324 }, + } + }; + + // This computation gives exactly the same results for k as + // k = ceil((kAlpha - e - 1) * 0.30102999566398114) + // for |e| <= 1500, but doesn't require floating-point operations. + // NB: log_10(2) ~= 78913 / 2^18 + assert(e >= -1500); + assert(e <= 1500); + const int f = kAlpha - e - 1; + const int k = (f * 78913) / (1 << 18) + static_cast(f > 0); + + const int index = (-kCachedPowersMinDecExp + k + (kCachedPowersDecStep - 1)) / kCachedPowersDecStep; + assert(index >= 0); + assert(static_cast(index) < kCachedPowers.size()); + + const cached_power cached = kCachedPowers[static_cast(index)]; + assert(kAlpha <= cached.e + e + 64); + assert(kGamma >= cached.e + e + 64); + + return cached; +} + +/*! +For n != 0, returns k, such that pow10 := 10^(k-1) <= n < 10^k. +For n == 0, returns 1 and sets pow10 := 1. +*/ +inline int find_largest_pow10(const std::uint32_t n, std::uint32_t& pow10) +{ + // LCOV_EXCL_START + if (n >= 1000000000) + { + pow10 = 1000000000; + return 10; + } + // LCOV_EXCL_STOP + else if (n >= 100000000) + { + pow10 = 100000000; + return 9; + } + else if (n >= 10000000) + { + pow10 = 10000000; + return 8; + } + else if (n >= 1000000) + { + pow10 = 1000000; + return 7; + } + else if (n >= 100000) + { + pow10 = 100000; + return 6; + } + else if (n >= 10000) + { + pow10 = 10000; + return 5; + } + else if (n >= 1000) + { + pow10 = 1000; + return 4; + } + else if (n >= 100) + { + pow10 = 100; + return 3; + } + else if (n >= 10) + { + pow10 = 10; + return 2; + } + else + { + pow10 = 1; + return 1; + } +} + +inline void grisu2_round(char* buf, int len, std::uint64_t dist, std::uint64_t delta, + std::uint64_t rest, std::uint64_t ten_k) +{ + assert(len >= 1); + assert(dist <= delta); + assert(rest <= delta); + assert(ten_k > 0); + + // <--------------------------- delta ----> + // <---- dist ---------> + // --------------[------------------+-------------------]-------------- + // M- w M+ + // + // ten_k + // <------> + // <---- rest ----> + // --------------[------------------+----+--------------]-------------- + // w V + // = buf * 10^k + // + // ten_k represents a unit-in-the-last-place in the decimal representation + // stored in buf. + // Decrement buf by ten_k while this takes buf closer to w. + + // The tests are written in this order to avoid overflow in unsigned + // integer arithmetic. + + while (rest < dist + and delta - rest >= ten_k + and (rest + ten_k < dist or dist - rest > rest + ten_k - dist)) + { + assert(buf[len - 1] != '0'); + buf[len - 1]--; + rest += ten_k; + } +} + +/*! +Generates V = buffer * 10^decimal_exponent, such that M- <= V <= M+. +M- and M+ must be normalized and share the same exponent -60 <= e <= -32. +*/ +inline void grisu2_digit_gen(char* buffer, int& length, int& decimal_exponent, + diyfp M_minus, diyfp w, diyfp M_plus) +{ + static_assert(kAlpha >= -60, "internal error"); + static_assert(kGamma <= -32, "internal error"); + + // Generates the digits (and the exponent) of a decimal floating-point + // number V = buffer * 10^decimal_exponent in the range [M-, M+]. The diyfp's + // w, M- and M+ share the same exponent e, which satisfies alpha <= e <= gamma. + // + // <--------------------------- delta ----> + // <---- dist ---------> + // --------------[------------------+-------------------]-------------- + // M- w M+ + // + // Grisu2 generates the digits of M+ from left to right and stops as soon as + // V is in [M-,M+]. + + assert(M_plus.e >= kAlpha); + assert(M_plus.e <= kGamma); + + std::uint64_t delta = diyfp::sub(M_plus, M_minus).f; // (significand of (M+ - M-), implicit exponent is e) + std::uint64_t dist = diyfp::sub(M_plus, w ).f; // (significand of (M+ - w ), implicit exponent is e) + + // Split M+ = f * 2^e into two parts p1 and p2 (note: e < 0): + // + // M+ = f * 2^e + // = ((f div 2^-e) * 2^-e + (f mod 2^-e)) * 2^e + // = ((p1 ) * 2^-e + (p2 )) * 2^e + // = p1 + p2 * 2^e + + const diyfp one(std::uint64_t{1} << -M_plus.e, M_plus.e); + + auto p1 = static_cast(M_plus.f >> -one.e); // p1 = f div 2^-e (Since -e >= 32, p1 fits into a 32-bit int.) + std::uint64_t p2 = M_plus.f & (one.f - 1); // p2 = f mod 2^-e + + // 1) + // + // Generate the digits of the integral part p1 = d[n-1]...d[1]d[0] + + assert(p1 > 0); + + std::uint32_t pow10; + const int k = find_largest_pow10(p1, pow10); + + // 10^(k-1) <= p1 < 10^k, pow10 = 10^(k-1) + // + // p1 = (p1 div 10^(k-1)) * 10^(k-1) + (p1 mod 10^(k-1)) + // = (d[k-1] ) * 10^(k-1) + (p1 mod 10^(k-1)) + // + // M+ = p1 + p2 * 2^e + // = d[k-1] * 10^(k-1) + (p1 mod 10^(k-1)) + p2 * 2^e + // = d[k-1] * 10^(k-1) + ((p1 mod 10^(k-1)) * 2^-e + p2) * 2^e + // = d[k-1] * 10^(k-1) + ( rest) * 2^e + // + // Now generate the digits d[n] of p1 from left to right (n = k-1,...,0) + // + // p1 = d[k-1]...d[n] * 10^n + d[n-1]...d[0] + // + // but stop as soon as + // + // rest * 2^e = (d[n-1]...d[0] * 2^-e + p2) * 2^e <= delta * 2^e + + int n = k; + while (n > 0) + { + // Invariants: + // M+ = buffer * 10^n + (p1 + p2 * 2^e) (buffer = 0 for n = k) + // pow10 = 10^(n-1) <= p1 < 10^n + // + const std::uint32_t d = p1 / pow10; // d = p1 div 10^(n-1) + const std::uint32_t r = p1 % pow10; // r = p1 mod 10^(n-1) + // + // M+ = buffer * 10^n + (d * 10^(n-1) + r) + p2 * 2^e + // = (buffer * 10 + d) * 10^(n-1) + (r + p2 * 2^e) + // + assert(d <= 9); + buffer[length++] = static_cast('0' + d); // buffer := buffer * 10 + d + // + // M+ = buffer * 10^(n-1) + (r + p2 * 2^e) + // + p1 = r; + n--; + // + // M+ = buffer * 10^n + (p1 + p2 * 2^e) + // pow10 = 10^n + // + + // Now check if enough digits have been generated. + // Compute + // + // p1 + p2 * 2^e = (p1 * 2^-e + p2) * 2^e = rest * 2^e + // + // Note: + // Since rest and delta share the same exponent e, it suffices to + // compare the significands. + const std::uint64_t rest = (std::uint64_t{p1} << -one.e) + p2; + if (rest <= delta) + { + // V = buffer * 10^n, with M- <= V <= M+. + + decimal_exponent += n; + + // We may now just stop. But instead look if the buffer could be + // decremented to bring V closer to w. + // + // pow10 = 10^n is now 1 ulp in the decimal representation V. + // The rounding procedure works with diyfp's with an implicit + // exponent of e. + // + // 10^n = (10^n * 2^-e) * 2^e = ulp * 2^e + // + const std::uint64_t ten_n = std::uint64_t{pow10} << -one.e; + grisu2_round(buffer, length, dist, delta, rest, ten_n); + + return; + } + + pow10 /= 10; + // + // pow10 = 10^(n-1) <= p1 < 10^n + // Invariants restored. + } + + // 2) + // + // The digits of the integral part have been generated: + // + // M+ = d[k-1]...d[1]d[0] + p2 * 2^e + // = buffer + p2 * 2^e + // + // Now generate the digits of the fractional part p2 * 2^e. + // + // Note: + // No decimal point is generated: the exponent is adjusted instead. + // + // p2 actually represents the fraction + // + // p2 * 2^e + // = p2 / 2^-e + // = d[-1] / 10^1 + d[-2] / 10^2 + ... + // + // Now generate the digits d[-m] of p1 from left to right (m = 1,2,...) + // + // p2 * 2^e = d[-1]d[-2]...d[-m] * 10^-m + // + 10^-m * (d[-m-1] / 10^1 + d[-m-2] / 10^2 + ...) + // + // using + // + // 10^m * p2 = ((10^m * p2) div 2^-e) * 2^-e + ((10^m * p2) mod 2^-e) + // = ( d) * 2^-e + ( r) + // + // or + // 10^m * p2 * 2^e = d + r * 2^e + // + // i.e. + // + // M+ = buffer + p2 * 2^e + // = buffer + 10^-m * (d + r * 2^e) + // = (buffer * 10^m + d) * 10^-m + 10^-m * r * 2^e + // + // and stop as soon as 10^-m * r * 2^e <= delta * 2^e + + assert(p2 > delta); + + int m = 0; + for (;;) + { + // Invariant: + // M+ = buffer * 10^-m + 10^-m * (d[-m-1] / 10 + d[-m-2] / 10^2 + ...) * 2^e + // = buffer * 10^-m + 10^-m * (p2 ) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * (10 * p2) ) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * ((10*p2 div 2^-e) * 2^-e + (10*p2 mod 2^-e)) * 2^e + // + assert(p2 <= (std::numeric_limits::max)() / 10); + p2 *= 10; + const std::uint64_t d = p2 >> -one.e; // d = (10 * p2) div 2^-e + const std::uint64_t r = p2 & (one.f - 1); // r = (10 * p2) mod 2^-e + // + // M+ = buffer * 10^-m + 10^-m * (1/10 * (d * 2^-e + r) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * (d + r * 2^e)) + // = (buffer * 10 + d) * 10^(-m-1) + 10^(-m-1) * r * 2^e + // + assert(d <= 9); + buffer[length++] = static_cast('0' + d); // buffer := buffer * 10 + d + // + // M+ = buffer * 10^(-m-1) + 10^(-m-1) * r * 2^e + // + p2 = r; + m++; + // + // M+ = buffer * 10^-m + 10^-m * p2 * 2^e + // Invariant restored. + + // Check if enough digits have been generated. + // + // 10^-m * p2 * 2^e <= delta * 2^e + // p2 * 2^e <= 10^m * delta * 2^e + // p2 <= 10^m * delta + delta *= 10; + dist *= 10; + if (p2 <= delta) + { + break; + } + } + + // V = buffer * 10^-m, with M- <= V <= M+. + + decimal_exponent -= m; + + // 1 ulp in the decimal representation is now 10^-m. + // Since delta and dist are now scaled by 10^m, we need to do the + // same with ulp in order to keep the units in sync. + // + // 10^m * 10^-m = 1 = 2^-e * 2^e = ten_m * 2^e + // + const std::uint64_t ten_m = one.f; + grisu2_round(buffer, length, dist, delta, p2, ten_m); + + // By construction this algorithm generates the shortest possible decimal + // number (Loitsch, Theorem 6.2) which rounds back to w. + // For an input number of precision p, at least + // + // N = 1 + ceil(p * log_10(2)) + // + // decimal digits are sufficient to identify all binary floating-point + // numbers (Matula, "In-and-Out conversions"). + // This implies that the algorithm does not produce more than N decimal + // digits. + // + // N = 17 for p = 53 (IEEE double precision) + // N = 9 for p = 24 (IEEE single precision) +} + +/*! +v = buf * 10^decimal_exponent +len is the length of the buffer (number of decimal digits) +The buffer must be large enough, i.e. >= max_digits10. +*/ +JSON_HEDLEY_NON_NULL(1) +inline void grisu2(char* buf, int& len, int& decimal_exponent, + diyfp m_minus, diyfp v, diyfp m_plus) +{ + assert(m_plus.e == m_minus.e); + assert(m_plus.e == v.e); + + // --------(-----------------------+-----------------------)-------- (A) + // m- v m+ + // + // --------------------(-----------+-----------------------)-------- (B) + // m- v m+ + // + // First scale v (and m- and m+) such that the exponent is in the range + // [alpha, gamma]. + + const cached_power cached = get_cached_power_for_binary_exponent(m_plus.e); + + const diyfp c_minus_k(cached.f, cached.e); // = c ~= 10^-k + + // The exponent of the products is = v.e + c_minus_k.e + q and is in the range [alpha,gamma] + const diyfp w = diyfp::mul(v, c_minus_k); + const diyfp w_minus = diyfp::mul(m_minus, c_minus_k); + const diyfp w_plus = diyfp::mul(m_plus, c_minus_k); + + // ----(---+---)---------------(---+---)---------------(---+---)---- + // w- w w+ + // = c*m- = c*v = c*m+ + // + // diyfp::mul rounds its result and c_minus_k is approximated too. w, w- and + // w+ are now off by a small amount. + // In fact: + // + // w - v * 10^k < 1 ulp + // + // To account for this inaccuracy, add resp. subtract 1 ulp. + // + // --------+---[---------------(---+---)---------------]---+-------- + // w- M- w M+ w+ + // + // Now any number in [M-, M+] (bounds included) will round to w when input, + // regardless of how the input rounding algorithm breaks ties. + // + // And digit_gen generates the shortest possible such number in [M-, M+]. + // Note that this does not mean that Grisu2 always generates the shortest + // possible number in the interval (m-, m+). + const diyfp M_minus(w_minus.f + 1, w_minus.e); + const diyfp M_plus (w_plus.f - 1, w_plus.e ); + + decimal_exponent = -cached.k; // = -(-k) = k + + grisu2_digit_gen(buf, len, decimal_exponent, M_minus, w, M_plus); +} + +/*! +v = buf * 10^decimal_exponent +len is the length of the buffer (number of decimal digits) +The buffer must be large enough, i.e. >= max_digits10. +*/ +template +JSON_HEDLEY_NON_NULL(1) +void grisu2(char* buf, int& len, int& decimal_exponent, FloatType value) +{ + static_assert(diyfp::kPrecision >= std::numeric_limits::digits + 3, + "internal error: not enough precision"); + + assert(std::isfinite(value)); + assert(value > 0); + + // If the neighbors (and boundaries) of 'value' are always computed for double-precision + // numbers, all float's can be recovered using strtod (and strtof). However, the resulting + // decimal representations are not exactly "short". + // + // The documentation for 'std::to_chars' (https://en.cppreference.com/w/cpp/utility/to_chars) + // says "value is converted to a string as if by std::sprintf in the default ("C") locale" + // and since sprintf promotes float's to double's, I think this is exactly what 'std::to_chars' + // does. + // On the other hand, the documentation for 'std::to_chars' requires that "parsing the + // representation using the corresponding std::from_chars function recovers value exactly". That + // indicates that single precision floating-point numbers should be recovered using + // 'std::strtof'. + // + // NB: If the neighbors are computed for single-precision numbers, there is a single float + // (7.0385307e-26f) which can't be recovered using strtod. The resulting double precision + // value is off by 1 ulp. +#if 0 + const boundaries w = compute_boundaries(static_cast(value)); +#else + const boundaries w = compute_boundaries(value); +#endif + + grisu2(buf, len, decimal_exponent, w.minus, w.w, w.plus); +} + +/*! +@brief appends a decimal representation of e to buf +@return a pointer to the element following the exponent. +@pre -1000 < e < 1000 +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* append_exponent(char* buf, int e) +{ + assert(e > -1000); + assert(e < 1000); + + if (e < 0) + { + e = -e; + *buf++ = '-'; + } + else + { + *buf++ = '+'; + } + + auto k = static_cast(e); + if (k < 10) + { + // Always print at least two digits in the exponent. + // This is for compatibility with printf("%g"). + *buf++ = '0'; + *buf++ = static_cast('0' + k); + } + else if (k < 100) + { + *buf++ = static_cast('0' + k / 10); + k %= 10; + *buf++ = static_cast('0' + k); + } + else + { + *buf++ = static_cast('0' + k / 100); + k %= 100; + *buf++ = static_cast('0' + k / 10); + k %= 10; + *buf++ = static_cast('0' + k); + } + + return buf; +} + +/*! +@brief prettify v = buf * 10^decimal_exponent + +If v is in the range [10^min_exp, 10^max_exp) it will be printed in fixed-point +notation. Otherwise it will be printed in exponential notation. + +@pre min_exp < 0 +@pre max_exp > 0 +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* format_buffer(char* buf, int len, int decimal_exponent, + int min_exp, int max_exp) +{ + assert(min_exp < 0); + assert(max_exp > 0); + + const int k = len; + const int n = len + decimal_exponent; + + // v = buf * 10^(n-k) + // k is the length of the buffer (number of decimal digits) + // n is the position of the decimal point relative to the start of the buffer. + + if (k <= n and n <= max_exp) + { + // digits[000] + // len <= max_exp + 2 + + std::memset(buf + k, '0', static_cast(n - k)); + // Make it look like a floating-point number (#362, #378) + buf[n + 0] = '.'; + buf[n + 1] = '0'; + return buf + (n + 2); + } + + if (0 < n and n <= max_exp) + { + // dig.its + // len <= max_digits10 + 1 + + assert(k > n); + + std::memmove(buf + (n + 1), buf + n, static_cast(k - n)); + buf[n] = '.'; + return buf + (k + 1); + } + + if (min_exp < n and n <= 0) + { + // 0.[000]digits + // len <= 2 + (-min_exp - 1) + max_digits10 + + std::memmove(buf + (2 + -n), buf, static_cast(k)); + buf[0] = '0'; + buf[1] = '.'; + std::memset(buf + 2, '0', static_cast(-n)); + return buf + (2 + (-n) + k); + } + + if (k == 1) + { + // dE+123 + // len <= 1 + 5 + + buf += 1; + } + else + { + // d.igitsE+123 + // len <= max_digits10 + 1 + 5 + + std::memmove(buf + 2, buf + 1, static_cast(k - 1)); + buf[1] = '.'; + buf += 1 + k; + } + + *buf++ = 'e'; + return append_exponent(buf, n - 1); +} + +} // namespace dtoa_impl + +/*! +@brief generates a decimal representation of the floating-point number value in [first, last). + +The format of the resulting decimal representation is similar to printf's %g +format. Returns an iterator pointing past-the-end of the decimal representation. + +@note The input number must be finite, i.e. NaN's and Inf's are not supported. +@note The buffer must be large enough. +@note The result is NOT null-terminated. +*/ +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* to_chars(char* first, const char* last, FloatType value) +{ + static_cast(last); // maybe unused - fix warning + assert(std::isfinite(value)); + + // Use signbit(value) instead of (value < 0) since signbit works for -0. + if (std::signbit(value)) + { + value = -value; + *first++ = '-'; + } + + if (value == 0) // +-0 + { + *first++ = '0'; + // Make it look like a floating-point number (#362, #378) + *first++ = '.'; + *first++ = '0'; + return first; + } + + assert(last - first >= std::numeric_limits::max_digits10); + + // Compute v = buffer * 10^decimal_exponent. + // The decimal digits are stored in the buffer, which needs to be interpreted + // as an unsigned decimal integer. + // len is the length of the buffer, i.e. the number of decimal digits. + int len = 0; + int decimal_exponent = 0; + dtoa_impl::grisu2(first, len, decimal_exponent, value); + + assert(len <= std::numeric_limits::max_digits10); + + // Format the buffer like printf("%.*g", prec, value) + constexpr int kMinExp = -4; + // Use digits10 here to increase compatibility with version 2. + constexpr int kMaxExp = std::numeric_limits::digits10; + + assert(last - first >= kMaxExp + 2); + assert(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits::max_digits10); + assert(last - first >= std::numeric_limits::max_digits10 + 6); + + return dtoa_impl::format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); +} + +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_json.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_json.hpp new file mode 100644 index 000000000..c3ac5aa8f --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/conversions/to_json.hpp @@ -0,0 +1,347 @@ +#pragma once + +#include // copy +#include // or, and, not +#include // begin, end +#include // string +#include // tuple, get +#include // is_same, is_constructible, is_floating_point, is_enum, underlying_type +#include // move, forward, declval, pair +#include // valarray +#include // vector + +#include +#include +#include +#include + +namespace nlohmann +{ +namespace detail +{ +////////////////// +// constructors // +////////////////// + +template struct external_constructor; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::boolean_t b) noexcept + { + j.m_type = value_t::boolean; + j.m_value = b; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::string_t& s) + { + j.m_type = value_t::string; + j.m_value = s; + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::string_t&& s) + { + j.m_type = value_t::string; + j.m_value = std::move(s); + j.assert_invariant(); + } + + template::value, + int> = 0> + static void construct(BasicJsonType& j, const CompatibleStringType& str) + { + j.m_type = value_t::string; + j.m_value.string = j.template create(str); + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept + { + j.m_type = value_t::number_float; + j.m_value = val; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::number_unsigned_t val) noexcept + { + j.m_type = value_t::number_unsigned; + j.m_value = val; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::number_integer_t val) noexcept + { + j.m_type = value_t::number_integer; + j.m_value = val; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::array_t& arr) + { + j.m_type = value_t::array; + j.m_value = arr; + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::array_t&& arr) + { + j.m_type = value_t::array; + j.m_value = std::move(arr); + j.assert_invariant(); + } + + template::value, + int> = 0> + static void construct(BasicJsonType& j, const CompatibleArrayType& arr) + { + using std::begin; + using std::end; + j.m_type = value_t::array; + j.m_value.array = j.template create(begin(arr), end(arr)); + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, const std::vector& arr) + { + j.m_type = value_t::array; + j.m_value = value_t::array; + j.m_value.array->reserve(arr.size()); + for (const bool x : arr) + { + j.m_value.array->push_back(x); + } + j.assert_invariant(); + } + + template::value, int> = 0> + static void construct(BasicJsonType& j, const std::valarray& arr) + { + j.m_type = value_t::array; + j.m_value = value_t::array; + j.m_value.array->resize(arr.size()); + if (arr.size() > 0) + { + std::copy(std::begin(arr), std::end(arr), j.m_value.array->begin()); + } + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::object_t& obj) + { + j.m_type = value_t::object; + j.m_value = obj; + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::object_t&& obj) + { + j.m_type = value_t::object; + j.m_value = std::move(obj); + j.assert_invariant(); + } + + template::value, int> = 0> + static void construct(BasicJsonType& j, const CompatibleObjectType& obj) + { + using std::begin; + using std::end; + + j.m_type = value_t::object; + j.m_value.object = j.template create(begin(obj), end(obj)); + j.assert_invariant(); + } +}; + +///////////// +// to_json // +///////////// + +template::value, int> = 0> +void to_json(BasicJsonType& j, T b) noexcept +{ + external_constructor::construct(j, b); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, const CompatibleString& s) +{ + external_constructor::construct(j, s); +} + +template +void to_json(BasicJsonType& j, typename BasicJsonType::string_t&& s) +{ + external_constructor::construct(j, std::move(s)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, FloatType val) noexcept +{ + external_constructor::construct(j, static_cast(val)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept +{ + external_constructor::construct(j, static_cast(val)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept +{ + external_constructor::construct(j, static_cast(val)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, EnumType e) noexcept +{ + using underlying_type = typename std::underlying_type::type; + external_constructor::construct(j, static_cast(e)); +} + +template +void to_json(BasicJsonType& j, const std::vector& e) +{ + external_constructor::construct(j, e); +} + +template ::value and + not is_compatible_object_type< + BasicJsonType, CompatibleArrayType>::value and + not is_compatible_string_type::value and + not is_basic_json::value, + int> = 0> +void to_json(BasicJsonType& j, const CompatibleArrayType& arr) +{ + external_constructor::construct(j, arr); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, const std::valarray& arr) +{ + external_constructor::construct(j, std::move(arr)); +} + +template +void to_json(BasicJsonType& j, typename BasicJsonType::array_t&& arr) +{ + external_constructor::construct(j, std::move(arr)); +} + +template::value and not is_basic_json::value, int> = 0> +void to_json(BasicJsonType& j, const CompatibleObjectType& obj) +{ + external_constructor::construct(j, obj); +} + +template +void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj) +{ + external_constructor::construct(j, std::move(obj)); +} + +template < + typename BasicJsonType, typename T, std::size_t N, + enable_if_t::value, + int> = 0 > +void to_json(BasicJsonType& j, const T(&arr)[N]) +{ + external_constructor::construct(j, arr); +} + +template +void to_json(BasicJsonType& j, const std::pair& p) +{ + j = { p.first, p.second }; +} + +// for https://github.com/nlohmann/json/pull/1134 +template < typename BasicJsonType, typename T, + enable_if_t>::value, int> = 0> +void to_json(BasicJsonType& j, const T& b) +{ + j = { {b.key(), b.value()} }; +} + +template +void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence /*unused*/) +{ + j = { std::get(t)... }; +} + +template +void to_json(BasicJsonType& j, const std::tuple& t) +{ + to_json_tuple_impl(j, t, index_sequence_for {}); +} + +struct to_json_fn +{ + template + auto operator()(BasicJsonType& j, T&& val) const noexcept(noexcept(to_json(j, std::forward(val)))) + -> decltype(to_json(j, std::forward(val)), void()) + { + return to_json(j, std::forward(val)); + } +}; +} // namespace detail + +/// namespace to hold default `to_json` function +namespace +{ +constexpr const auto& to_json = detail::static_const::value; +} // namespace +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/exceptions.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/exceptions.hpp new file mode 100644 index 000000000..ed836188c --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/exceptions.hpp @@ -0,0 +1,356 @@ +#pragma once + +#include // exception +#include // runtime_error +#include // to_string + +#include +#include + +namespace nlohmann +{ +namespace detail +{ +//////////////// +// exceptions // +//////////////// + +/*! +@brief general exception of the @ref basic_json class + +This class is an extension of `std::exception` objects with a member @a id for +exception ids. It is used as the base class for all exceptions thrown by the +@ref basic_json class. This class can hence be used as "wildcard" to catch +exceptions. + +Subclasses: +- @ref parse_error for exceptions indicating a parse error +- @ref invalid_iterator for exceptions indicating errors with iterators +- @ref type_error for exceptions indicating executing a member function with + a wrong type +- @ref out_of_range for exceptions indicating access out of the defined range +- @ref other_error for exceptions indicating other library errors + +@internal +@note To have nothrow-copy-constructible exceptions, we internally use + `std::runtime_error` which can cope with arbitrary-length error messages. + Intermediate strings are built with static functions and then passed to + the actual constructor. +@endinternal + +@liveexample{The following code shows how arbitrary library exceptions can be +caught.,exception} + +@since version 3.0.0 +*/ +class exception : public std::exception +{ + public: + /// returns the explanatory string + JSON_HEDLEY_RETURNS_NON_NULL + const char* what() const noexcept override + { + return m.what(); + } + + /// the id of the exception + const int id; + + protected: + JSON_HEDLEY_NON_NULL(3) + exception(int id_, const char* what_arg) : id(id_), m(what_arg) {} + + static std::string name(const std::string& ename, int id_) + { + return "[json.exception." + ename + "." + std::to_string(id_) + "] "; + } + + private: + /// an exception object as storage for error messages + std::runtime_error m; +}; + +/*! +@brief exception indicating a parse error + +This exception is thrown by the library when a parse error occurs. Parse errors +can occur during the deserialization of JSON text, CBOR, MessagePack, as well +as when using JSON Patch. + +Member @a byte holds the byte index of the last read character in the input +file. + +Exceptions have ids 1xx. + +name / id | example message | description +------------------------------ | --------------- | ------------------------- +json.exception.parse_error.101 | parse error at 2: unexpected end of input; expected string literal | This error indicates a syntax error while deserializing a JSON text. The error message describes that an unexpected token (character) was encountered, and the member @a byte indicates the error position. +json.exception.parse_error.102 | parse error at 14: missing or wrong low surrogate | JSON uses the `\uxxxx` format to describe Unicode characters. Code points above above 0xFFFF are split into two `\uxxxx` entries ("surrogate pairs"). This error indicates that the surrogate pair is incomplete or contains an invalid code point. +json.exception.parse_error.103 | parse error: code points above 0x10FFFF are invalid | Unicode supports code points up to 0x10FFFF. Code points above 0x10FFFF are invalid. +json.exception.parse_error.104 | parse error: JSON patch must be an array of objects | [RFC 6902](https://tools.ietf.org/html/rfc6902) requires a JSON Patch document to be a JSON document that represents an array of objects. +json.exception.parse_error.105 | parse error: operation must have string member 'op' | An operation of a JSON Patch document must contain exactly one "op" member, whose value indicates the operation to perform. Its value must be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors. +json.exception.parse_error.106 | parse error: array index '01' must not begin with '0' | An array index in a JSON Pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)) may be `0` or any number without a leading `0`. +json.exception.parse_error.107 | parse error: JSON pointer must be empty or begin with '/' - was: 'foo' | A JSON Pointer must be a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a `/` character. +json.exception.parse_error.108 | parse error: escape character '~' must be followed with '0' or '1' | In a JSON Pointer, only `~0` and `~1` are valid escape sequences. +json.exception.parse_error.109 | parse error: array index 'one' is not a number | A JSON Pointer array index must be a number. +json.exception.parse_error.110 | parse error at 1: cannot read 2 bytes from vector | When parsing CBOR or MessagePack, the byte vector ends before the complete value has been read. +json.exception.parse_error.112 | parse error at 1: error reading CBOR; last byte: 0xF8 | Not all types of CBOR or MessagePack are supported. This exception occurs if an unsupported byte was read. +json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last byte: 0x98 | While parsing a map key, a value that is not a string has been read. +json.exception.parse_error.114 | parse error: Unsupported BSON record type 0x0F | The parsing of the corresponding BSON record type is not implemented (yet). + +@note For an input with n bytes, 1 is the index of the first character and n+1 + is the index of the terminating null byte or the end of file. This also + holds true when reading a byte vector (CBOR or MessagePack). + +@liveexample{The following code shows how a `parse_error` exception can be +caught.,parse_error} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class parse_error : public exception +{ + public: + /*! + @brief create a parse error exception + @param[in] id_ the id of the exception + @param[in] pos the position where the error occurred (or with + chars_read_total=0 if the position cannot be + determined) + @param[in] what_arg the explanatory string + @return parse_error object + */ + static parse_error create(int id_, const position_t& pos, const std::string& what_arg) + { + std::string w = exception::name("parse_error", id_) + "parse error" + + position_string(pos) + ": " + what_arg; + return parse_error(id_, pos.chars_read_total, w.c_str()); + } + + static parse_error create(int id_, std::size_t byte_, const std::string& what_arg) + { + std::string w = exception::name("parse_error", id_) + "parse error" + + (byte_ != 0 ? (" at byte " + std::to_string(byte_)) : "") + + ": " + what_arg; + return parse_error(id_, byte_, w.c_str()); + } + + /*! + @brief byte index of the parse error + + The byte index of the last read character in the input file. + + @note For an input with n bytes, 1 is the index of the first character and + n+1 is the index of the terminating null byte or the end of file. + This also holds true when reading a byte vector (CBOR or MessagePack). + */ + const std::size_t byte; + + private: + parse_error(int id_, std::size_t byte_, const char* what_arg) + : exception(id_, what_arg), byte(byte_) {} + + static std::string position_string(const position_t& pos) + { + return " at line " + std::to_string(pos.lines_read + 1) + + ", column " + std::to_string(pos.chars_read_current_line); + } +}; + +/*! +@brief exception indicating errors with iterators + +This exception is thrown if iterators passed to a library function do not match +the expected semantics. + +Exceptions have ids 2xx. + +name / id | example message | description +----------------------------------- | --------------- | ------------------------- +json.exception.invalid_iterator.201 | iterators are not compatible | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. +json.exception.invalid_iterator.202 | iterator does not fit current value | In an erase or insert function, the passed iterator @a pos does not belong to the JSON value for which the function was called. It hence does not define a valid position for the deletion/insertion. +json.exception.invalid_iterator.203 | iterators do not fit current value | Either iterator passed to function @ref erase(IteratorType first, IteratorType last) does not belong to the JSON value from which values shall be erased. It hence does not define a valid range to delete values from. +json.exception.invalid_iterator.204 | iterators out of range | When an iterator range for a primitive type (number, boolean, or string) is passed to a constructor or an erase function, this range has to be exactly (@ref begin(), @ref end()), because this is the only way the single stored value is expressed. All other ranges are invalid. +json.exception.invalid_iterator.205 | iterator out of range | When an iterator for a primitive type (number, boolean, or string) is passed to an erase function, the iterator has to be the @ref begin() iterator, because it is the only way to address the stored value. All other iterators are invalid. +json.exception.invalid_iterator.206 | cannot construct with iterators from null | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) belong to a JSON null value and hence to not define a valid range. +json.exception.invalid_iterator.207 | cannot use key() for non-object iterators | The key() member function can only be used on iterators belonging to a JSON object, because other types do not have a concept of a key. +json.exception.invalid_iterator.208 | cannot use operator[] for object iterators | The operator[] to specify a concrete offset cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. +json.exception.invalid_iterator.209 | cannot use offsets with object iterators | The offset operators (+, -, +=, -=) cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. +json.exception.invalid_iterator.210 | iterators do not fit | The iterator range passed to the insert function are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. +json.exception.invalid_iterator.211 | passed iterators may not belong to container | The iterator range passed to the insert function must not be a subrange of the container to insert to. +json.exception.invalid_iterator.212 | cannot compare iterators of different containers | When two iterators are compared, they must belong to the same container. +json.exception.invalid_iterator.213 | cannot compare order of object iterators | The order of object iterators cannot be compared, because JSON objects are unordered. +json.exception.invalid_iterator.214 | cannot get value | Cannot get value for iterator: Either the iterator belongs to a null value or it is an iterator to a primitive type (number, boolean, or string), but the iterator is different to @ref begin(). + +@liveexample{The following code shows how an `invalid_iterator` exception can be +caught.,invalid_iterator} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class invalid_iterator : public exception +{ + public: + static invalid_iterator create(int id_, const std::string& what_arg) + { + std::string w = exception::name("invalid_iterator", id_) + what_arg; + return invalid_iterator(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + invalid_iterator(int id_, const char* what_arg) + : exception(id_, what_arg) {} +}; + +/*! +@brief exception indicating executing a member function with a wrong type + +This exception is thrown in case of a type error; that is, a library function is +executed on a JSON value whose type does not match the expected semantics. + +Exceptions have ids 3xx. + +name / id | example message | description +----------------------------- | --------------- | ------------------------- +json.exception.type_error.301 | cannot create object from initializer list | To create an object from an initializer list, the initializer list must consist only of a list of pairs whose first element is a string. When this constraint is violated, an array is created instead. +json.exception.type_error.302 | type must be object, but is array | During implicit or explicit value conversion, the JSON type must be compatible to the target type. For instance, a JSON string can only be converted into string types, but not into numbers or boolean types. +json.exception.type_error.303 | incompatible ReferenceType for get_ref, actual type is object | To retrieve a reference to a value stored in a @ref basic_json object with @ref get_ref, the type of the reference must match the value type. For instance, for a JSON array, the @a ReferenceType must be @ref array_t &. +json.exception.type_error.304 | cannot use at() with string | The @ref at() member functions can only be executed for certain JSON types. +json.exception.type_error.305 | cannot use operator[] with string | The @ref operator[] member functions can only be executed for certain JSON types. +json.exception.type_error.306 | cannot use value() with string | The @ref value() member functions can only be executed for certain JSON types. +json.exception.type_error.307 | cannot use erase() with string | The @ref erase() member functions can only be executed for certain JSON types. +json.exception.type_error.308 | cannot use push_back() with string | The @ref push_back() and @ref operator+= member functions can only be executed for certain JSON types. +json.exception.type_error.309 | cannot use insert() with | The @ref insert() member functions can only be executed for certain JSON types. +json.exception.type_error.310 | cannot use swap() with number | The @ref swap() member functions can only be executed for certain JSON types. +json.exception.type_error.311 | cannot use emplace_back() with string | The @ref emplace_back() member function can only be executed for certain JSON types. +json.exception.type_error.312 | cannot use update() with string | The @ref update() member functions can only be executed for certain JSON types. +json.exception.type_error.313 | invalid value to unflatten | The @ref unflatten function converts an object whose keys are JSON Pointers back into an arbitrary nested JSON value. The JSON Pointers must not overlap, because then the resulting value would not be well defined. +json.exception.type_error.314 | only objects can be unflattened | The @ref unflatten function only works for an object whose keys are JSON Pointers. +json.exception.type_error.315 | values in object must be primitive | The @ref unflatten function only works for an object whose keys are JSON Pointers and whose values are primitive. +json.exception.type_error.316 | invalid UTF-8 byte at index 10: 0x7E | The @ref dump function only works with UTF-8 encoded strings; that is, if you assign a `std::string` to a JSON value, make sure it is UTF-8 encoded. | +json.exception.type_error.317 | JSON value cannot be serialized to requested format | The dynamic type of the object cannot be represented in the requested serialization format (e.g. a raw `true` or `null` JSON object cannot be serialized to BSON) | + +@liveexample{The following code shows how a `type_error` exception can be +caught.,type_error} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class type_error : public exception +{ + public: + static type_error create(int id_, const std::string& what_arg) + { + std::string w = exception::name("type_error", id_) + what_arg; + return type_error(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; + +/*! +@brief exception indicating access out of the defined range + +This exception is thrown in case a library function is called on an input +parameter that exceeds the expected range, for instance in case of array +indices or nonexisting object keys. + +Exceptions have ids 4xx. + +name / id | example message | description +------------------------------- | --------------- | ------------------------- +json.exception.out_of_range.401 | array index 3 is out of range | The provided array index @a i is larger than @a size-1. +json.exception.out_of_range.402 | array index '-' (3) is out of range | The special array index `-` in a JSON Pointer never describes a valid element of the array, but the index past the end. That is, it can only be used to add elements at this position, but not to read it. +json.exception.out_of_range.403 | key 'foo' not found | The provided key was not found in the JSON object. +json.exception.out_of_range.404 | unresolved reference token 'foo' | A reference token in a JSON Pointer could not be resolved. +json.exception.out_of_range.405 | JSON pointer has no parent | The JSON Patch operations 'remove' and 'add' can not be applied to the root element of the JSON value. +json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed number could not be stored as without changing it to NaN or INF. +json.exception.out_of_range.407 | number overflow serializing '9223372036854775808' | UBJSON and BSON only support integer numbers up to 9223372036854775807. | +json.exception.out_of_range.408 | excessive array size: 8658170730974374167 | The size (following `#`) of an UBJSON array or object exceeds the maximal capacity. | +json.exception.out_of_range.409 | BSON key cannot contain code point U+0000 (at byte 2) | Key identifiers to be serialized to BSON cannot contain code point U+0000, since the key is stored as zero-terminated c-string | + +@liveexample{The following code shows how an `out_of_range` exception can be +caught.,out_of_range} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class out_of_range : public exception +{ + public: + static out_of_range create(int id_, const std::string& what_arg) + { + std::string w = exception::name("out_of_range", id_) + what_arg; + return out_of_range(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; + +/*! +@brief exception indicating other library errors + +This exception is thrown in case of errors that cannot be classified with the +other exception types. + +Exceptions have ids 5xx. + +name / id | example message | description +------------------------------ | --------------- | ------------------------- +json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "value":"bar"} | A JSON Patch operation 'test' failed. The unsuccessful operation is also printed. + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range + +@liveexample{The following code shows how an `other_error` exception can be +caught.,other_error} + +@since version 3.0.0 +*/ +class other_error : public exception +{ + public: + static other_error create(int id_, const std::string& what_arg) + { + std::string w = exception::name("other_error", id_) + what_arg; + return other_error(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/input/binary_reader.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/binary_reader.hpp new file mode 100644 index 000000000..1b6e0f9b7 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/binary_reader.hpp @@ -0,0 +1,1983 @@ +#pragma once + +#include // generate_n +#include // array +#include // assert +#include // ldexp +#include // size_t +#include // uint8_t, uint16_t, uint32_t, uint64_t +#include // snprintf +#include // memcpy +#include // back_inserter +#include // numeric_limits +#include // char_traits, string +#include // make_pair, move + +#include +#include +#include +#include +#include +#include + +namespace nlohmann +{ +namespace detail +{ +/////////////////// +// binary reader // +/////////////////// + +/*! +@brief deserialization of CBOR, MessagePack, and UBJSON values +*/ +template> +class binary_reader +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using json_sax_t = SAX; + + public: + /*! + @brief create a binary reader + + @param[in] adapter input adapter to read from + */ + explicit binary_reader(input_adapter_t adapter) : ia(std::move(adapter)) + { + (void)detail::is_sax_static_asserts {}; + assert(ia); + } + + // make class move-only + binary_reader(const binary_reader&) = delete; + binary_reader(binary_reader&&) = default; + binary_reader& operator=(const binary_reader&) = delete; + binary_reader& operator=(binary_reader&&) = default; + ~binary_reader() = default; + + /*! + @param[in] format the binary format to parse + @param[in] sax_ a SAX event processor + @param[in] strict whether to expect the input to be consumed completed + + @return + */ + JSON_HEDLEY_NON_NULL(3) + bool sax_parse(const input_format_t format, + json_sax_t* sax_, + const bool strict = true) + { + sax = sax_; + bool result = false; + + switch (format) + { + case input_format_t::bson: + result = parse_bson_internal(); + break; + + case input_format_t::cbor: + result = parse_cbor_internal(); + break; + + case input_format_t::msgpack: + result = parse_msgpack_internal(); + break; + + case input_format_t::ubjson: + result = parse_ubjson_internal(); + break; + + default: // LCOV_EXCL_LINE + assert(false); // LCOV_EXCL_LINE + } + + // strict mode: next byte must be EOF + if (result and strict) + { + if (format == input_format_t::ubjson) + { + get_ignore_noop(); + } + else + { + get(); + } + + if (JSON_HEDLEY_UNLIKELY(current != std::char_traits::eof())) + { + return sax->parse_error(chars_read, get_token_string(), + parse_error::create(110, chars_read, exception_message(format, "expected end of input; last byte: 0x" + get_token_string(), "value"))); + } + } + + return result; + } + + /*! + @brief determine system byte order + + @return true if and only if system's byte order is little endian + + @note from http://stackoverflow.com/a/1001328/266378 + */ + static constexpr bool little_endianess(int num = 1) noexcept + { + return *reinterpret_cast(&num) == 1; + } + + private: + ////////// + // BSON // + ////////// + + /*! + @brief Reads in a BSON-object and passes it to the SAX-parser. + @return whether a valid BSON-value was passed to the SAX parser + */ + bool parse_bson_internal() + { + std::int32_t document_size; + get_number(input_format_t::bson, document_size); + + if (JSON_HEDLEY_UNLIKELY(not sax->start_object(std::size_t(-1)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(not parse_bson_element_list(/*is_array*/false))) + { + return false; + } + + return sax->end_object(); + } + + /*! + @brief Parses a C-style string from the BSON input. + @param[in, out] result A reference to the string variable where the read + string is to be stored. + @return `true` if the \x00-byte indicating the end of the string was + encountered before the EOF; false` indicates an unexpected EOF. + */ + bool get_bson_cstr(string_t& result) + { + auto out = std::back_inserter(result); + while (true) + { + get(); + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::bson, "cstring"))) + { + return false; + } + if (current == 0x00) + { + return true; + } + *out++ = static_cast(current); + } + + return true; + } + + /*! + @brief Parses a zero-terminated string of length @a len from the BSON + input. + @param[in] len The length (including the zero-byte at the end) of the + string to be read. + @param[in, out] result A reference to the string variable where the read + string is to be stored. + @tparam NumberType The type of the length @a len + @pre len >= 1 + @return `true` if the string was successfully parsed + */ + template + bool get_bson_string(const NumberType len, string_t& result) + { + if (JSON_HEDLEY_UNLIKELY(len < 1)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::bson, "string length must be at least 1, is " + std::to_string(len), "string"))); + } + + return get_string(input_format_t::bson, len - static_cast(1), result) and get() != std::char_traits::eof(); + } + + /*! + @brief Read a BSON document element of the given @a element_type. + @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html + @param[in] element_type_parse_position The position in the input stream, + where the `element_type` was read. + @warning Not all BSON element types are supported yet. An unsupported + @a element_type will give rise to a parse_error.114: + Unsupported BSON record type 0x... + @return whether a valid BSON-object/array was passed to the SAX parser + */ + bool parse_bson_element_internal(const int element_type, + const std::size_t element_type_parse_position) + { + switch (element_type) + { + case 0x01: // double + { + double number; + return get_number(input_format_t::bson, number) and sax->number_float(static_cast(number), ""); + } + + case 0x02: // string + { + std::int32_t len; + string_t value; + return get_number(input_format_t::bson, len) and get_bson_string(len, value) and sax->string(value); + } + + case 0x03: // object + { + return parse_bson_internal(); + } + + case 0x04: // array + { + return parse_bson_array(); + } + + case 0x08: // boolean + { + return sax->boolean(get() != 0); + } + + case 0x0A: // null + { + return sax->null(); + } + + case 0x10: // int32 + { + std::int32_t value; + return get_number(input_format_t::bson, value) and sax->number_integer(value); + } + + case 0x12: // int64 + { + std::int64_t value; + return get_number(input_format_t::bson, value) and sax->number_integer(value); + } + + default: // anything else not supported (yet) + { + std::array cr{{}}; + (std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(element_type)); + return sax->parse_error(element_type_parse_position, std::string(cr.data()), parse_error::create(114, element_type_parse_position, "Unsupported BSON record type 0x" + std::string(cr.data()))); + } + } + } + + /*! + @brief Read a BSON element list (as specified in the BSON-spec) + + The same binary layout is used for objects and arrays, hence it must be + indicated with the argument @a is_array which one is expected + (true --> array, false --> object). + + @param[in] is_array Determines if the element list being read is to be + treated as an object (@a is_array == false), or as an + array (@a is_array == true). + @return whether a valid BSON-object/array was passed to the SAX parser + */ + bool parse_bson_element_list(const bool is_array) + { + string_t key; + while (int element_type = get()) + { + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::bson, "element list"))) + { + return false; + } + + const std::size_t element_type_parse_position = chars_read; + if (JSON_HEDLEY_UNLIKELY(not get_bson_cstr(key))) + { + return false; + } + + if (not is_array and not sax->key(key)) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(not parse_bson_element_internal(element_type, element_type_parse_position))) + { + return false; + } + + // get_bson_cstr only appends + key.clear(); + } + + return true; + } + + /*! + @brief Reads an array from the BSON input and passes it to the SAX-parser. + @return whether a valid BSON-array was passed to the SAX parser + */ + bool parse_bson_array() + { + std::int32_t document_size; + get_number(input_format_t::bson, document_size); + + if (JSON_HEDLEY_UNLIKELY(not sax->start_array(std::size_t(-1)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(not parse_bson_element_list(/*is_array*/true))) + { + return false; + } + + return sax->end_array(); + } + + ////////// + // CBOR // + ////////// + + /*! + @param[in] get_char whether a new character should be retrieved from the + input (true, default) or whether the last read + character should be considered instead + + @return whether a valid CBOR value was passed to the SAX parser + */ + bool parse_cbor_internal(const bool get_char = true) + { + switch (get_char ? get() : current) + { + // EOF + case std::char_traits::eof(): + return unexpect_eof(input_format_t::cbor, "value"); + + // Integer 0x00..0x17 (0..23) + case 0x00: + case 0x01: + case 0x02: + case 0x03: + case 0x04: + case 0x05: + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + return sax->number_unsigned(static_cast(current)); + + case 0x18: // Unsigned integer (one-byte uint8_t follows) + { + std::uint8_t number; + return get_number(input_format_t::cbor, number) and sax->number_unsigned(number); + } + + case 0x19: // Unsigned integer (two-byte uint16_t follows) + { + std::uint16_t number; + return get_number(input_format_t::cbor, number) and sax->number_unsigned(number); + } + + case 0x1A: // Unsigned integer (four-byte uint32_t follows) + { + std::uint32_t number; + return get_number(input_format_t::cbor, number) and sax->number_unsigned(number); + } + + case 0x1B: // Unsigned integer (eight-byte uint64_t follows) + { + std::uint64_t number; + return get_number(input_format_t::cbor, number) and sax->number_unsigned(number); + } + + // Negative integer -1-0x00..-1-0x17 (-1..-24) + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + return sax->number_integer(static_cast(0x20 - 1 - current)); + + case 0x38: // Negative integer (one-byte uint8_t follows) + { + std::uint8_t number; + return get_number(input_format_t::cbor, number) and sax->number_integer(static_cast(-1) - number); + } + + case 0x39: // Negative integer -1-n (two-byte uint16_t follows) + { + std::uint16_t number; + return get_number(input_format_t::cbor, number) and sax->number_integer(static_cast(-1) - number); + } + + case 0x3A: // Negative integer -1-n (four-byte uint32_t follows) + { + std::uint32_t number; + return get_number(input_format_t::cbor, number) and sax->number_integer(static_cast(-1) - number); + } + + case 0x3B: // Negative integer -1-n (eight-byte uint64_t follows) + { + std::uint64_t number; + return get_number(input_format_t::cbor, number) and sax->number_integer(static_cast(-1) + - static_cast(number)); + } + + // UTF-8 string (0x00..0x17 bytes follow) + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: // UTF-8 string (one-byte uint8_t for n follows) + case 0x79: // UTF-8 string (two-byte uint16_t for n follow) + case 0x7A: // UTF-8 string (four-byte uint32_t for n follow) + case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow) + case 0x7F: // UTF-8 string (indefinite length) + { + string_t s; + return get_cbor_string(s) and sax->string(s); + } + + // array (0x00..0x17 data items follow) + case 0x80: + case 0x81: + case 0x82: + case 0x83: + case 0x84: + case 0x85: + case 0x86: + case 0x87: + case 0x88: + case 0x89: + case 0x8A: + case 0x8B: + case 0x8C: + case 0x8D: + case 0x8E: + case 0x8F: + case 0x90: + case 0x91: + case 0x92: + case 0x93: + case 0x94: + case 0x95: + case 0x96: + case 0x97: + return get_cbor_array(static_cast(static_cast(current) & 0x1Fu)); + + case 0x98: // array (one-byte uint8_t for n follows) + { + std::uint8_t len; + return get_number(input_format_t::cbor, len) and get_cbor_array(static_cast(len)); + } + + case 0x99: // array (two-byte uint16_t for n follow) + { + std::uint16_t len; + return get_number(input_format_t::cbor, len) and get_cbor_array(static_cast(len)); + } + + case 0x9A: // array (four-byte uint32_t for n follow) + { + std::uint32_t len; + return get_number(input_format_t::cbor, len) and get_cbor_array(static_cast(len)); + } + + case 0x9B: // array (eight-byte uint64_t for n follow) + { + std::uint64_t len; + return get_number(input_format_t::cbor, len) and get_cbor_array(static_cast(len)); + } + + case 0x9F: // array (indefinite length) + return get_cbor_array(std::size_t(-1)); + + // map (0x00..0x17 pairs of data items follow) + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB0: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + return get_cbor_object(static_cast(static_cast(current) & 0x1Fu)); + + case 0xB8: // map (one-byte uint8_t for n follows) + { + std::uint8_t len; + return get_number(input_format_t::cbor, len) and get_cbor_object(static_cast(len)); + } + + case 0xB9: // map (two-byte uint16_t for n follow) + { + std::uint16_t len; + return get_number(input_format_t::cbor, len) and get_cbor_object(static_cast(len)); + } + + case 0xBA: // map (four-byte uint32_t for n follow) + { + std::uint32_t len; + return get_number(input_format_t::cbor, len) and get_cbor_object(static_cast(len)); + } + + case 0xBB: // map (eight-byte uint64_t for n follow) + { + std::uint64_t len; + return get_number(input_format_t::cbor, len) and get_cbor_object(static_cast(len)); + } + + case 0xBF: // map (indefinite length) + return get_cbor_object(std::size_t(-1)); + + case 0xF4: // false + return sax->boolean(false); + + case 0xF5: // true + return sax->boolean(true); + + case 0xF6: // null + return sax->null(); + + case 0xF9: // Half-Precision Float (two-byte IEEE 754) + { + const int byte1_raw = get(); + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::cbor, "number"))) + { + return false; + } + const int byte2_raw = get(); + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::cbor, "number"))) + { + return false; + } + + const auto byte1 = static_cast(byte1_raw); + const auto byte2 = static_cast(byte2_raw); + + // code from RFC 7049, Appendix D, Figure 3: + // As half-precision floating-point numbers were only added + // to IEEE 754 in 2008, today's programming platforms often + // still only have limited support for them. It is very + // easy to include at least decoding support for them even + // without such support. An example of a small decoder for + // half-precision floating-point numbers in the C language + // is shown in Fig. 3. + const auto half = static_cast((byte1 << 8u) + byte2); + const double val = [&half] + { + const int exp = (half >> 10u) & 0x1Fu; + const unsigned int mant = half & 0x3FFu; + assert(0 <= exp and exp <= 32); + assert(mant <= 1024); + switch (exp) + { + case 0: + return std::ldexp(mant, -24); + case 31: + return (mant == 0) + ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + default: + return std::ldexp(mant + 1024, exp - 25); + } + }(); + return sax->number_float((half & 0x8000u) != 0 + ? static_cast(-val) + : static_cast(val), ""); + } + + case 0xFA: // Single-Precision Float (four-byte IEEE 754) + { + float number; + return get_number(input_format_t::cbor, number) and sax->number_float(static_cast(number), ""); + } + + case 0xFB: // Double-Precision Float (eight-byte IEEE 754) + { + double number; + return get_number(input_format_t::cbor, number) and sax->number_float(static_cast(number), ""); + } + + default: // anything else (0xFF is handled inside the other types) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::cbor, "invalid byte: 0x" + last_token, "value"))); + } + } + } + + /*! + @brief reads a CBOR string + + This function first reads starting bytes to determine the expected + string length and then copies this number of bytes into a string. + Additionally, CBOR's strings with indefinite lengths are supported. + + @param[out] result created string + + @return whether string creation completed + */ + bool get_cbor_string(string_t& result) + { + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::cbor, "string"))) + { + return false; + } + + switch (current) + { + // UTF-8 string (0x00..0x17 bytes follow) + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + { + return get_string(input_format_t::cbor, static_cast(current) & 0x1Fu, result); + } + + case 0x78: // UTF-8 string (one-byte uint8_t for n follows) + { + std::uint8_t len; + return get_number(input_format_t::cbor, len) and get_string(input_format_t::cbor, len, result); + } + + case 0x79: // UTF-8 string (two-byte uint16_t for n follow) + { + std::uint16_t len; + return get_number(input_format_t::cbor, len) and get_string(input_format_t::cbor, len, result); + } + + case 0x7A: // UTF-8 string (four-byte uint32_t for n follow) + { + std::uint32_t len; + return get_number(input_format_t::cbor, len) and get_string(input_format_t::cbor, len, result); + } + + case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow) + { + std::uint64_t len; + return get_number(input_format_t::cbor, len) and get_string(input_format_t::cbor, len, result); + } + + case 0x7F: // UTF-8 string (indefinite length) + { + while (get() != 0xFF) + { + string_t chunk; + if (not get_cbor_string(chunk)) + { + return false; + } + result.append(chunk); + } + return true; + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::cbor, "expected length specification (0x60-0x7B) or indefinite string type (0x7F); last byte: 0x" + last_token, "string"))); + } + } + } + + /*! + @param[in] len the length of the array or std::size_t(-1) for an + array of indefinite size + @return whether array creation completed + */ + bool get_cbor_array(const std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_array(len))) + { + return false; + } + + if (len != std::size_t(-1)) + { + for (std::size_t i = 0; i < len; ++i) + { + if (JSON_HEDLEY_UNLIKELY(not parse_cbor_internal())) + { + return false; + } + } + } + else + { + while (get() != 0xFF) + { + if (JSON_HEDLEY_UNLIKELY(not parse_cbor_internal(false))) + { + return false; + } + } + } + + return sax->end_array(); + } + + /*! + @param[in] len the length of the object or std::size_t(-1) for an + object of indefinite size + @return whether object creation completed + */ + bool get_cbor_object(const std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_object(len))) + { + return false; + } + + string_t key; + if (len != std::size_t(-1)) + { + for (std::size_t i = 0; i < len; ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(not get_cbor_string(key) or not sax->key(key))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(not parse_cbor_internal())) + { + return false; + } + key.clear(); + } + } + else + { + while (get() != 0xFF) + { + if (JSON_HEDLEY_UNLIKELY(not get_cbor_string(key) or not sax->key(key))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(not parse_cbor_internal())) + { + return false; + } + key.clear(); + } + } + + return sax->end_object(); + } + + ///////////// + // MsgPack // + ///////////// + + /*! + @return whether a valid MessagePack value was passed to the SAX parser + */ + bool parse_msgpack_internal() + { + switch (get()) + { + // EOF + case std::char_traits::eof(): + return unexpect_eof(input_format_t::msgpack, "value"); + + // positive fixint + case 0x00: + case 0x01: + case 0x02: + case 0x03: + case 0x04: + case 0x05: + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: + case 0x19: + case 0x1A: + case 0x1B: + case 0x1C: + case 0x1D: + case 0x1E: + case 0x1F: + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + case 0x4C: + case 0x4D: + case 0x4E: + case 0x4F: + case 0x50: + case 0x51: + case 0x52: + case 0x53: + case 0x54: + case 0x55: + case 0x56: + case 0x57: + case 0x58: + case 0x59: + case 0x5A: + case 0x5B: + case 0x5C: + case 0x5D: + case 0x5E: + case 0x5F: + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: + case 0x79: + case 0x7A: + case 0x7B: + case 0x7C: + case 0x7D: + case 0x7E: + case 0x7F: + return sax->number_unsigned(static_cast(current)); + + // fixmap + case 0x80: + case 0x81: + case 0x82: + case 0x83: + case 0x84: + case 0x85: + case 0x86: + case 0x87: + case 0x88: + case 0x89: + case 0x8A: + case 0x8B: + case 0x8C: + case 0x8D: + case 0x8E: + case 0x8F: + return get_msgpack_object(static_cast(static_cast(current) & 0x0Fu)); + + // fixarray + case 0x90: + case 0x91: + case 0x92: + case 0x93: + case 0x94: + case 0x95: + case 0x96: + case 0x97: + case 0x98: + case 0x99: + case 0x9A: + case 0x9B: + case 0x9C: + case 0x9D: + case 0x9E: + case 0x9F: + return get_msgpack_array(static_cast(static_cast(current) & 0x0Fu)); + + // fixstr + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB0: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBC: + case 0xBD: + case 0xBE: + case 0xBF: + case 0xD9: // str 8 + case 0xDA: // str 16 + case 0xDB: // str 32 + { + string_t s; + return get_msgpack_string(s) and sax->string(s); + } + + case 0xC0: // nil + return sax->null(); + + case 0xC2: // false + return sax->boolean(false); + + case 0xC3: // true + return sax->boolean(true); + + case 0xCA: // float 32 + { + float number; + return get_number(input_format_t::msgpack, number) and sax->number_float(static_cast(number), ""); + } + + case 0xCB: // float 64 + { + double number; + return get_number(input_format_t::msgpack, number) and sax->number_float(static_cast(number), ""); + } + + case 0xCC: // uint 8 + { + std::uint8_t number; + return get_number(input_format_t::msgpack, number) and sax->number_unsigned(number); + } + + case 0xCD: // uint 16 + { + std::uint16_t number; + return get_number(input_format_t::msgpack, number) and sax->number_unsigned(number); + } + + case 0xCE: // uint 32 + { + std::uint32_t number; + return get_number(input_format_t::msgpack, number) and sax->number_unsigned(number); + } + + case 0xCF: // uint 64 + { + std::uint64_t number; + return get_number(input_format_t::msgpack, number) and sax->number_unsigned(number); + } + + case 0xD0: // int 8 + { + std::int8_t number; + return get_number(input_format_t::msgpack, number) and sax->number_integer(number); + } + + case 0xD1: // int 16 + { + std::int16_t number; + return get_number(input_format_t::msgpack, number) and sax->number_integer(number); + } + + case 0xD2: // int 32 + { + std::int32_t number; + return get_number(input_format_t::msgpack, number) and sax->number_integer(number); + } + + case 0xD3: // int 64 + { + std::int64_t number; + return get_number(input_format_t::msgpack, number) and sax->number_integer(number); + } + + case 0xDC: // array 16 + { + std::uint16_t len; + return get_number(input_format_t::msgpack, len) and get_msgpack_array(static_cast(len)); + } + + case 0xDD: // array 32 + { + std::uint32_t len; + return get_number(input_format_t::msgpack, len) and get_msgpack_array(static_cast(len)); + } + + case 0xDE: // map 16 + { + std::uint16_t len; + return get_number(input_format_t::msgpack, len) and get_msgpack_object(static_cast(len)); + } + + case 0xDF: // map 32 + { + std::uint32_t len; + return get_number(input_format_t::msgpack, len) and get_msgpack_object(static_cast(len)); + } + + // negative fixint + case 0xE0: + case 0xE1: + case 0xE2: + case 0xE3: + case 0xE4: + case 0xE5: + case 0xE6: + case 0xE7: + case 0xE8: + case 0xE9: + case 0xEA: + case 0xEB: + case 0xEC: + case 0xED: + case 0xEE: + case 0xEF: + case 0xF0: + case 0xF1: + case 0xF2: + case 0xF3: + case 0xF4: + case 0xF5: + case 0xF6: + case 0xF7: + case 0xF8: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: + case 0xFE: + case 0xFF: + return sax->number_integer(static_cast(current)); + + default: // anything else + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::msgpack, "invalid byte: 0x" + last_token, "value"))); + } + } + } + + /*! + @brief reads a MessagePack string + + This function first reads starting bytes to determine the expected + string length and then copies this number of bytes into a string. + + @param[out] result created string + + @return whether string creation completed + */ + bool get_msgpack_string(string_t& result) + { + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::msgpack, "string"))) + { + return false; + } + + switch (current) + { + // fixstr + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB0: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBC: + case 0xBD: + case 0xBE: + case 0xBF: + { + return get_string(input_format_t::msgpack, static_cast(current) & 0x1Fu, result); + } + + case 0xD9: // str 8 + { + std::uint8_t len; + return get_number(input_format_t::msgpack, len) and get_string(input_format_t::msgpack, len, result); + } + + case 0xDA: // str 16 + { + std::uint16_t len; + return get_number(input_format_t::msgpack, len) and get_string(input_format_t::msgpack, len, result); + } + + case 0xDB: // str 32 + { + std::uint32_t len; + return get_number(input_format_t::msgpack, len) and get_string(input_format_t::msgpack, len, result); + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::msgpack, "expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x" + last_token, "string"))); + } + } + } + + /*! + @param[in] len the length of the array + @return whether array creation completed + */ + bool get_msgpack_array(const std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_array(len))) + { + return false; + } + + for (std::size_t i = 0; i < len; ++i) + { + if (JSON_HEDLEY_UNLIKELY(not parse_msgpack_internal())) + { + return false; + } + } + + return sax->end_array(); + } + + /*! + @param[in] len the length of the object + @return whether object creation completed + */ + bool get_msgpack_object(const std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_object(len))) + { + return false; + } + + string_t key; + for (std::size_t i = 0; i < len; ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(not get_msgpack_string(key) or not sax->key(key))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(not parse_msgpack_internal())) + { + return false; + } + key.clear(); + } + + return sax->end_object(); + } + + //////////// + // UBJSON // + //////////// + + /*! + @param[in] get_char whether a new character should be retrieved from the + input (true, default) or whether the last read + character should be considered instead + + @return whether a valid UBJSON value was passed to the SAX parser + */ + bool parse_ubjson_internal(const bool get_char = true) + { + return get_ubjson_value(get_char ? get_ignore_noop() : current); + } + + /*! + @brief reads a UBJSON string + + This function is either called after reading the 'S' byte explicitly + indicating a string, or in case of an object key where the 'S' byte can be + left out. + + @param[out] result created string + @param[in] get_char whether a new character should be retrieved from the + input (true, default) or whether the last read + character should be considered instead + + @return whether string creation completed + */ + bool get_ubjson_string(string_t& result, const bool get_char = true) + { + if (get_char) + { + get(); // TODO(niels): may we ignore N here? + } + + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::ubjson, "value"))) + { + return false; + } + + switch (current) + { + case 'U': + { + std::uint8_t len; + return get_number(input_format_t::ubjson, len) and get_string(input_format_t::ubjson, len, result); + } + + case 'i': + { + std::int8_t len; + return get_number(input_format_t::ubjson, len) and get_string(input_format_t::ubjson, len, result); + } + + case 'I': + { + std::int16_t len; + return get_number(input_format_t::ubjson, len) and get_string(input_format_t::ubjson, len, result); + } + + case 'l': + { + std::int32_t len; + return get_number(input_format_t::ubjson, len) and get_string(input_format_t::ubjson, len, result); + } + + case 'L': + { + std::int64_t len; + return get_number(input_format_t::ubjson, len) and get_string(input_format_t::ubjson, len, result); + } + + default: + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "expected length type specification (U, i, I, l, L); last byte: 0x" + last_token, "string"))); + } + } + + /*! + @param[out] result determined size + @return whether size determination completed + */ + bool get_ubjson_size_value(std::size_t& result) + { + switch (get_ignore_noop()) + { + case 'U': + { + std::uint8_t number; + if (JSON_HEDLEY_UNLIKELY(not get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'i': + { + std::int8_t number; + if (JSON_HEDLEY_UNLIKELY(not get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'I': + { + std::int16_t number; + if (JSON_HEDLEY_UNLIKELY(not get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'l': + { + std::int32_t number; + if (JSON_HEDLEY_UNLIKELY(not get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'L': + { + std::int64_t number; + if (JSON_HEDLEY_UNLIKELY(not get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "expected length type specification (U, i, I, l, L) after '#'; last byte: 0x" + last_token, "size"))); + } + } + } + + /*! + @brief determine the type and size for a container + + In the optimized UBJSON format, a type and a size can be provided to allow + for a more compact representation. + + @param[out] result pair of the size and the type + + @return whether pair creation completed + */ + bool get_ubjson_size_type(std::pair& result) + { + result.first = string_t::npos; // size + result.second = 0; // type + + get_ignore_noop(); + + if (current == '$') + { + result.second = get(); // must not ignore 'N', because 'N' maybe the type + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::ubjson, "type"))) + { + return false; + } + + get_ignore_noop(); + if (JSON_HEDLEY_UNLIKELY(current != '#')) + { + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::ubjson, "value"))) + { + return false; + } + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::ubjson, "expected '#' after type information; last byte: 0x" + last_token, "size"))); + } + + return get_ubjson_size_value(result.first); + } + + if (current == '#') + { + return get_ubjson_size_value(result.first); + } + + return true; + } + + /*! + @param prefix the previously read or set type prefix + @return whether value creation completed + */ + bool get_ubjson_value(const int prefix) + { + switch (prefix) + { + case std::char_traits::eof(): // EOF + return unexpect_eof(input_format_t::ubjson, "value"); + + case 'T': // true + return sax->boolean(true); + case 'F': // false + return sax->boolean(false); + + case 'Z': // null + return sax->null(); + + case 'U': + { + std::uint8_t number; + return get_number(input_format_t::ubjson, number) and sax->number_unsigned(number); + } + + case 'i': + { + std::int8_t number; + return get_number(input_format_t::ubjson, number) and sax->number_integer(number); + } + + case 'I': + { + std::int16_t number; + return get_number(input_format_t::ubjson, number) and sax->number_integer(number); + } + + case 'l': + { + std::int32_t number; + return get_number(input_format_t::ubjson, number) and sax->number_integer(number); + } + + case 'L': + { + std::int64_t number; + return get_number(input_format_t::ubjson, number) and sax->number_integer(number); + } + + case 'd': + { + float number; + return get_number(input_format_t::ubjson, number) and sax->number_float(static_cast(number), ""); + } + + case 'D': + { + double number; + return get_number(input_format_t::ubjson, number) and sax->number_float(static_cast(number), ""); + } + + case 'C': // char + { + get(); + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(input_format_t::ubjson, "char"))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(current > 127)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "byte after 'C' must be in range 0x00..0x7F; last byte: 0x" + last_token, "char"))); + } + string_t s(1, static_cast(current)); + return sax->string(s); + } + + case 'S': // string + { + string_t s; + return get_ubjson_string(s) and sax->string(s); + } + + case '[': // array + return get_ubjson_array(); + + case '{': // object + return get_ubjson_object(); + + default: // anything else + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::ubjson, "invalid byte: 0x" + last_token, "value"))); + } + } + } + + /*! + @return whether array creation completed + */ + bool get_ubjson_array() + { + std::pair size_and_type; + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_size_type(size_and_type))) + { + return false; + } + + if (size_and_type.first != string_t::npos) + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_array(size_and_type.first))) + { + return false; + } + + if (size_and_type.second != 0) + { + if (size_and_type.second != 'N') + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_value(size_and_type.second))) + { + return false; + } + } + } + } + else + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(not parse_ubjson_internal())) + { + return false; + } + } + } + } + else + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_array(std::size_t(-1)))) + { + return false; + } + + while (current != ']') + { + if (JSON_HEDLEY_UNLIKELY(not parse_ubjson_internal(false))) + { + return false; + } + get_ignore_noop(); + } + } + + return sax->end_array(); + } + + /*! + @return whether object creation completed + */ + bool get_ubjson_object() + { + std::pair size_and_type; + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_size_type(size_and_type))) + { + return false; + } + + string_t key; + if (size_and_type.first != string_t::npos) + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_object(size_and_type.first))) + { + return false; + } + + if (size_and_type.second != 0) + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_string(key) or not sax->key(key))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_value(size_and_type.second))) + { + return false; + } + key.clear(); + } + } + else + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_string(key) or not sax->key(key))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(not parse_ubjson_internal())) + { + return false; + } + key.clear(); + } + } + } + else + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_object(std::size_t(-1)))) + { + return false; + } + + while (current != '}') + { + if (JSON_HEDLEY_UNLIKELY(not get_ubjson_string(key, false) or not sax->key(key))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(not parse_ubjson_internal())) + { + return false; + } + get_ignore_noop(); + key.clear(); + } + } + + return sax->end_object(); + } + + /////////////////////// + // Utility functions // + /////////////////////// + + /*! + @brief get next character from the input + + This function provides the interface to the used input adapter. It does + not throw in case the input reached EOF, but returns a -'ve valued + `std::char_traits::eof()` in that case. + + @return character read from the input + */ + int get() + { + ++chars_read; + return current = ia->get_character(); + } + + /*! + @return character read from the input after ignoring all 'N' entries + */ + int get_ignore_noop() + { + do + { + get(); + } + while (current == 'N'); + + return current; + } + + /* + @brief read a number from the input + + @tparam NumberType the type of the number + @param[in] format the current format (for diagnostics) + @param[out] result number of type @a NumberType + + @return whether conversion completed + + @note This function needs to respect the system's endianess, because + bytes in CBOR, MessagePack, and UBJSON are stored in network order + (big endian) and therefore need reordering on little endian systems. + */ + template + bool get_number(const input_format_t format, NumberType& result) + { + // step 1: read input into array with system's byte order + std::array vec; + for (std::size_t i = 0; i < sizeof(NumberType); ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(format, "number"))) + { + return false; + } + + // reverse byte order prior to conversion if necessary + if (is_little_endian != InputIsLittleEndian) + { + vec[sizeof(NumberType) - i - 1] = static_cast(current); + } + else + { + vec[i] = static_cast(current); // LCOV_EXCL_LINE + } + } + + // step 2: convert array into number of type T and return + std::memcpy(&result, vec.data(), sizeof(NumberType)); + return true; + } + + /*! + @brief create a string by reading characters from the input + + @tparam NumberType the type of the number + @param[in] format the current format (for diagnostics) + @param[in] len number of characters to read + @param[out] result string created by reading @a len bytes + + @return whether string creation completed + + @note We can not reserve @a len bytes for the result, because @a len + may be too large. Usually, @ref unexpect_eof() detects the end of + the input before we run out of string memory. + */ + template + bool get_string(const input_format_t format, + const NumberType len, + string_t& result) + { + bool success = true; + std::generate_n(std::back_inserter(result), len, [this, &success, &format]() + { + get(); + if (JSON_HEDLEY_UNLIKELY(not unexpect_eof(format, "string"))) + { + success = false; + } + return static_cast(current); + }); + return success; + } + + /*! + @param[in] format the current format (for diagnostics) + @param[in] context further context information (for diagnostics) + @return whether the last read character is not EOF + */ + JSON_HEDLEY_NON_NULL(3) + bool unexpect_eof(const input_format_t format, const char* context) const + { + if (JSON_HEDLEY_UNLIKELY(current == std::char_traits::eof())) + { + return sax->parse_error(chars_read, "", + parse_error::create(110, chars_read, exception_message(format, "unexpected end of input", context))); + } + return true; + } + + /*! + @return a string representation of the last read byte + */ + std::string get_token_string() const + { + std::array cr{{}}; + (std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(current)); + return std::string{cr.data()}; + } + + /*! + @param[in] format the current format + @param[in] detail a detailed error message + @param[in] context further context information + @return a message string to use in the parse_error exceptions + */ + std::string exception_message(const input_format_t format, + const std::string& detail, + const std::string& context) const + { + std::string error_msg = "syntax error while parsing "; + + switch (format) + { + case input_format_t::cbor: + error_msg += "CBOR"; + break; + + case input_format_t::msgpack: + error_msg += "MessagePack"; + break; + + case input_format_t::ubjson: + error_msg += "UBJSON"; + break; + + case input_format_t::bson: + error_msg += "BSON"; + break; + + default: // LCOV_EXCL_LINE + assert(false); // LCOV_EXCL_LINE + } + + return error_msg + " " + context + ": " + detail; + } + + private: + /// input adapter + input_adapter_t ia = nullptr; + + /// the current character + int current = std::char_traits::eof(); + + /// the number of characters read + std::size_t chars_read = 0; + + /// whether we can assume little endianess + const bool is_little_endian = little_endianess(); + + /// the SAX parser + json_sax_t* sax = nullptr; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/input/input_adapters.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/input_adapters.hpp new file mode 100644 index 000000000..9512a771e --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/input_adapters.hpp @@ -0,0 +1,442 @@ +#pragma once + +#include // array +#include // assert +#include // size_t +#include //FILE * +#include // strlen +#include // istream +#include // begin, end, iterator_traits, random_access_iterator_tag, distance, next +#include // shared_ptr, make_shared, addressof +#include // accumulate +#include // string, char_traits +#include // enable_if, is_base_of, is_pointer, is_integral, remove_pointer +#include // pair, declval + +#include +#include + +namespace nlohmann +{ +namespace detail +{ +/// the supported input formats +enum class input_format_t { json, cbor, msgpack, ubjson, bson }; + +//////////////////// +// input adapters // +//////////////////// + +/*! +@brief abstract input adapter interface + +Produces a stream of std::char_traits::int_type characters from a +std::istream, a buffer, or some other input type. Accepts the return of +exactly one non-EOF character for future input. The int_type characters +returned consist of all valid char values as positive values (typically +unsigned char), plus an EOF value outside that range, specified by the value +of the function std::char_traits::eof(). This value is typically -1, but +could be any arbitrary value which is not a valid char value. +*/ +struct input_adapter_protocol +{ + /// get a character [0,255] or std::char_traits::eof(). + virtual std::char_traits::int_type get_character() = 0; + virtual ~input_adapter_protocol() = default; +}; + +/// a type to simplify interfaces +using input_adapter_t = std::shared_ptr; + +/*! +Input adapter for stdio file access. This adapter read only 1 byte and do not use any + buffer. This adapter is a very low level adapter. +*/ +class file_input_adapter : public input_adapter_protocol +{ + public: + JSON_HEDLEY_NON_NULL(2) + explicit file_input_adapter(std::FILE* f) noexcept + : m_file(f) + {} + + // make class move-only + file_input_adapter(const file_input_adapter&) = delete; + file_input_adapter(file_input_adapter&&) = default; + file_input_adapter& operator=(const file_input_adapter&) = delete; + file_input_adapter& operator=(file_input_adapter&&) = default; + ~file_input_adapter() override = default; + + std::char_traits::int_type get_character() noexcept override + { + return std::fgetc(m_file); + } + + private: + /// the file pointer to read from + std::FILE* m_file; +}; + + +/*! +Input adapter for a (caching) istream. Ignores a UFT Byte Order Mark at +beginning of input. Does not support changing the underlying std::streambuf +in mid-input. Maintains underlying std::istream and std::streambuf to support +subsequent use of standard std::istream operations to process any input +characters following those used in parsing the JSON input. Clears the +std::istream flags; any input errors (e.g., EOF) will be detected by the first +subsequent call for input from the std::istream. +*/ +class input_stream_adapter : public input_adapter_protocol +{ + public: + ~input_stream_adapter() override + { + // clear stream flags; we use underlying streambuf I/O, do not + // maintain ifstream flags, except eof + is.clear(is.rdstate() & std::ios::eofbit); + } + + explicit input_stream_adapter(std::istream& i) + : is(i), sb(*i.rdbuf()) + {} + + // delete because of pointer members + input_stream_adapter(const input_stream_adapter&) = delete; + input_stream_adapter& operator=(input_stream_adapter&) = delete; + input_stream_adapter(input_stream_adapter&&) = delete; + input_stream_adapter& operator=(input_stream_adapter&&) = delete; + + // std::istream/std::streambuf use std::char_traits::to_int_type, to + // ensure that std::char_traits::eof() and the character 0xFF do not + // end up as the same value, eg. 0xFFFFFFFF. + std::char_traits::int_type get_character() override + { + auto res = sb.sbumpc(); + // set eof manually, as we don't use the istream interface. + if (res == EOF) + { + is.clear(is.rdstate() | std::ios::eofbit); + } + return res; + } + + private: + /// the associated input stream + std::istream& is; + std::streambuf& sb; +}; + +/// input adapter for buffer input +class input_buffer_adapter : public input_adapter_protocol +{ + public: + input_buffer_adapter(const char* b, const std::size_t l) noexcept + : cursor(b), limit(b == nullptr ? nullptr : (b + l)) + {} + + // delete because of pointer members + input_buffer_adapter(const input_buffer_adapter&) = delete; + input_buffer_adapter& operator=(input_buffer_adapter&) = delete; + input_buffer_adapter(input_buffer_adapter&&) = delete; + input_buffer_adapter& operator=(input_buffer_adapter&&) = delete; + ~input_buffer_adapter() override = default; + + std::char_traits::int_type get_character() noexcept override + { + if (JSON_HEDLEY_LIKELY(cursor < limit)) + { + assert(cursor != nullptr and limit != nullptr); + return std::char_traits::to_int_type(*(cursor++)); + } + + return std::char_traits::eof(); + } + + private: + /// pointer to the current character + const char* cursor; + /// pointer past the last character + const char* const limit; +}; + +template +struct wide_string_input_helper +{ + // UTF-32 + static void fill_buffer(const WideStringType& str, + size_t& current_wchar, + std::array::int_type, 4>& utf8_bytes, + size_t& utf8_bytes_index, + size_t& utf8_bytes_filled) + { + utf8_bytes_index = 0; + + if (current_wchar == str.size()) + { + utf8_bytes[0] = std::char_traits::eof(); + utf8_bytes_filled = 1; + } + else + { + // get the current character + const auto wc = static_cast(str[current_wchar++]); + + // UTF-32 to UTF-8 encoding + if (wc < 0x80) + { + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + else if (wc <= 0x7FF) + { + utf8_bytes[0] = static_cast::int_type>(0xC0u | ((wc >> 6u) & 0x1Fu)); + utf8_bytes[1] = static_cast::int_type>(0x80u | (wc & 0x3Fu)); + utf8_bytes_filled = 2; + } + else if (wc <= 0xFFFF) + { + utf8_bytes[0] = static_cast::int_type>(0xE0u | ((wc >> 12u) & 0x0Fu)); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((wc >> 6u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | (wc & 0x3Fu)); + utf8_bytes_filled = 3; + } + else if (wc <= 0x10FFFF) + { + utf8_bytes[0] = static_cast::int_type>(0xF0u | ((wc >> 18u) & 0x07u)); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((wc >> 12u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | ((wc >> 6u) & 0x3Fu)); + utf8_bytes[3] = static_cast::int_type>(0x80u | (wc & 0x3Fu)); + utf8_bytes_filled = 4; + } + else + { + // unknown character + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + } + } +}; + +template +struct wide_string_input_helper +{ + // UTF-16 + static void fill_buffer(const WideStringType& str, + size_t& current_wchar, + std::array::int_type, 4>& utf8_bytes, + size_t& utf8_bytes_index, + size_t& utf8_bytes_filled) + { + utf8_bytes_index = 0; + + if (current_wchar == str.size()) + { + utf8_bytes[0] = std::char_traits::eof(); + utf8_bytes_filled = 1; + } + else + { + // get the current character + const auto wc = static_cast(str[current_wchar++]); + + // UTF-16 to UTF-8 encoding + if (wc < 0x80) + { + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + else if (wc <= 0x7FF) + { + utf8_bytes[0] = static_cast::int_type>(0xC0u | ((wc >> 6u))); + utf8_bytes[1] = static_cast::int_type>(0x80u | (wc & 0x3Fu)); + utf8_bytes_filled = 2; + } + else if (0xD800 > wc or wc >= 0xE000) + { + utf8_bytes[0] = static_cast::int_type>(0xE0u | ((wc >> 12u))); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((wc >> 6u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | (wc & 0x3Fu)); + utf8_bytes_filled = 3; + } + else + { + if (current_wchar < str.size()) + { + const auto wc2 = static_cast(str[current_wchar++]); + const auto charcode = 0x10000u + (((wc & 0x3FFu) << 10u) | (wc2 & 0x3FFu)); + utf8_bytes[0] = static_cast::int_type>(0xF0u | (charcode >> 18u)); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((charcode >> 12u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | ((charcode >> 6u) & 0x3Fu)); + utf8_bytes[3] = static_cast::int_type>(0x80u | (charcode & 0x3Fu)); + utf8_bytes_filled = 4; + } + else + { + // unknown character + ++current_wchar; + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + } + } + } +}; + +template +class wide_string_input_adapter : public input_adapter_protocol +{ + public: + explicit wide_string_input_adapter(const WideStringType& w) noexcept + : str(w) + {} + + std::char_traits::int_type get_character() noexcept override + { + // check if buffer needs to be filled + if (utf8_bytes_index == utf8_bytes_filled) + { + fill_buffer(); + + assert(utf8_bytes_filled > 0); + assert(utf8_bytes_index == 0); + } + + // use buffer + assert(utf8_bytes_filled > 0); + assert(utf8_bytes_index < utf8_bytes_filled); + return utf8_bytes[utf8_bytes_index++]; + } + + private: + template + void fill_buffer() + { + wide_string_input_helper::fill_buffer(str, current_wchar, utf8_bytes, utf8_bytes_index, utf8_bytes_filled); + } + + /// the wstring to process + const WideStringType& str; + + /// index of the current wchar in str + std::size_t current_wchar = 0; + + /// a buffer for UTF-8 bytes + std::array::int_type, 4> utf8_bytes = {{0, 0, 0, 0}}; + + /// index to the utf8_codes array for the next valid byte + std::size_t utf8_bytes_index = 0; + /// number of valid bytes in the utf8_codes array + std::size_t utf8_bytes_filled = 0; +}; + +class input_adapter +{ + public: + // native support + JSON_HEDLEY_NON_NULL(2) + input_adapter(std::FILE* file) + : ia(std::make_shared(file)) {} + /// input adapter for input stream + input_adapter(std::istream& i) + : ia(std::make_shared(i)) {} + + /// input adapter for input stream + input_adapter(std::istream&& i) + : ia(std::make_shared(i)) {} + + input_adapter(const std::wstring& ws) + : ia(std::make_shared>(ws)) {} + + input_adapter(const std::u16string& ws) + : ia(std::make_shared>(ws)) {} + + input_adapter(const std::u32string& ws) + : ia(std::make_shared>(ws)) {} + + /// input adapter for buffer + template::value and + std::is_integral::type>::value and + sizeof(typename std::remove_pointer::type) == 1, + int>::type = 0> + input_adapter(CharT b, std::size_t l) + : ia(std::make_shared(reinterpret_cast(b), l)) {} + + // derived support + + /// input adapter for string literal + template::value and + std::is_integral::type>::value and + sizeof(typename std::remove_pointer::type) == 1, + int>::type = 0> + input_adapter(CharT b) + : input_adapter(reinterpret_cast(b), + std::strlen(reinterpret_cast(b))) {} + + /// input adapter for iterator range with contiguous storage + template::iterator_category, std::random_access_iterator_tag>::value, + int>::type = 0> + input_adapter(IteratorType first, IteratorType last) + { +#ifndef NDEBUG + // assertion to check that the iterator range is indeed contiguous, + // see http://stackoverflow.com/a/35008842/266378 for more discussion + const auto is_contiguous = std::accumulate( + first, last, std::pair(true, 0), + [&first](std::pair res, decltype(*first) val) + { + res.first &= (val == *(std::next(std::addressof(*first), res.second++))); + return res; + }).first; + assert(is_contiguous); +#endif + + // assertion to check that each element is 1 byte long + static_assert( + sizeof(typename iterator_traits::value_type) == 1, + "each element in the iterator range must have the size of 1 byte"); + + const auto len = static_cast(std::distance(first, last)); + if (JSON_HEDLEY_LIKELY(len > 0)) + { + // there is at least one element: use the address of first + ia = std::make_shared(reinterpret_cast(&(*first)), len); + } + else + { + // the address of first cannot be used: use nullptr + ia = std::make_shared(nullptr, len); + } + } + + /// input adapter for array + template + input_adapter(T (&array)[N]) + : input_adapter(std::begin(array), std::end(array)) {} + + /// input adapter for contiguous container + template::value and + std::is_base_of()))>::iterator_category>::value, + int>::type = 0> + input_adapter(const ContiguousContainer& c) + : input_adapter(std::begin(c), std::end(c)) {} + + operator input_adapter_t() + { + return ia; + } + + private: + /// the actual adapter + input_adapter_t ia = nullptr; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/input/json_sax.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/json_sax.hpp new file mode 100644 index 000000000..606b7862e --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/json_sax.hpp @@ -0,0 +1,701 @@ +#pragma once + +#include // assert +#include +#include // string +#include // move +#include // vector + +#include +#include + +namespace nlohmann +{ + +/*! +@brief SAX interface + +This class describes the SAX interface used by @ref nlohmann::json::sax_parse. +Each function is called in different situations while the input is parsed. The +boolean return value informs the parser whether to continue processing the +input. +*/ +template +struct json_sax +{ + /// type for (signed) integers + using number_integer_t = typename BasicJsonType::number_integer_t; + /// type for unsigned integers + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + /// type for floating-point numbers + using number_float_t = typename BasicJsonType::number_float_t; + /// type for strings + using string_t = typename BasicJsonType::string_t; + + /*! + @brief a null value was read + @return whether parsing should proceed + */ + virtual bool null() = 0; + + /*! + @brief a boolean value was read + @param[in] val boolean value + @return whether parsing should proceed + */ + virtual bool boolean(bool val) = 0; + + /*! + @brief an integer number was read + @param[in] val integer value + @return whether parsing should proceed + */ + virtual bool number_integer(number_integer_t val) = 0; + + /*! + @brief an unsigned integer number was read + @param[in] val unsigned integer value + @return whether parsing should proceed + */ + virtual bool number_unsigned(number_unsigned_t val) = 0; + + /*! + @brief an floating-point number was read + @param[in] val floating-point value + @param[in] s raw token value + @return whether parsing should proceed + */ + virtual bool number_float(number_float_t val, const string_t& s) = 0; + + /*! + @brief a string was read + @param[in] val string value + @return whether parsing should proceed + @note It is safe to move the passed string. + */ + virtual bool string(string_t& val) = 0; + + /*! + @brief the beginning of an object was read + @param[in] elements number of object elements or -1 if unknown + @return whether parsing should proceed + @note binary formats may report the number of elements + */ + virtual bool start_object(std::size_t elements) = 0; + + /*! + @brief an object key was read + @param[in] val object key + @return whether parsing should proceed + @note It is safe to move the passed string. + */ + virtual bool key(string_t& val) = 0; + + /*! + @brief the end of an object was read + @return whether parsing should proceed + */ + virtual bool end_object() = 0; + + /*! + @brief the beginning of an array was read + @param[in] elements number of array elements or -1 if unknown + @return whether parsing should proceed + @note binary formats may report the number of elements + */ + virtual bool start_array(std::size_t elements) = 0; + + /*! + @brief the end of an array was read + @return whether parsing should proceed + */ + virtual bool end_array() = 0; + + /*! + @brief a parse error occurred + @param[in] position the position in the input where the error occurs + @param[in] last_token the last read token + @param[in] ex an exception object describing the error + @return whether parsing should proceed (must return false) + */ + virtual bool parse_error(std::size_t position, + const std::string& last_token, + const detail::exception& ex) = 0; + + virtual ~json_sax() = default; +}; + + +namespace detail +{ +/*! +@brief SAX implementation to create a JSON value from SAX events + +This class implements the @ref json_sax interface and processes the SAX events +to create a JSON value which makes it basically a DOM parser. The structure or +hierarchy of the JSON value is managed by the stack `ref_stack` which contains +a pointer to the respective array or object for each recursion depth. + +After successful parsing, the value that is passed by reference to the +constructor contains the parsed value. + +@tparam BasicJsonType the JSON type +*/ +template +class json_sax_dom_parser +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + + /*! + @param[in, out] r reference to a JSON value that is manipulated while + parsing + @param[in] allow_exceptions_ whether parse errors yield exceptions + */ + explicit json_sax_dom_parser(BasicJsonType& r, const bool allow_exceptions_ = true) + : root(r), allow_exceptions(allow_exceptions_) + {} + + // make class move-only + json_sax_dom_parser(const json_sax_dom_parser&) = delete; + json_sax_dom_parser(json_sax_dom_parser&&) = default; + json_sax_dom_parser& operator=(const json_sax_dom_parser&) = delete; + json_sax_dom_parser& operator=(json_sax_dom_parser&&) = default; + ~json_sax_dom_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + handle_value(val); + return true; + } + + bool start_object(std::size_t len) + { + ref_stack.push_back(handle_value(BasicJsonType::value_t::object)); + + if (JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) and len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, + "excessive object size: " + std::to_string(len))); + } + + return true; + } + + bool key(string_t& val) + { + // add null at given key and store the reference for later + object_element = &(ref_stack.back()->m_value.object->operator[](val)); + return true; + } + + bool end_object() + { + ref_stack.pop_back(); + return true; + } + + bool start_array(std::size_t len) + { + ref_stack.push_back(handle_value(BasicJsonType::value_t::array)); + + if (JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) and len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, + "excessive array size: " + std::to_string(len))); + } + + return true; + } + + bool end_array() + { + ref_stack.pop_back(); + return true; + } + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const detail::exception& ex) + { + errored = true; + if (allow_exceptions) + { + // determine the proper exception type from the id + switch ((ex.id / 100) % 100) + { + case 1: + JSON_THROW(*static_cast(&ex)); + case 4: + JSON_THROW(*static_cast(&ex)); + // LCOV_EXCL_START + case 2: + JSON_THROW(*static_cast(&ex)); + case 3: + JSON_THROW(*static_cast(&ex)); + case 5: + JSON_THROW(*static_cast(&ex)); + default: + assert(false); + // LCOV_EXCL_STOP + } + } + return false; + } + + constexpr bool is_errored() const + { + return errored; + } + + private: + /*! + @invariant If the ref stack is empty, then the passed value will be the new + root. + @invariant If the ref stack contains a value, then it is an array or an + object to which we can add elements + */ + template + JSON_HEDLEY_RETURNS_NON_NULL + BasicJsonType* handle_value(Value&& v) + { + if (ref_stack.empty()) + { + root = BasicJsonType(std::forward(v)); + return &root; + } + + assert(ref_stack.back()->is_array() or ref_stack.back()->is_object()); + + if (ref_stack.back()->is_array()) + { + ref_stack.back()->m_value.array->emplace_back(std::forward(v)); + return &(ref_stack.back()->m_value.array->back()); + } + + assert(ref_stack.back()->is_object()); + assert(object_element); + *object_element = BasicJsonType(std::forward(v)); + return object_element; + } + + /// the parsed JSON value + BasicJsonType& root; + /// stack to model hierarchy of values + std::vector ref_stack {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether a syntax error occurred + bool errored = false; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; +}; + +template +class json_sax_dom_callback_parser +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using parser_callback_t = typename BasicJsonType::parser_callback_t; + using parse_event_t = typename BasicJsonType::parse_event_t; + + json_sax_dom_callback_parser(BasicJsonType& r, + const parser_callback_t cb, + const bool allow_exceptions_ = true) + : root(r), callback(cb), allow_exceptions(allow_exceptions_) + { + keep_stack.push_back(true); + } + + // make class move-only + json_sax_dom_callback_parser(const json_sax_dom_callback_parser&) = delete; + json_sax_dom_callback_parser(json_sax_dom_callback_parser&&) = default; + json_sax_dom_callback_parser& operator=(const json_sax_dom_callback_parser&) = delete; + json_sax_dom_callback_parser& operator=(json_sax_dom_callback_parser&&) = default; + ~json_sax_dom_callback_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + handle_value(val); + return true; + } + + bool start_object(std::size_t len) + { + // check callback for object start + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::object_start, discarded); + keep_stack.push_back(keep); + + auto val = handle_value(BasicJsonType::value_t::object, true); + ref_stack.push_back(val.second); + + // check object limit + if (ref_stack.back() and JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) and len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, "excessive object size: " + std::to_string(len))); + } + + return true; + } + + bool key(string_t& val) + { + BasicJsonType k = BasicJsonType(val); + + // check callback for key + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, k); + key_keep_stack.push_back(keep); + + // add discarded value at given key and store the reference for later + if (keep and ref_stack.back()) + { + object_element = &(ref_stack.back()->m_value.object->operator[](val) = discarded); + } + + return true; + } + + bool end_object() + { + if (ref_stack.back() and not callback(static_cast(ref_stack.size()) - 1, parse_event_t::object_end, *ref_stack.back())) + { + // discard object + *ref_stack.back() = discarded; + } + + assert(not ref_stack.empty()); + assert(not keep_stack.empty()); + ref_stack.pop_back(); + keep_stack.pop_back(); + + if (not ref_stack.empty() and ref_stack.back() and ref_stack.back()->is_object()) + { + // remove discarded value + for (auto it = ref_stack.back()->begin(); it != ref_stack.back()->end(); ++it) + { + if (it->is_discarded()) + { + ref_stack.back()->erase(it); + break; + } + } + } + + return true; + } + + bool start_array(std::size_t len) + { + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::array_start, discarded); + keep_stack.push_back(keep); + + auto val = handle_value(BasicJsonType::value_t::array, true); + ref_stack.push_back(val.second); + + // check array limit + if (ref_stack.back() and JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) and len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, "excessive array size: " + std::to_string(len))); + } + + return true; + } + + bool end_array() + { + bool keep = true; + + if (ref_stack.back()) + { + keep = callback(static_cast(ref_stack.size()) - 1, parse_event_t::array_end, *ref_stack.back()); + if (not keep) + { + // discard array + *ref_stack.back() = discarded; + } + } + + assert(not ref_stack.empty()); + assert(not keep_stack.empty()); + ref_stack.pop_back(); + keep_stack.pop_back(); + + // remove discarded value + if (not keep and not ref_stack.empty() and ref_stack.back()->is_array()) + { + ref_stack.back()->m_value.array->pop_back(); + } + + return true; + } + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const detail::exception& ex) + { + errored = true; + if (allow_exceptions) + { + // determine the proper exception type from the id + switch ((ex.id / 100) % 100) + { + case 1: + JSON_THROW(*static_cast(&ex)); + case 4: + JSON_THROW(*static_cast(&ex)); + // LCOV_EXCL_START + case 2: + JSON_THROW(*static_cast(&ex)); + case 3: + JSON_THROW(*static_cast(&ex)); + case 5: + JSON_THROW(*static_cast(&ex)); + default: + assert(false); + // LCOV_EXCL_STOP + } + } + return false; + } + + constexpr bool is_errored() const + { + return errored; + } + + private: + /*! + @param[in] v value to add to the JSON value we build during parsing + @param[in] skip_callback whether we should skip calling the callback + function; this is required after start_array() and + start_object() SAX events, because otherwise we would call the + callback function with an empty array or object, respectively. + + @invariant If the ref stack is empty, then the passed value will be the new + root. + @invariant If the ref stack contains a value, then it is an array or an + object to which we can add elements + + @return pair of boolean (whether value should be kept) and pointer (to the + passed value in the ref_stack hierarchy; nullptr if not kept) + */ + template + std::pair handle_value(Value&& v, const bool skip_callback = false) + { + assert(not keep_stack.empty()); + + // do not handle this value if we know it would be added to a discarded + // container + if (not keep_stack.back()) + { + return {false, nullptr}; + } + + // create value + auto value = BasicJsonType(std::forward(v)); + + // check callback + const bool keep = skip_callback or callback(static_cast(ref_stack.size()), parse_event_t::value, value); + + // do not handle this value if we just learnt it shall be discarded + if (not keep) + { + return {false, nullptr}; + } + + if (ref_stack.empty()) + { + root = std::move(value); + return {true, &root}; + } + + // skip this value if we already decided to skip the parent + // (https://github.com/nlohmann/json/issues/971#issuecomment-413678360) + if (not ref_stack.back()) + { + return {false, nullptr}; + } + + // we now only expect arrays and objects + assert(ref_stack.back()->is_array() or ref_stack.back()->is_object()); + + // array + if (ref_stack.back()->is_array()) + { + ref_stack.back()->m_value.array->push_back(std::move(value)); + return {true, &(ref_stack.back()->m_value.array->back())}; + } + + // object + assert(ref_stack.back()->is_object()); + // check if we should store an element for the current key + assert(not key_keep_stack.empty()); + const bool store_element = key_keep_stack.back(); + key_keep_stack.pop_back(); + + if (not store_element) + { + return {false, nullptr}; + } + + assert(object_element); + *object_element = std::move(value); + return {true, object_element}; + } + + /// the parsed JSON value + BasicJsonType& root; + /// stack to model hierarchy of values + std::vector ref_stack {}; + /// stack to manage which values to keep + std::vector keep_stack {}; + /// stack to manage which object keys to keep + std::vector key_keep_stack {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether a syntax error occurred + bool errored = false; + /// callback function + const parser_callback_t callback = nullptr; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; + /// a discarded value for the callback + BasicJsonType discarded = BasicJsonType::value_t::discarded; +}; + +template +class json_sax_acceptor +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + + bool null() + { + return true; + } + + bool boolean(bool /*unused*/) + { + return true; + } + + bool number_integer(number_integer_t /*unused*/) + { + return true; + } + + bool number_unsigned(number_unsigned_t /*unused*/) + { + return true; + } + + bool number_float(number_float_t /*unused*/, const string_t& /*unused*/) + { + return true; + } + + bool string(string_t& /*unused*/) + { + return true; + } + + bool start_object(std::size_t /*unused*/ = std::size_t(-1)) + { + return true; + } + + bool key(string_t& /*unused*/) + { + return true; + } + + bool end_object() + { + return true; + } + + bool start_array(std::size_t /*unused*/ = std::size_t(-1)) + { + return true; + } + + bool end_array() + { + return true; + } + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const detail::exception& /*unused*/) + { + return false; + } +}; +} // namespace detail + +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/input/lexer.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/lexer.hpp new file mode 100644 index 000000000..0843d749d --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/lexer.hpp @@ -0,0 +1,1512 @@ +#pragma once + +#include // array +#include // localeconv +#include // size_t +#include // snprintf +#include // strtof, strtod, strtold, strtoll, strtoull +#include // initializer_list +#include // char_traits, string +#include // move +#include // vector + +#include +#include +#include + +namespace nlohmann +{ +namespace detail +{ +/////////// +// lexer // +/////////// + +/*! +@brief lexical analysis + +This class organizes the lexical analysis during JSON deserialization. +*/ +template +class lexer +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + + public: + /// token types for the parser + enum class token_type + { + uninitialized, ///< indicating the scanner is uninitialized + literal_true, ///< the `true` literal + literal_false, ///< the `false` literal + literal_null, ///< the `null` literal + value_string, ///< a string -- use get_string() for actual value + value_unsigned, ///< an unsigned integer -- use get_number_unsigned() for actual value + value_integer, ///< a signed integer -- use get_number_integer() for actual value + value_float, ///< an floating point number -- use get_number_float() for actual value + begin_array, ///< the character for array begin `[` + begin_object, ///< the character for object begin `{` + end_array, ///< the character for array end `]` + end_object, ///< the character for object end `}` + name_separator, ///< the name separator `:` + value_separator, ///< the value separator `,` + parse_error, ///< indicating a parse error + end_of_input, ///< indicating the end of the input buffer + literal_or_value ///< a literal or the begin of a value (only for diagnostics) + }; + + /// return name of values of type token_type (only used for errors) + JSON_HEDLEY_RETURNS_NON_NULL + JSON_HEDLEY_CONST + static const char* token_type_name(const token_type t) noexcept + { + switch (t) + { + case token_type::uninitialized: + return ""; + case token_type::literal_true: + return "true literal"; + case token_type::literal_false: + return "false literal"; + case token_type::literal_null: + return "null literal"; + case token_type::value_string: + return "string literal"; + case lexer::token_type::value_unsigned: + case lexer::token_type::value_integer: + case lexer::token_type::value_float: + return "number literal"; + case token_type::begin_array: + return "'['"; + case token_type::begin_object: + return "'{'"; + case token_type::end_array: + return "']'"; + case token_type::end_object: + return "'}'"; + case token_type::name_separator: + return "':'"; + case token_type::value_separator: + return "','"; + case token_type::parse_error: + return ""; + case token_type::end_of_input: + return "end of input"; + case token_type::literal_or_value: + return "'[', '{', or a literal"; + // LCOV_EXCL_START + default: // catch non-enum values + return "unknown token"; + // LCOV_EXCL_STOP + } + } + + explicit lexer(detail::input_adapter_t&& adapter) + : ia(std::move(adapter)), decimal_point_char(get_decimal_point()) {} + + // delete because of pointer members + lexer(const lexer&) = delete; + lexer(lexer&&) = delete; + lexer& operator=(lexer&) = delete; + lexer& operator=(lexer&&) = delete; + ~lexer() = default; + + private: + ///////////////////// + // locales + ///////////////////// + + /// return the locale-dependent decimal point + JSON_HEDLEY_PURE + static char get_decimal_point() noexcept + { + const auto loc = localeconv(); + assert(loc != nullptr); + return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point); + } + + ///////////////////// + // scan functions + ///////////////////// + + /*! + @brief get codepoint from 4 hex characters following `\u` + + For input "\u c1 c2 c3 c4" the codepoint is: + (c1 * 0x1000) + (c2 * 0x0100) + (c3 * 0x0010) + c4 + = (c1 << 12) + (c2 << 8) + (c3 << 4) + (c4 << 0) + + Furthermore, the possible characters '0'..'9', 'A'..'F', and 'a'..'f' + must be converted to the integers 0x0..0x9, 0xA..0xF, 0xA..0xF, resp. The + conversion is done by subtracting the offset (0x30, 0x37, and 0x57) + between the ASCII value of the character and the desired integer value. + + @return codepoint (0x0000..0xFFFF) or -1 in case of an error (e.g. EOF or + non-hex character) + */ + int get_codepoint() + { + // this function only makes sense after reading `\u` + assert(current == 'u'); + int codepoint = 0; + + const auto factors = { 12u, 8u, 4u, 0u }; + for (const auto factor : factors) + { + get(); + + if (current >= '0' and current <= '9') + { + codepoint += static_cast((static_cast(current) - 0x30u) << factor); + } + else if (current >= 'A' and current <= 'F') + { + codepoint += static_cast((static_cast(current) - 0x37u) << factor); + } + else if (current >= 'a' and current <= 'f') + { + codepoint += static_cast((static_cast(current) - 0x57u) << factor); + } + else + { + return -1; + } + } + + assert(0x0000 <= codepoint and codepoint <= 0xFFFF); + return codepoint; + } + + /*! + @brief check if the next byte(s) are inside a given range + + Adds the current byte and, for each passed range, reads a new byte and + checks if it is inside the range. If a violation was detected, set up an + error message and return false. Otherwise, return true. + + @param[in] ranges list of integers; interpreted as list of pairs of + inclusive lower and upper bound, respectively + + @pre The passed list @a ranges must have 2, 4, or 6 elements; that is, + 1, 2, or 3 pairs. This precondition is enforced by an assertion. + + @return true if and only if no range violation was detected + */ + bool next_byte_in_range(std::initializer_list ranges) + { + assert(ranges.size() == 2 or ranges.size() == 4 or ranges.size() == 6); + add(current); + + for (auto range = ranges.begin(); range != ranges.end(); ++range) + { + get(); + if (JSON_HEDLEY_LIKELY(*range <= current and current <= *(++range))) + { + add(current); + } + else + { + error_message = "invalid string: ill-formed UTF-8 byte"; + return false; + } + } + + return true; + } + + /*! + @brief scan a string literal + + This function scans a string according to Sect. 7 of RFC 7159. While + scanning, bytes are escaped and copied into buffer token_buffer. Then the + function returns successfully, token_buffer is *not* null-terminated (as it + may contain \0 bytes), and token_buffer.size() is the number of bytes in the + string. + + @return token_type::value_string if string could be successfully scanned, + token_type::parse_error otherwise + + @note In case of errors, variable error_message contains a textual + description. + */ + token_type scan_string() + { + // reset token_buffer (ignore opening quote) + reset(); + + // we entered the function by reading an open quote + assert(current == '\"'); + + while (true) + { + // get next character + switch (get()) + { + // end of file while parsing string + case std::char_traits::eof(): + { + error_message = "invalid string: missing closing quote"; + return token_type::parse_error; + } + + // closing quote + case '\"': + { + return token_type::value_string; + } + + // escapes + case '\\': + { + switch (get()) + { + // quotation mark + case '\"': + add('\"'); + break; + // reverse solidus + case '\\': + add('\\'); + break; + // solidus + case '/': + add('/'); + break; + // backspace + case 'b': + add('\b'); + break; + // form feed + case 'f': + add('\f'); + break; + // line feed + case 'n': + add('\n'); + break; + // carriage return + case 'r': + add('\r'); + break; + // tab + case 't': + add('\t'); + break; + + // unicode escapes + case 'u': + { + const int codepoint1 = get_codepoint(); + int codepoint = codepoint1; // start with codepoint1 + + if (JSON_HEDLEY_UNLIKELY(codepoint1 == -1)) + { + error_message = "invalid string: '\\u' must be followed by 4 hex digits"; + return token_type::parse_error; + } + + // check if code point is a high surrogate + if (0xD800 <= codepoint1 and codepoint1 <= 0xDBFF) + { + // expect next \uxxxx entry + if (JSON_HEDLEY_LIKELY(get() == '\\' and get() == 'u')) + { + const int codepoint2 = get_codepoint(); + + if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1)) + { + error_message = "invalid string: '\\u' must be followed by 4 hex digits"; + return token_type::parse_error; + } + + // check if codepoint2 is a low surrogate + if (JSON_HEDLEY_LIKELY(0xDC00 <= codepoint2 and codepoint2 <= 0xDFFF)) + { + // overwrite codepoint + codepoint = static_cast( + // high surrogate occupies the most significant 22 bits + (static_cast(codepoint1) << 10u) + // low surrogate occupies the least significant 15 bits + + static_cast(codepoint2) + // there is still the 0xD800, 0xDC00 and 0x10000 noise + // in the result so we have to subtract with: + // (0xD800 << 10) + DC00 - 0x10000 = 0x35FDC00 + - 0x35FDC00u); + } + else + { + error_message = "invalid string: surrogate U+DC00..U+DFFF must be followed by U+DC00..U+DFFF"; + return token_type::parse_error; + } + } + else + { + error_message = "invalid string: surrogate U+DC00..U+DFFF must be followed by U+DC00..U+DFFF"; + return token_type::parse_error; + } + } + else + { + if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 and codepoint1 <= 0xDFFF)) + { + error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF"; + return token_type::parse_error; + } + } + + // result of the above calculation yields a proper codepoint + assert(0x00 <= codepoint and codepoint <= 0x10FFFF); + + // translate codepoint into bytes + if (codepoint < 0x80) + { + // 1-byte characters: 0xxxxxxx (ASCII) + add(codepoint); + } + else if (codepoint <= 0x7FF) + { + // 2-byte characters: 110xxxxx 10xxxxxx + add(static_cast(0xC0u | (static_cast(codepoint) >> 6u))); + add(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + else if (codepoint <= 0xFFFF) + { + // 3-byte characters: 1110xxxx 10xxxxxx 10xxxxxx + add(static_cast(0xE0u | (static_cast(codepoint) >> 12u))); + add(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); + add(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + else + { + // 4-byte characters: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + add(static_cast(0xF0u | (static_cast(codepoint) >> 18u))); + add(static_cast(0x80u | ((static_cast(codepoint) >> 12u) & 0x3Fu))); + add(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); + add(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + + break; + } + + // other characters after escape + default: + error_message = "invalid string: forbidden character after backslash"; + return token_type::parse_error; + } + + break; + } + + // invalid control characters + case 0x00: + { + error_message = "invalid string: control character U+0000 (NUL) must be escaped to \\u0000"; + return token_type::parse_error; + } + + case 0x01: + { + error_message = "invalid string: control character U+0001 (SOH) must be escaped to \\u0001"; + return token_type::parse_error; + } + + case 0x02: + { + error_message = "invalid string: control character U+0002 (STX) must be escaped to \\u0002"; + return token_type::parse_error; + } + + case 0x03: + { + error_message = "invalid string: control character U+0003 (ETX) must be escaped to \\u0003"; + return token_type::parse_error; + } + + case 0x04: + { + error_message = "invalid string: control character U+0004 (EOT) must be escaped to \\u0004"; + return token_type::parse_error; + } + + case 0x05: + { + error_message = "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005"; + return token_type::parse_error; + } + + case 0x06: + { + error_message = "invalid string: control character U+0006 (ACK) must be escaped to \\u0006"; + return token_type::parse_error; + } + + case 0x07: + { + error_message = "invalid string: control character U+0007 (BEL) must be escaped to \\u0007"; + return token_type::parse_error; + } + + case 0x08: + { + error_message = "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b"; + return token_type::parse_error; + } + + case 0x09: + { + error_message = "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t"; + return token_type::parse_error; + } + + case 0x0A: + { + error_message = "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n"; + return token_type::parse_error; + } + + case 0x0B: + { + error_message = "invalid string: control character U+000B (VT) must be escaped to \\u000B"; + return token_type::parse_error; + } + + case 0x0C: + { + error_message = "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f"; + return token_type::parse_error; + } + + case 0x0D: + { + error_message = "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r"; + return token_type::parse_error; + } + + case 0x0E: + { + error_message = "invalid string: control character U+000E (SO) must be escaped to \\u000E"; + return token_type::parse_error; + } + + case 0x0F: + { + error_message = "invalid string: control character U+000F (SI) must be escaped to \\u000F"; + return token_type::parse_error; + } + + case 0x10: + { + error_message = "invalid string: control character U+0010 (DLE) must be escaped to \\u0010"; + return token_type::parse_error; + } + + case 0x11: + { + error_message = "invalid string: control character U+0011 (DC1) must be escaped to \\u0011"; + return token_type::parse_error; + } + + case 0x12: + { + error_message = "invalid string: control character U+0012 (DC2) must be escaped to \\u0012"; + return token_type::parse_error; + } + + case 0x13: + { + error_message = "invalid string: control character U+0013 (DC3) must be escaped to \\u0013"; + return token_type::parse_error; + } + + case 0x14: + { + error_message = "invalid string: control character U+0014 (DC4) must be escaped to \\u0014"; + return token_type::parse_error; + } + + case 0x15: + { + error_message = "invalid string: control character U+0015 (NAK) must be escaped to \\u0015"; + return token_type::parse_error; + } + + case 0x16: + { + error_message = "invalid string: control character U+0016 (SYN) must be escaped to \\u0016"; + return token_type::parse_error; + } + + case 0x17: + { + error_message = "invalid string: control character U+0017 (ETB) must be escaped to \\u0017"; + return token_type::parse_error; + } + + case 0x18: + { + error_message = "invalid string: control character U+0018 (CAN) must be escaped to \\u0018"; + return token_type::parse_error; + } + + case 0x19: + { + error_message = "invalid string: control character U+0019 (EM) must be escaped to \\u0019"; + return token_type::parse_error; + } + + case 0x1A: + { + error_message = "invalid string: control character U+001A (SUB) must be escaped to \\u001A"; + return token_type::parse_error; + } + + case 0x1B: + { + error_message = "invalid string: control character U+001B (ESC) must be escaped to \\u001B"; + return token_type::parse_error; + } + + case 0x1C: + { + error_message = "invalid string: control character U+001C (FS) must be escaped to \\u001C"; + return token_type::parse_error; + } + + case 0x1D: + { + error_message = "invalid string: control character U+001D (GS) must be escaped to \\u001D"; + return token_type::parse_error; + } + + case 0x1E: + { + error_message = "invalid string: control character U+001E (RS) must be escaped to \\u001E"; + return token_type::parse_error; + } + + case 0x1F: + { + error_message = "invalid string: control character U+001F (US) must be escaped to \\u001F"; + return token_type::parse_error; + } + + // U+0020..U+007F (except U+0022 (quote) and U+005C (backspace)) + case 0x20: + case 0x21: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + case 0x4C: + case 0x4D: + case 0x4E: + case 0x4F: + case 0x50: + case 0x51: + case 0x52: + case 0x53: + case 0x54: + case 0x55: + case 0x56: + case 0x57: + case 0x58: + case 0x59: + case 0x5A: + case 0x5B: + case 0x5D: + case 0x5E: + case 0x5F: + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: + case 0x79: + case 0x7A: + case 0x7B: + case 0x7C: + case 0x7D: + case 0x7E: + case 0x7F: + { + add(current); + break; + } + + // U+0080..U+07FF: bytes C2..DF 80..BF + case 0xC2: + case 0xC3: + case 0xC4: + case 0xC5: + case 0xC6: + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCB: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xD3: + case 0xD4: + case 0xD5: + case 0xD6: + case 0xD7: + case 0xD8: + case 0xD9: + case 0xDA: + case 0xDB: + case 0xDC: + case 0xDD: + case 0xDE: + case 0xDF: + { + if (JSON_HEDLEY_UNLIKELY(not next_byte_in_range({0x80, 0xBF}))) + { + return token_type::parse_error; + } + break; + } + + // U+0800..U+0FFF: bytes E0 A0..BF 80..BF + case 0xE0: + { + if (JSON_HEDLEY_UNLIKELY(not (next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+1000..U+CFFF: bytes E1..EC 80..BF 80..BF + // U+E000..U+FFFF: bytes EE..EF 80..BF 80..BF + case 0xE1: + case 0xE2: + case 0xE3: + case 0xE4: + case 0xE5: + case 0xE6: + case 0xE7: + case 0xE8: + case 0xE9: + case 0xEA: + case 0xEB: + case 0xEC: + case 0xEE: + case 0xEF: + { + if (JSON_HEDLEY_UNLIKELY(not (next_byte_in_range({0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+D000..U+D7FF: bytes ED 80..9F 80..BF + case 0xED: + { + if (JSON_HEDLEY_UNLIKELY(not (next_byte_in_range({0x80, 0x9F, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+10000..U+3FFFF F0 90..BF 80..BF 80..BF + case 0xF0: + { + if (JSON_HEDLEY_UNLIKELY(not (next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF + case 0xF1: + case 0xF2: + case 0xF3: + { + if (JSON_HEDLEY_UNLIKELY(not (next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+100000..U+10FFFF F4 80..8F 80..BF 80..BF + case 0xF4: + { + if (JSON_HEDLEY_UNLIKELY(not (next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // remaining bytes (80..C1 and F5..FF) are ill-formed + default: + { + error_message = "invalid string: ill-formed UTF-8 byte"; + return token_type::parse_error; + } + } + } + } + + JSON_HEDLEY_NON_NULL(2) + static void strtof(float& f, const char* str, char** endptr) noexcept + { + f = std::strtof(str, endptr); + } + + JSON_HEDLEY_NON_NULL(2) + static void strtof(double& f, const char* str, char** endptr) noexcept + { + f = std::strtod(str, endptr); + } + + JSON_HEDLEY_NON_NULL(2) + static void strtof(long double& f, const char* str, char** endptr) noexcept + { + f = std::strtold(str, endptr); + } + + /*! + @brief scan a number literal + + This function scans a string according to Sect. 6 of RFC 7159. + + The function is realized with a deterministic finite state machine derived + from the grammar described in RFC 7159. Starting in state "init", the + input is read and used to determined the next state. Only state "done" + accepts the number. State "error" is a trap state to model errors. In the + table below, "anything" means any character but the ones listed before. + + state | 0 | 1-9 | e E | + | - | . | anything + ---------|----------|----------|----------|---------|---------|----------|----------- + init | zero | any1 | [error] | [error] | minus | [error] | [error] + minus | zero | any1 | [error] | [error] | [error] | [error] | [error] + zero | done | done | exponent | done | done | decimal1 | done + any1 | any1 | any1 | exponent | done | done | decimal1 | done + decimal1 | decimal2 | [error] | [error] | [error] | [error] | [error] | [error] + decimal2 | decimal2 | decimal2 | exponent | done | done | done | done + exponent | any2 | any2 | [error] | sign | sign | [error] | [error] + sign | any2 | any2 | [error] | [error] | [error] | [error] | [error] + any2 | any2 | any2 | done | done | done | done | done + + The state machine is realized with one label per state (prefixed with + "scan_number_") and `goto` statements between them. The state machine + contains cycles, but any cycle can be left when EOF is read. Therefore, + the function is guaranteed to terminate. + + During scanning, the read bytes are stored in token_buffer. This string is + then converted to a signed integer, an unsigned integer, or a + floating-point number. + + @return token_type::value_unsigned, token_type::value_integer, or + token_type::value_float if number could be successfully scanned, + token_type::parse_error otherwise + + @note The scanner is independent of the current locale. Internally, the + locale's decimal point is used instead of `.` to work with the + locale-dependent converters. + */ + token_type scan_number() // lgtm [cpp/use-of-goto] + { + // reset token_buffer to store the number's bytes + reset(); + + // the type of the parsed number; initially set to unsigned; will be + // changed if minus sign, decimal point or exponent is read + token_type number_type = token_type::value_unsigned; + + // state (init): we just found out we need to scan a number + switch (current) + { + case '-': + { + add(current); + goto scan_number_minus; + } + + case '0': + { + add(current); + goto scan_number_zero; + } + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any1; + } + + // all other characters are rejected outside scan_number() + default: // LCOV_EXCL_LINE + assert(false); // LCOV_EXCL_LINE + } + +scan_number_minus: + // state: we just parsed a leading minus sign + number_type = token_type::value_integer; + switch (get()) + { + case '0': + { + add(current); + goto scan_number_zero; + } + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any1; + } + + default: + { + error_message = "invalid number; expected digit after '-'"; + return token_type::parse_error; + } + } + +scan_number_zero: + // state: we just parse a zero (maybe with a leading minus sign) + switch (get()) + { + case '.': + { + add(decimal_point_char); + goto scan_number_decimal1; + } + + case 'e': + case 'E': + { + add(current); + goto scan_number_exponent; + } + + default: + goto scan_number_done; + } + +scan_number_any1: + // state: we just parsed a number 0-9 (maybe with a leading minus sign) + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any1; + } + + case '.': + { + add(decimal_point_char); + goto scan_number_decimal1; + } + + case 'e': + case 'E': + { + add(current); + goto scan_number_exponent; + } + + default: + goto scan_number_done; + } + +scan_number_decimal1: + // state: we just parsed a decimal point + number_type = token_type::value_float; + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_decimal2; + } + + default: + { + error_message = "invalid number; expected digit after '.'"; + return token_type::parse_error; + } + } + +scan_number_decimal2: + // we just parsed at least one number after a decimal point + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_decimal2; + } + + case 'e': + case 'E': + { + add(current); + goto scan_number_exponent; + } + + default: + goto scan_number_done; + } + +scan_number_exponent: + // we just parsed an exponent + number_type = token_type::value_float; + switch (get()) + { + case '+': + case '-': + { + add(current); + goto scan_number_sign; + } + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any2; + } + + default: + { + error_message = + "invalid number; expected '+', '-', or digit after exponent"; + return token_type::parse_error; + } + } + +scan_number_sign: + // we just parsed an exponent sign + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any2; + } + + default: + { + error_message = "invalid number; expected digit after exponent sign"; + return token_type::parse_error; + } + } + +scan_number_any2: + // we just parsed a number after the exponent or exponent sign + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any2; + } + + default: + goto scan_number_done; + } + +scan_number_done: + // unget the character after the number (we only read it to know that + // we are done scanning a number) + unget(); + + char* endptr = nullptr; + errno = 0; + + // try to parse integers first and fall back to floats + if (number_type == token_type::value_unsigned) + { + const auto x = std::strtoull(token_buffer.data(), &endptr, 10); + + // we checked the number format before + assert(endptr == token_buffer.data() + token_buffer.size()); + + if (errno == 0) + { + value_unsigned = static_cast(x); + if (value_unsigned == x) + { + return token_type::value_unsigned; + } + } + } + else if (number_type == token_type::value_integer) + { + const auto x = std::strtoll(token_buffer.data(), &endptr, 10); + + // we checked the number format before + assert(endptr == token_buffer.data() + token_buffer.size()); + + if (errno == 0) + { + value_integer = static_cast(x); + if (value_integer == x) + { + return token_type::value_integer; + } + } + } + + // this code is reached if we parse a floating-point number or if an + // integer conversion above failed + strtof(value_float, token_buffer.data(), &endptr); + + // we checked the number format before + assert(endptr == token_buffer.data() + token_buffer.size()); + + return token_type::value_float; + } + + /*! + @param[in] literal_text the literal text to expect + @param[in] length the length of the passed literal text + @param[in] return_type the token type to return on success + */ + JSON_HEDLEY_NON_NULL(2) + token_type scan_literal(const char* literal_text, const std::size_t length, + token_type return_type) + { + assert(current == literal_text[0]); + for (std::size_t i = 1; i < length; ++i) + { + if (JSON_HEDLEY_UNLIKELY(get() != literal_text[i])) + { + error_message = "invalid literal"; + return token_type::parse_error; + } + } + return return_type; + } + + ///////////////////// + // input management + ///////////////////// + + /// reset token_buffer; current character is beginning of token + void reset() noexcept + { + token_buffer.clear(); + token_string.clear(); + token_string.push_back(std::char_traits::to_char_type(current)); + } + + /* + @brief get next character from the input + + This function provides the interface to the used input adapter. It does + not throw in case the input reached EOF, but returns a + `std::char_traits::eof()` in that case. Stores the scanned characters + for use in error messages. + + @return character read from the input + */ + std::char_traits::int_type get() + { + ++position.chars_read_total; + ++position.chars_read_current_line; + + if (next_unget) + { + // just reset the next_unget variable and work with current + next_unget = false; + } + else + { + current = ia->get_character(); + } + + if (JSON_HEDLEY_LIKELY(current != std::char_traits::eof())) + { + token_string.push_back(std::char_traits::to_char_type(current)); + } + + if (current == '\n') + { + ++position.lines_read; + position.chars_read_current_line = 0; + } + + return current; + } + + /*! + @brief unget current character (read it again on next get) + + We implement unget by setting variable next_unget to true. The input is not + changed - we just simulate ungetting by modifying chars_read_total, + chars_read_current_line, and token_string. The next call to get() will + behave as if the unget character is read again. + */ + void unget() + { + next_unget = true; + + --position.chars_read_total; + + // in case we "unget" a newline, we have to also decrement the lines_read + if (position.chars_read_current_line == 0) + { + if (position.lines_read > 0) + { + --position.lines_read; + } + } + else + { + --position.chars_read_current_line; + } + + if (JSON_HEDLEY_LIKELY(current != std::char_traits::eof())) + { + assert(not token_string.empty()); + token_string.pop_back(); + } + } + + /// add a character to token_buffer + void add(int c) + { + token_buffer.push_back(std::char_traits::to_char_type(c)); + } + + public: + ///////////////////// + // value getters + ///////////////////// + + /// return integer value + constexpr number_integer_t get_number_integer() const noexcept + { + return value_integer; + } + + /// return unsigned integer value + constexpr number_unsigned_t get_number_unsigned() const noexcept + { + return value_unsigned; + } + + /// return floating-point value + constexpr number_float_t get_number_float() const noexcept + { + return value_float; + } + + /// return current string value (implicitly resets the token; useful only once) + string_t& get_string() + { + return token_buffer; + } + + ///////////////////// + // diagnostics + ///////////////////// + + /// return position of last read token + constexpr position_t get_position() const noexcept + { + return position; + } + + /// return the last read token (for errors only). Will never contain EOF + /// (an arbitrary value that is not a valid char value, often -1), because + /// 255 may legitimately occur. May contain NUL, which should be escaped. + std::string get_token_string() const + { + // escape control characters + std::string result; + for (const auto c : token_string) + { + if ('\x00' <= c and c <= '\x1F') + { + // escape control characters + std::array cs{{}}; + (std::snprintf)(cs.data(), cs.size(), "", static_cast(c)); + result += cs.data(); + } + else + { + // add character as is + result.push_back(c); + } + } + + return result; + } + + /// return syntax error message + JSON_HEDLEY_RETURNS_NON_NULL + constexpr const char* get_error_message() const noexcept + { + return error_message; + } + + ///////////////////// + // actual scanner + ///////////////////// + + /*! + @brief skip the UTF-8 byte order mark + @return true iff there is no BOM or the correct BOM has been skipped + */ + bool skip_bom() + { + if (get() == 0xEF) + { + // check if we completely parse the BOM + return get() == 0xBB and get() == 0xBF; + } + + // the first character is not the beginning of the BOM; unget it to + // process is later + unget(); + return true; + } + + token_type scan() + { + // initially, skip the BOM + if (position.chars_read_total == 0 and not skip_bom()) + { + error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; + return token_type::parse_error; + } + + // read next character and ignore whitespace + do + { + get(); + } + while (current == ' ' or current == '\t' or current == '\n' or current == '\r'); + + switch (current) + { + // structural characters + case '[': + return token_type::begin_array; + case ']': + return token_type::end_array; + case '{': + return token_type::begin_object; + case '}': + return token_type::end_object; + case ':': + return token_type::name_separator; + case ',': + return token_type::value_separator; + + // literals + case 't': + return scan_literal("true", 4, token_type::literal_true); + case 'f': + return scan_literal("false", 5, token_type::literal_false); + case 'n': + return scan_literal("null", 4, token_type::literal_null); + + // string + case '\"': + return scan_string(); + + // number + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return scan_number(); + + // end of input (the null byte is needed when parsing from + // string literals) + case '\0': + case std::char_traits::eof(): + return token_type::end_of_input; + + // error + default: + error_message = "invalid literal"; + return token_type::parse_error; + } + } + + private: + /// input adapter + detail::input_adapter_t ia = nullptr; + + /// the current character + std::char_traits::int_type current = std::char_traits::eof(); + + /// whether the next get() call should just return current + bool next_unget = false; + + /// the start position of the current token + position_t position {}; + + /// raw input token string (for error messages) + std::vector token_string {}; + + /// buffer for variable-length tokens (numbers, strings) + string_t token_buffer {}; + + /// a description of occurred lexer errors + const char* error_message = ""; + + // number values + number_integer_t value_integer = 0; + number_unsigned_t value_unsigned = 0; + number_float_t value_float = 0; + + /// the decimal point + const char decimal_point_char = '.'; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/input/parser.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/parser.hpp new file mode 100644 index 000000000..8d4febcbf --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/parser.hpp @@ -0,0 +1,498 @@ +#pragma once + +#include // assert +#include // isfinite +#include // uint8_t +#include // function +#include // string +#include // move +#include // vector + +#include +#include +#include +#include +#include +#include +#include + +namespace nlohmann +{ +namespace detail +{ +//////////// +// parser // +//////////// + +/*! +@brief syntax analysis + +This class implements a recursive decent parser. +*/ +template +class parser +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using lexer_t = lexer; + using token_type = typename lexer_t::token_type; + + public: + enum class parse_event_t : uint8_t + { + /// the parser read `{` and started to process a JSON object + object_start, + /// the parser read `}` and finished processing a JSON object + object_end, + /// the parser read `[` and started to process a JSON array + array_start, + /// the parser read `]` and finished processing a JSON array + array_end, + /// the parser read a key of a value in an object + key, + /// the parser finished reading a JSON value + value + }; + + using parser_callback_t = + std::function; + + /// a parser reading from an input adapter + explicit parser(detail::input_adapter_t&& adapter, + const parser_callback_t cb = nullptr, + const bool allow_exceptions_ = true) + : callback(cb), m_lexer(std::move(adapter)), allow_exceptions(allow_exceptions_) + { + // read first token + get_token(); + } + + /*! + @brief public parser interface + + @param[in] strict whether to expect the last token to be EOF + @param[in,out] result parsed JSON value + + @throw parse_error.101 in case of an unexpected token + @throw parse_error.102 if to_unicode fails or surrogate error + @throw parse_error.103 if to_unicode fails + */ + void parse(const bool strict, BasicJsonType& result) + { + if (callback) + { + json_sax_dom_callback_parser sdp(result, callback, allow_exceptions); + sax_parse_internal(&sdp); + result.assert_invariant(); + + // in strict mode, input must be completely read + if (strict and (get_token() != token_type::end_of_input)) + { + sdp.parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::end_of_input, "value"))); + } + + // in case of an error, return discarded value + if (sdp.is_errored()) + { + result = value_t::discarded; + return; + } + + // set top-level value to null if it was discarded by the callback + // function + if (result.is_discarded()) + { + result = nullptr; + } + } + else + { + json_sax_dom_parser sdp(result, allow_exceptions); + sax_parse_internal(&sdp); + result.assert_invariant(); + + // in strict mode, input must be completely read + if (strict and (get_token() != token_type::end_of_input)) + { + sdp.parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::end_of_input, "value"))); + } + + // in case of an error, return discarded value + if (sdp.is_errored()) + { + result = value_t::discarded; + return; + } + } + } + + /*! + @brief public accept interface + + @param[in] strict whether to expect the last token to be EOF + @return whether the input is a proper JSON text + */ + bool accept(const bool strict = true) + { + json_sax_acceptor sax_acceptor; + return sax_parse(&sax_acceptor, strict); + } + + template + JSON_HEDLEY_NON_NULL(2) + bool sax_parse(SAX* sax, const bool strict = true) + { + (void)detail::is_sax_static_asserts {}; + const bool result = sax_parse_internal(sax); + + // strict mode: next byte must be EOF + if (result and strict and (get_token() != token_type::end_of_input)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::end_of_input, "value"))); + } + + return result; + } + + private: + template + JSON_HEDLEY_NON_NULL(2) + bool sax_parse_internal(SAX* sax) + { + // stack to remember the hierarchy of structured values we are parsing + // true = array; false = object + std::vector states; + // value to avoid a goto (see comment where set to true) + bool skip_to_state_evaluation = false; + + while (true) + { + if (not skip_to_state_evaluation) + { + // invariant: get_token() was called before each iteration + switch (last_token) + { + case token_type::begin_object: + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_object(std::size_t(-1)))) + { + return false; + } + + // closing } -> we are done + if (get_token() == token_type::end_object) + { + if (JSON_HEDLEY_UNLIKELY(not sax->end_object())) + { + return false; + } + break; + } + + // parse key + if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::value_string, "object key"))); + } + if (JSON_HEDLEY_UNLIKELY(not sax->key(m_lexer.get_string()))) + { + return false; + } + + // parse separator (:) + if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::name_separator, "object separator"))); + } + + // remember we are now inside an object + states.push_back(false); + + // parse values + get_token(); + continue; + } + + case token_type::begin_array: + { + if (JSON_HEDLEY_UNLIKELY(not sax->start_array(std::size_t(-1)))) + { + return false; + } + + // closing ] -> we are done + if (get_token() == token_type::end_array) + { + if (JSON_HEDLEY_UNLIKELY(not sax->end_array())) + { + return false; + } + break; + } + + // remember we are now inside an array + states.push_back(true); + + // parse values (no need to call get_token) + continue; + } + + case token_type::value_float: + { + const auto res = m_lexer.get_number_float(); + + if (JSON_HEDLEY_UNLIKELY(not std::isfinite(res))) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + out_of_range::create(406, "number overflow parsing '" + m_lexer.get_token_string() + "'")); + } + + if (JSON_HEDLEY_UNLIKELY(not sax->number_float(res, m_lexer.get_string()))) + { + return false; + } + + break; + } + + case token_type::literal_false: + { + if (JSON_HEDLEY_UNLIKELY(not sax->boolean(false))) + { + return false; + } + break; + } + + case token_type::literal_null: + { + if (JSON_HEDLEY_UNLIKELY(not sax->null())) + { + return false; + } + break; + } + + case token_type::literal_true: + { + if (JSON_HEDLEY_UNLIKELY(not sax->boolean(true))) + { + return false; + } + break; + } + + case token_type::value_integer: + { + if (JSON_HEDLEY_UNLIKELY(not sax->number_integer(m_lexer.get_number_integer()))) + { + return false; + } + break; + } + + case token_type::value_string: + { + if (JSON_HEDLEY_UNLIKELY(not sax->string(m_lexer.get_string()))) + { + return false; + } + break; + } + + case token_type::value_unsigned: + { + if (JSON_HEDLEY_UNLIKELY(not sax->number_unsigned(m_lexer.get_number_unsigned()))) + { + return false; + } + break; + } + + case token_type::parse_error: + { + // using "uninitialized" to avoid "expected" message + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::uninitialized, "value"))); + } + + default: // the last token was unexpected + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::literal_or_value, "value"))); + } + } + } + else + { + skip_to_state_evaluation = false; + } + + // we reached this line after we successfully parsed a value + if (states.empty()) + { + // empty stack: we reached the end of the hierarchy: done + return true; + } + + if (states.back()) // array + { + // comma -> next value + if (get_token() == token_type::value_separator) + { + // parse a new value + get_token(); + continue; + } + + // closing ] + if (JSON_HEDLEY_LIKELY(last_token == token_type::end_array)) + { + if (JSON_HEDLEY_UNLIKELY(not sax->end_array())) + { + return false; + } + + // We are done with this array. Before we can parse a + // new value, we need to evaluate the new state first. + // By setting skip_to_state_evaluation to false, we + // are effectively jumping to the beginning of this if. + assert(not states.empty()); + states.pop_back(); + skip_to_state_evaluation = true; + continue; + } + + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::end_array, "array"))); + } + else // object + { + // comma -> next value + if (get_token() == token_type::value_separator) + { + // parse key + if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::value_string)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::value_string, "object key"))); + } + + if (JSON_HEDLEY_UNLIKELY(not sax->key(m_lexer.get_string()))) + { + return false; + } + + // parse separator (:) + if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::name_separator, "object separator"))); + } + + // parse values + get_token(); + continue; + } + + // closing } + if (JSON_HEDLEY_LIKELY(last_token == token_type::end_object)) + { + if (JSON_HEDLEY_UNLIKELY(not sax->end_object())) + { + return false; + } + + // We are done with this object. Before we can parse a + // new value, we need to evaluate the new state first. + // By setting skip_to_state_evaluation to false, we + // are effectively jumping to the beginning of this if. + assert(not states.empty()); + states.pop_back(); + skip_to_state_evaluation = true; + continue; + } + + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::end_object, "object"))); + } + } + } + + /// get next token from lexer + token_type get_token() + { + return last_token = m_lexer.scan(); + } + + std::string exception_message(const token_type expected, const std::string& context) + { + std::string error_msg = "syntax error "; + + if (not context.empty()) + { + error_msg += "while parsing " + context + " "; + } + + error_msg += "- "; + + if (last_token == token_type::parse_error) + { + error_msg += std::string(m_lexer.get_error_message()) + "; last read: '" + + m_lexer.get_token_string() + "'"; + } + else + { + error_msg += "unexpected " + std::string(lexer_t::token_type_name(last_token)); + } + + if (expected != token_type::uninitialized) + { + error_msg += "; expected " + std::string(lexer_t::token_type_name(expected)); + } + + return error_msg; + } + + private: + /// callback function + const parser_callback_t callback = nullptr; + /// the type of the last read token + token_type last_token = token_type::uninitialized; + /// the lexer + lexer_t m_lexer; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/input/position_t.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/position_t.hpp new file mode 100644 index 000000000..14e9649fb --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/input/position_t.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include // size_t + +namespace nlohmann +{ +namespace detail +{ +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/internal_iterator.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/internal_iterator.hpp new file mode 100644 index 000000000..2c81f723f --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/internal_iterator.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace nlohmann +{ +namespace detail +{ +/*! +@brief an iterator value + +@note This structure could easily be a union, but MSVC currently does not allow +unions members with complex constructors, see https://github.com/nlohmann/json/pull/105. +*/ +template struct internal_iterator +{ + /// iterator for JSON objects + typename BasicJsonType::object_t::iterator object_iterator {}; + /// iterator for JSON arrays + typename BasicJsonType::array_t::iterator array_iterator {}; + /// generic iterator for all other types + primitive_iterator_t primitive_iterator {}; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iter_impl.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iter_impl.hpp new file mode 100644 index 000000000..3a3629719 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iter_impl.hpp @@ -0,0 +1,638 @@ +#pragma once + +#include // not +#include // iterator, random_access_iterator_tag, bidirectional_iterator_tag, advance, next +#include // conditional, is_const, remove_const + +#include +#include +#include +#include +#include +#include +#include + +namespace nlohmann +{ +namespace detail +{ +// forward declare, to be able to friend it later on +template class iteration_proxy; +template class iteration_proxy_value; + +/*! +@brief a template for a bidirectional iterator for the @ref basic_json class +This class implements a both iterators (iterator and const_iterator) for the +@ref basic_json class. +@note An iterator is called *initialized* when a pointer to a JSON value has + been set (e.g., by a constructor or a copy assignment). If the iterator is + default-constructed, it is *uninitialized* and most methods are undefined. + **The library uses assertions to detect calls on uninitialized iterators.** +@requirement The class satisfies the following concept requirements: +- +[BidirectionalIterator](https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator): + The iterator that can be moved can be moved in both directions (i.e. + incremented and decremented). +@since version 1.0.0, simplified in version 2.0.9, change to bidirectional + iterators in version 3.0.0 (see https://github.com/nlohmann/json/issues/593) +*/ +template +class iter_impl +{ + /// allow basic_json to access private members + friend iter_impl::value, typename std::remove_const::type, const BasicJsonType>::type>; + friend BasicJsonType; + friend iteration_proxy; + friend iteration_proxy_value; + + using object_t = typename BasicJsonType::object_t; + using array_t = typename BasicJsonType::array_t; + // make sure BasicJsonType is basic_json or const basic_json + static_assert(is_basic_json::type>::value, + "iter_impl only accepts (const) basic_json"); + + public: + + /// The std::iterator class template (used as a base class to provide typedefs) is deprecated in C++17. + /// The C++ Standard has never required user-defined iterators to derive from std::iterator. + /// A user-defined iterator should provide publicly accessible typedefs named + /// iterator_category, value_type, difference_type, pointer, and reference. + /// Note that value_type is required to be non-const, even for constant iterators. + using iterator_category = std::bidirectional_iterator_tag; + + /// the type of the values when the iterator is dereferenced + using value_type = typename BasicJsonType::value_type; + /// a type to represent differences between iterators + using difference_type = typename BasicJsonType::difference_type; + /// defines a pointer to the type iterated over (value_type) + using pointer = typename std::conditional::value, + typename BasicJsonType::const_pointer, + typename BasicJsonType::pointer>::type; + /// defines a reference to the type iterated over (value_type) + using reference = + typename std::conditional::value, + typename BasicJsonType::const_reference, + typename BasicJsonType::reference>::type; + + /// default constructor + iter_impl() = default; + + /*! + @brief constructor for a given JSON instance + @param[in] object pointer to a JSON object for this iterator + @pre object != nullptr + @post The iterator is initialized; i.e. `m_object != nullptr`. + */ + explicit iter_impl(pointer object) noexcept : m_object(object) + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + m_it.object_iterator = typename object_t::iterator(); + break; + } + + case value_t::array: + { + m_it.array_iterator = typename array_t::iterator(); + break; + } + + default: + { + m_it.primitive_iterator = primitive_iterator_t(); + break; + } + } + } + + /*! + @note The conventional copy constructor and copy assignment are implicitly + defined. Combined with the following converting constructor and + assignment, they support: (1) copy from iterator to iterator, (2) + copy from const iterator to const iterator, and (3) conversion from + iterator to const iterator. However conversion from const iterator + to iterator is not defined. + */ + + /*! + @brief const copy constructor + @param[in] other const iterator to copy from + @note This copy constructor had to be defined explicitly to circumvent a bug + occurring on msvc v19.0 compiler (VS 2015) debug build. For more + information refer to: https://github.com/nlohmann/json/issues/1608 + */ + iter_impl(const iter_impl& other) noexcept + : m_object(other.m_object), m_it(other.m_it) + {} + + /*! + @brief converting assignment + @param[in] other const iterator to copy from + @return const/non-const iterator + @note It is not checked whether @a other is initialized. + */ + iter_impl& operator=(const iter_impl& other) noexcept + { + m_object = other.m_object; + m_it = other.m_it; + return *this; + } + + /*! + @brief converting constructor + @param[in] other non-const iterator to copy from + @note It is not checked whether @a other is initialized. + */ + iter_impl(const iter_impl::type>& other) noexcept + : m_object(other.m_object), m_it(other.m_it) + {} + + /*! + @brief converting assignment + @param[in] other non-const iterator to copy from + @return const/non-const iterator + @note It is not checked whether @a other is initialized. + */ + iter_impl& operator=(const iter_impl::type>& other) noexcept + { + m_object = other.m_object; + m_it = other.m_it; + return *this; + } + + private: + /*! + @brief set the iterator to the first value + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + void set_begin() noexcept + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + m_it.object_iterator = m_object->m_value.object->begin(); + break; + } + + case value_t::array: + { + m_it.array_iterator = m_object->m_value.array->begin(); + break; + } + + case value_t::null: + { + // set to end so begin()==end() is true: null is empty + m_it.primitive_iterator.set_end(); + break; + } + + default: + { + m_it.primitive_iterator.set_begin(); + break; + } + } + } + + /*! + @brief set the iterator past the last value + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + void set_end() noexcept + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + m_it.object_iterator = m_object->m_value.object->end(); + break; + } + + case value_t::array: + { + m_it.array_iterator = m_object->m_value.array->end(); + break; + } + + default: + { + m_it.primitive_iterator.set_end(); + break; + } + } + } + + public: + /*! + @brief return a reference to the value pointed to by the iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + reference operator*() const + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + assert(m_it.object_iterator != m_object->m_value.object->end()); + return m_it.object_iterator->second; + } + + case value_t::array: + { + assert(m_it.array_iterator != m_object->m_value.array->end()); + return *m_it.array_iterator; + } + + case value_t::null: + JSON_THROW(invalid_iterator::create(214, "cannot get value")); + + default: + { + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) + { + return *m_object; + } + + JSON_THROW(invalid_iterator::create(214, "cannot get value")); + } + } + } + + /*! + @brief dereference the iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + pointer operator->() const + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + assert(m_it.object_iterator != m_object->m_value.object->end()); + return &(m_it.object_iterator->second); + } + + case value_t::array: + { + assert(m_it.array_iterator != m_object->m_value.array->end()); + return &*m_it.array_iterator; + } + + default: + { + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) + { + return m_object; + } + + JSON_THROW(invalid_iterator::create(214, "cannot get value")); + } + } + } + + /*! + @brief post-increment (it++) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl const operator++(int) + { + auto result = *this; + ++(*this); + return result; + } + + /*! + @brief pre-increment (++it) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator++() + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + std::advance(m_it.object_iterator, 1); + break; + } + + case value_t::array: + { + std::advance(m_it.array_iterator, 1); + break; + } + + default: + { + ++m_it.primitive_iterator; + break; + } + } + + return *this; + } + + /*! + @brief post-decrement (it--) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl const operator--(int) + { + auto result = *this; + --(*this); + return result; + } + + /*! + @brief pre-decrement (--it) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator--() + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + std::advance(m_it.object_iterator, -1); + break; + } + + case value_t::array: + { + std::advance(m_it.array_iterator, -1); + break; + } + + default: + { + --m_it.primitive_iterator; + break; + } + } + + return *this; + } + + /*! + @brief comparison: equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator==(const iter_impl& other) const + { + // if objects are not the same, the comparison is undefined + if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) + { + JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers")); + } + + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + return (m_it.object_iterator == other.m_it.object_iterator); + + case value_t::array: + return (m_it.array_iterator == other.m_it.array_iterator); + + default: + return (m_it.primitive_iterator == other.m_it.primitive_iterator); + } + } + + /*! + @brief comparison: not equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator!=(const iter_impl& other) const + { + return not operator==(other); + } + + /*! + @brief comparison: smaller + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator<(const iter_impl& other) const + { + // if objects are not the same, the comparison is undefined + if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) + { + JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers")); + } + + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(213, "cannot compare order of object iterators")); + + case value_t::array: + return (m_it.array_iterator < other.m_it.array_iterator); + + default: + return (m_it.primitive_iterator < other.m_it.primitive_iterator); + } + } + + /*! + @brief comparison: less than or equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator<=(const iter_impl& other) const + { + return not other.operator < (*this); + } + + /*! + @brief comparison: greater than + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator>(const iter_impl& other) const + { + return not operator<=(other); + } + + /*! + @brief comparison: greater than or equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator>=(const iter_impl& other) const + { + return not operator<(other); + } + + /*! + @brief add to iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator+=(difference_type i) + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators")); + + case value_t::array: + { + std::advance(m_it.array_iterator, i); + break; + } + + default: + { + m_it.primitive_iterator += i; + break; + } + } + + return *this; + } + + /*! + @brief subtract from iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator-=(difference_type i) + { + return operator+=(-i); + } + + /*! + @brief add to iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl operator+(difference_type i) const + { + auto result = *this; + result += i; + return result; + } + + /*! + @brief addition of distance and iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + friend iter_impl operator+(difference_type i, const iter_impl& it) + { + auto result = it; + result += i; + return result; + } + + /*! + @brief subtract from iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl operator-(difference_type i) const + { + auto result = *this; + result -= i; + return result; + } + + /*! + @brief return difference + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + difference_type operator-(const iter_impl& other) const + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators")); + + case value_t::array: + return m_it.array_iterator - other.m_it.array_iterator; + + default: + return m_it.primitive_iterator - other.m_it.primitive_iterator; + } + } + + /*! + @brief access to successor + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + reference operator[](difference_type n) const + { + assert(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(208, "cannot use operator[] for object iterators")); + + case value_t::array: + return *std::next(m_it.array_iterator, n); + + case value_t::null: + JSON_THROW(invalid_iterator::create(214, "cannot get value")); + + default: + { + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.get_value() == -n)) + { + return *m_object; + } + + JSON_THROW(invalid_iterator::create(214, "cannot get value")); + } + } + } + + /*! + @brief return the key of an object iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + const typename object_t::key_type& key() const + { + assert(m_object != nullptr); + + if (JSON_HEDLEY_LIKELY(m_object->is_object())) + { + return m_it.object_iterator->first; + } + + JSON_THROW(invalid_iterator::create(207, "cannot use key() for non-object iterators")); + } + + /*! + @brief return the value of an iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + reference value() const + { + return operator*(); + } + + private: + /// associated JSON instance + pointer m_object = nullptr; + /// the actual iterator of the associated instance + internal_iterator::type> m_it {}; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iteration_proxy.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iteration_proxy.hpp new file mode 100644 index 000000000..c61d96296 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iteration_proxy.hpp @@ -0,0 +1,176 @@ +#pragma once + +#include // size_t +#include // input_iterator_tag +#include // string, to_string +#include // tuple_size, get, tuple_element + +#include +#include + +namespace nlohmann +{ +namespace detail +{ +template +void int_to_string( string_type& target, std::size_t value ) +{ + target = std::to_string(value); +} +template class iteration_proxy_value +{ + public: + using difference_type = std::ptrdiff_t; + using value_type = iteration_proxy_value; + using pointer = value_type * ; + using reference = value_type & ; + using iterator_category = std::input_iterator_tag; + using string_type = typename std::remove_cv< typename std::remove_reference().key() ) >::type >::type; + + private: + /// the iterator + IteratorType anchor; + /// an index for arrays (used to create key names) + std::size_t array_index = 0; + /// last stringified array index + mutable std::size_t array_index_last = 0; + /// a string representation of the array index + mutable string_type array_index_str = "0"; + /// an empty string (to return a reference for primitive values) + const string_type empty_str = ""; + + public: + explicit iteration_proxy_value(IteratorType it) noexcept : anchor(it) {} + + /// dereference operator (needed for range-based for) + iteration_proxy_value& operator*() + { + return *this; + } + + /// increment operator (needed for range-based for) + iteration_proxy_value& operator++() + { + ++anchor; + ++array_index; + + return *this; + } + + /// equality operator (needed for InputIterator) + bool operator==(const iteration_proxy_value& o) const + { + return anchor == o.anchor; + } + + /// inequality operator (needed for range-based for) + bool operator!=(const iteration_proxy_value& o) const + { + return anchor != o.anchor; + } + + /// return key of the iterator + const string_type& key() const + { + assert(anchor.m_object != nullptr); + + switch (anchor.m_object->type()) + { + // use integer array index as key + case value_t::array: + { + if (array_index != array_index_last) + { + int_to_string( array_index_str, array_index ); + array_index_last = array_index; + } + return array_index_str; + } + + // use key from the object + case value_t::object: + return anchor.key(); + + // use an empty key for all primitive types + default: + return empty_str; + } + } + + /// return value of the iterator + typename IteratorType::reference value() const + { + return anchor.value(); + } +}; + +/// proxy class for the items() function +template class iteration_proxy +{ + private: + /// the container to iterate + typename IteratorType::reference container; + + public: + /// construct iteration proxy from a container + explicit iteration_proxy(typename IteratorType::reference cont) noexcept + : container(cont) {} + + /// return iterator begin (needed for range-based for) + iteration_proxy_value begin() noexcept + { + return iteration_proxy_value(container.begin()); + } + + /// return iterator end (needed for range-based for) + iteration_proxy_value end() noexcept + { + return iteration_proxy_value(container.end()); + } +}; +// Structured Bindings Support +// For further reference see https://blog.tartanllama.xyz/structured-bindings/ +// And see https://github.com/nlohmann/json/pull/1391 +template = 0> +auto get(const nlohmann::detail::iteration_proxy_value& i) -> decltype(i.key()) +{ + return i.key(); +} +// Structured Bindings Support +// For further reference see https://blog.tartanllama.xyz/structured-bindings/ +// And see https://github.com/nlohmann/json/pull/1391 +template = 0> +auto get(const nlohmann::detail::iteration_proxy_value& i) -> decltype(i.value()) +{ + return i.value(); +} +} // namespace detail +} // namespace nlohmann + +// The Addition to the STD Namespace is required to add +// Structured Bindings Support to the iteration_proxy_value class +// For further reference see https://blog.tartanllama.xyz/structured-bindings/ +// And see https://github.com/nlohmann/json/pull/1391 +namespace std +{ +#if defined(__clang__) + // Fix: https://github.com/nlohmann/json/issues/1401 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wmismatched-tags" +#endif +template +class tuple_size<::nlohmann::detail::iteration_proxy_value> + : public std::integral_constant {}; + +template +class tuple_element> +{ + public: + using type = decltype( + get(std::declval < + ::nlohmann::detail::iteration_proxy_value> ())); +}; +#if defined(__clang__) + #pragma clang diagnostic pop +#endif +} // namespace std diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iterator_traits.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iterator_traits.hpp new file mode 100644 index 000000000..4cced80ca --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/iterator_traits.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include // random_access_iterator_tag + +#include +#include + +namespace nlohmann +{ +namespace detail +{ +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/json_reverse_iterator.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/json_reverse_iterator.hpp new file mode 100644 index 000000000..f3b5b5db6 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/json_reverse_iterator.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include // ptrdiff_t +#include // reverse_iterator +#include // declval + +namespace nlohmann +{ +namespace detail +{ +////////////////////// +// reverse_iterator // +////////////////////// + +/*! +@brief a template for a reverse iterator class + +@tparam Base the base iterator type to reverse. Valid types are @ref +iterator (to create @ref reverse_iterator) and @ref const_iterator (to +create @ref const_reverse_iterator). + +@requirement The class satisfies the following concept requirements: +- +[BidirectionalIterator](https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator): + The iterator that can be moved can be moved in both directions (i.e. + incremented and decremented). +- [OutputIterator](https://en.cppreference.com/w/cpp/named_req/OutputIterator): + It is possible to write to the pointed-to element (only if @a Base is + @ref iterator). + +@since version 1.0.0 +*/ +template +class json_reverse_iterator : public std::reverse_iterator +{ + public: + using difference_type = std::ptrdiff_t; + /// shortcut to the reverse iterator adapter + using base_iterator = std::reverse_iterator; + /// the reference type for the pointed-to element + using reference = typename Base::reference; + + /// create reverse iterator from iterator + explicit json_reverse_iterator(const typename base_iterator::iterator_type& it) noexcept + : base_iterator(it) {} + + /// create reverse iterator from base class + explicit json_reverse_iterator(const base_iterator& it) noexcept : base_iterator(it) {} + + /// post-increment (it++) + json_reverse_iterator const operator++(int) + { + return static_cast(base_iterator::operator++(1)); + } + + /// pre-increment (++it) + json_reverse_iterator& operator++() + { + return static_cast(base_iterator::operator++()); + } + + /// post-decrement (it--) + json_reverse_iterator const operator--(int) + { + return static_cast(base_iterator::operator--(1)); + } + + /// pre-decrement (--it) + json_reverse_iterator& operator--() + { + return static_cast(base_iterator::operator--()); + } + + /// add to iterator + json_reverse_iterator& operator+=(difference_type i) + { + return static_cast(base_iterator::operator+=(i)); + } + + /// add to iterator + json_reverse_iterator operator+(difference_type i) const + { + return static_cast(base_iterator::operator+(i)); + } + + /// subtract from iterator + json_reverse_iterator operator-(difference_type i) const + { + return static_cast(base_iterator::operator-(i)); + } + + /// return difference + difference_type operator-(const json_reverse_iterator& other) const + { + return base_iterator(*this) - base_iterator(other); + } + + /// access to successor + reference operator[](difference_type n) const + { + return *(this->operator+(n)); + } + + /// return the key of an object iterator + auto key() const -> decltype(std::declval().key()) + { + auto it = --this->base(); + return it.key(); + } + + /// return the value of an iterator + reference value() const + { + auto it = --this->base(); + return it.operator * (); + } +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/primitive_iterator.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/primitive_iterator.hpp new file mode 100644 index 000000000..28d6f1a65 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/iterators/primitive_iterator.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include // ptrdiff_t +#include // numeric_limits + +namespace nlohmann +{ +namespace detail +{ +/* +@brief an iterator for primitive JSON types + +This class models an iterator for primitive JSON types (boolean, number, +string). It's only purpose is to allow the iterator/const_iterator classes +to "iterate" over primitive values. Internally, the iterator is modeled by +a `difference_type` variable. Value begin_value (`0`) models the begin, +end_value (`1`) models past the end. +*/ +class primitive_iterator_t +{ + private: + using difference_type = std::ptrdiff_t; + static constexpr difference_type begin_value = 0; + static constexpr difference_type end_value = begin_value + 1; + + /// iterator as signed integer type + difference_type m_it = (std::numeric_limits::min)(); + + public: + constexpr difference_type get_value() const noexcept + { + return m_it; + } + + /// set iterator to a defined beginning + void set_begin() noexcept + { + m_it = begin_value; + } + + /// set iterator to a defined past the end + void set_end() noexcept + { + m_it = end_value; + } + + /// return whether the iterator can be dereferenced + constexpr bool is_begin() const noexcept + { + return m_it == begin_value; + } + + /// return whether the iterator is at end + constexpr bool is_end() const noexcept + { + return m_it == end_value; + } + + friend constexpr bool operator==(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept + { + return lhs.m_it == rhs.m_it; + } + + friend constexpr bool operator<(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept + { + return lhs.m_it < rhs.m_it; + } + + primitive_iterator_t operator+(difference_type n) noexcept + { + auto result = *this; + result += n; + return result; + } + + friend constexpr difference_type operator-(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept + { + return lhs.m_it - rhs.m_it; + } + + primitive_iterator_t& operator++() noexcept + { + ++m_it; + return *this; + } + + primitive_iterator_t const operator++(int) noexcept + { + auto result = *this; + ++m_it; + return result; + } + + primitive_iterator_t& operator--() noexcept + { + --m_it; + return *this; + } + + primitive_iterator_t const operator--(int) noexcept + { + auto result = *this; + --m_it; + return result; + } + + primitive_iterator_t& operator+=(difference_type n) noexcept + { + m_it += n; + return *this; + } + + primitive_iterator_t& operator-=(difference_type n) noexcept + { + m_it -= n; + return *this; + } +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/json_pointer.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/json_pointer.hpp new file mode 100644 index 000000000..87af34233 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/json_pointer.hpp @@ -0,0 +1,1011 @@ +#pragma once + +#include // all_of +#include // assert +#include // isdigit +#include // accumulate +#include // string +#include // move +#include // vector + +#include +#include +#include + +namespace nlohmann +{ +template +class json_pointer +{ + // allow basic_json to access private members + NLOHMANN_BASIC_JSON_TPL_DECLARATION + friend class basic_json; + + public: + /*! + @brief create JSON pointer + + Create a JSON pointer according to the syntax described in + [Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3). + + @param[in] s string representing the JSON pointer; if omitted, the empty + string is assumed which references the whole JSON value + + @throw parse_error.107 if the given JSON pointer @a s is nonempty and does + not begin with a slash (`/`); see example below + + @throw parse_error.108 if a tilde (`~`) in the given JSON pointer @a s is + not followed by `0` (representing `~`) or `1` (representing `/`); see + example below + + @liveexample{The example shows the construction several valid JSON pointers + as well as the exceptional behavior.,json_pointer} + + @since version 2.0.0 + */ + explicit json_pointer(const std::string& s = "") + : reference_tokens(split(s)) + {} + + /*! + @brief return a string representation of the JSON pointer + + @invariant For each JSON pointer `ptr`, it holds: + @code {.cpp} + ptr == json_pointer(ptr.to_string()); + @endcode + + @return a string representation of the JSON pointer + + @liveexample{The example shows the result of `to_string`.,json_pointer__to_string} + + @since version 2.0.0 + */ + std::string to_string() const + { + return std::accumulate(reference_tokens.begin(), reference_tokens.end(), + std::string{}, + [](const std::string & a, const std::string & b) + { + return a + "/" + escape(b); + }); + } + + /// @copydoc to_string() + operator std::string() const + { + return to_string(); + } + + /*! + @brief append another JSON pointer at the end of this JSON pointer + + @param[in] ptr JSON pointer to append + @return JSON pointer with @a ptr appended + + @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add} + + @complexity Linear in the length of @a ptr. + + @sa @ref operator/=(std::string) to append a reference token + @sa @ref operator/=(std::size_t) to append an array index + @sa @ref operator/(const json_pointer&, const json_pointer&) for a binary operator + + @since version 3.6.0 + */ + json_pointer& operator/=(const json_pointer& ptr) + { + reference_tokens.insert(reference_tokens.end(), + ptr.reference_tokens.begin(), + ptr.reference_tokens.end()); + return *this; + } + + /*! + @brief append an unescaped reference token at the end of this JSON pointer + + @param[in] token reference token to append + @return JSON pointer with @a token appended without escaping @a token + + @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add} + + @complexity Amortized constant. + + @sa @ref operator/=(const json_pointer&) to append a JSON pointer + @sa @ref operator/=(std::size_t) to append an array index + @sa @ref operator/(const json_pointer&, std::size_t) for a binary operator + + @since version 3.6.0 + */ + json_pointer& operator/=(std::string token) + { + push_back(std::move(token)); + return *this; + } + + /*! + @brief append an array index at the end of this JSON pointer + + @param[in] array_index array index to append + @return JSON pointer with @a array_index appended + + @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add} + + @complexity Amortized constant. + + @sa @ref operator/=(const json_pointer&) to append a JSON pointer + @sa @ref operator/=(std::string) to append a reference token + @sa @ref operator/(const json_pointer&, std::string) for a binary operator + + @since version 3.6.0 + */ + json_pointer& operator/=(std::size_t array_index) + { + return *this /= std::to_string(array_index); + } + + /*! + @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer + + @param[in] lhs JSON pointer + @param[in] rhs JSON pointer + @return a new JSON pointer with @a rhs appended to @a lhs + + @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary} + + @complexity Linear in the length of @a lhs and @a rhs. + + @sa @ref operator/=(const json_pointer&) to append a JSON pointer + + @since version 3.6.0 + */ + friend json_pointer operator/(const json_pointer& lhs, + const json_pointer& rhs) + { + return json_pointer(lhs) /= rhs; + } + + /*! + @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer + + @param[in] ptr JSON pointer + @param[in] token reference token + @return a new JSON pointer with unescaped @a token appended to @a ptr + + @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary} + + @complexity Linear in the length of @a ptr. + + @sa @ref operator/=(std::string) to append a reference token + + @since version 3.6.0 + */ + friend json_pointer operator/(const json_pointer& ptr, std::string token) + { + return json_pointer(ptr) /= std::move(token); + } + + /*! + @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer + + @param[in] ptr JSON pointer + @param[in] array_index array index + @return a new JSON pointer with @a array_index appended to @a ptr + + @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary} + + @complexity Linear in the length of @a ptr. + + @sa @ref operator/=(std::size_t) to append an array index + + @since version 3.6.0 + */ + friend json_pointer operator/(const json_pointer& ptr, std::size_t array_index) + { + return json_pointer(ptr) /= array_index; + } + + /*! + @brief returns the parent of this JSON pointer + + @return parent of this JSON pointer; in case this JSON pointer is the root, + the root itself is returned + + @complexity Linear in the length of the JSON pointer. + + @liveexample{The example shows the result of `parent_pointer` for different + JSON Pointers.,json_pointer__parent_pointer} + + @since version 3.6.0 + */ + json_pointer parent_pointer() const + { + if (empty()) + { + return *this; + } + + json_pointer res = *this; + res.pop_back(); + return res; + } + + /*! + @brief remove last reference token + + @pre not `empty()` + + @liveexample{The example shows the usage of `pop_back`.,json_pointer__pop_back} + + @complexity Constant. + + @throw out_of_range.405 if JSON pointer has no parent + + @since version 3.6.0 + */ + void pop_back() + { + if (JSON_HEDLEY_UNLIKELY(empty())) + { + JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent")); + } + + reference_tokens.pop_back(); + } + + /*! + @brief return last reference token + + @pre not `empty()` + @return last reference token + + @liveexample{The example shows the usage of `back`.,json_pointer__back} + + @complexity Constant. + + @throw out_of_range.405 if JSON pointer has no parent + + @since version 3.6.0 + */ + const std::string& back() const + { + if (JSON_HEDLEY_UNLIKELY(empty())) + { + JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent")); + } + + return reference_tokens.back(); + } + + /*! + @brief append an unescaped token at the end of the reference pointer + + @param[in] token token to add + + @complexity Amortized constant. + + @liveexample{The example shows the result of `push_back` for different + JSON Pointers.,json_pointer__push_back} + + @since version 3.6.0 + */ + void push_back(const std::string& token) + { + reference_tokens.push_back(token); + } + + /// @copydoc push_back(const std::string&) + void push_back(std::string&& token) + { + reference_tokens.push_back(std::move(token)); + } + + /*! + @brief return whether pointer points to the root document + + @return true iff the JSON pointer points to the root document + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example shows the result of `empty` for different JSON + Pointers.,json_pointer__empty} + + @since version 3.6.0 + */ + bool empty() const noexcept + { + return reference_tokens.empty(); + } + + private: + /*! + @param[in] s reference token to be converted into an array index + + @return integer representation of @a s + + @throw out_of_range.404 if string @a s could not be converted to an integer + */ + static int array_index(const std::string& s) + { + std::size_t processed_chars = 0; + const int res = std::stoi(s, &processed_chars); + + // check if the string was completely read + if (JSON_HEDLEY_UNLIKELY(processed_chars != s.size())) + { + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'")); + } + + return res; + } + + json_pointer top() const + { + if (JSON_HEDLEY_UNLIKELY(empty())) + { + JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent")); + } + + json_pointer result = *this; + result.reference_tokens = {reference_tokens[0]}; + return result; + } + + /*! + @brief create and return a reference to the pointed to value + + @complexity Linear in the number of reference tokens. + + @throw parse_error.109 if array index is not a number + @throw type_error.313 if value cannot be unflattened + */ + BasicJsonType& get_and_create(BasicJsonType& j) const + { + using size_type = typename BasicJsonType::size_type; + auto result = &j; + + // in case no reference tokens exist, return a reference to the JSON value + // j which will be overwritten by a primitive value + for (const auto& reference_token : reference_tokens) + { + switch (result->type()) + { + case detail::value_t::null: + { + if (reference_token == "0") + { + // start a new array if reference token is 0 + result = &result->operator[](0); + } + else + { + // start a new object otherwise + result = &result->operator[](reference_token); + } + break; + } + + case detail::value_t::object: + { + // create an entry in the object + result = &result->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + // create an entry in the array + JSON_TRY + { + result = &result->operator[](static_cast(array_index(reference_token))); + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + /* + The following code is only reached if there exists a reference + token _and_ the current value is primitive. In this case, we have + an error situation, because primitive values may only occur as + single value; that is, with an empty list of reference tokens. + */ + default: + JSON_THROW(detail::type_error::create(313, "invalid value to unflatten")); + } + } + + return *result; + } + + /*! + @brief return a reference to the pointed to value + + @note This version does not throw if a value is not present, but tries to + create nested values instead. For instance, calling this function + with pointer `"/this/that"` on a null value is equivalent to calling + `operator[]("this").operator[]("that")` on that value, effectively + changing the null value to an object. + + @param[in] ptr a JSON value + + @return reference to the JSON value pointed to by the JSON pointer + + @complexity Linear in the length of the JSON pointer. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + BasicJsonType& get_unchecked(BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + // convert null values to arrays or objects before continuing + if (ptr->is_null()) + { + // check if reference token is a number + const bool nums = + std::all_of(reference_token.begin(), reference_token.end(), + [](const unsigned char x) + { + return std::isdigit(x); + }); + + // change value to array for numbers or "-" or to object otherwise + *ptr = (nums or reference_token == "-") + ? detail::value_t::array + : detail::value_t::object; + } + + switch (ptr->type()) + { + case detail::value_t::object: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + if (reference_token == "-") + { + // explicitly treat "-" as index beyond the end + ptr = &ptr->operator[](ptr->m_value.array->size()); + } + else + { + // convert array index to number; unchecked access + JSON_TRY + { + ptr = &ptr->operator[]( + static_cast(array_index(reference_token))); + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + } + break; + } + + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'")); + } + } + + return *ptr; + } + + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + BasicJsonType& get_checked(BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + JSON_THROW(detail::out_of_range::create(402, + "array index '-' (" + std::to_string(ptr->m_value.array->size()) + + ") is out of range")); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + // note: at performs range check + JSON_TRY + { + ptr = &ptr->at(static_cast(array_index(reference_token))); + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'")); + } + } + + return *ptr; + } + + /*! + @brief return a const reference to the pointed to value + + @param[in] ptr a JSON value + + @return const reference to the JSON value pointed to by the JSON + pointer + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" cannot be used for const access + JSON_THROW(detail::out_of_range::create(402, + "array index '-' (" + std::to_string(ptr->m_value.array->size()) + + ") is out of range")); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + // use unchecked array access + JSON_TRY + { + ptr = &ptr->operator[]( + static_cast(array_index(reference_token))); + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'")); + } + } + + return *ptr; + } + + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + const BasicJsonType& get_checked(const BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + JSON_THROW(detail::out_of_range::create(402, + "array index '-' (" + std::to_string(ptr->m_value.array->size()) + + ") is out of range")); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + // note: at performs range check + JSON_TRY + { + ptr = &ptr->at(static_cast(array_index(reference_token))); + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'")); + } + } + + return *ptr; + } + + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + */ + bool contains(const BasicJsonType* ptr) const + { + using size_type = typename BasicJsonType::size_type; + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + if (not ptr->contains(reference_token)) + { + // we did not find the key in the object + return false; + } + + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + return false; + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1 and reference_token[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, + "array index '" + reference_token + + "' must not begin with '0'")); + } + + JSON_TRY + { + const auto idx = static_cast(array_index(reference_token)); + if (idx >= ptr->size()) + { + // index out of range + return false; + } + + ptr = &ptr->operator[](idx); + break; + } + JSON_CATCH(std::invalid_argument&) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); + } + break; + } + + default: + { + // we do not expect primitive values if there is still a + // reference token to process + return false; + } + } + } + + // no reference token left means we found a primitive value + return true; + } + + /*! + @brief split the string input to reference tokens + + @note This function is only called by the json_pointer constructor. + All exceptions below are documented there. + + @throw parse_error.107 if the pointer is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' + */ + static std::vector split(const std::string& reference_string) + { + std::vector result; + + // special case: empty reference string -> no reference tokens + if (reference_string.empty()) + { + return result; + } + + // check if nonempty reference string begins with slash + if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/')) + { + JSON_THROW(detail::parse_error::create(107, 1, + "JSON pointer must be empty or begin with '/' - was: '" + + reference_string + "'")); + } + + // extract the reference tokens: + // - slash: position of the last read slash (or end of string) + // - start: position after the previous slash + for ( + // search for the first slash after the first character + std::size_t slash = reference_string.find_first_of('/', 1), + // set the beginning of the first reference token + start = 1; + // we can stop if start == 0 (if slash == std::string::npos) + start != 0; + // set the beginning of the next reference token + // (will eventually be 0 if slash == std::string::npos) + start = (slash == std::string::npos) ? 0 : slash + 1, + // find next slash + slash = reference_string.find_first_of('/', start)) + { + // use the text between the beginning of the reference token + // (start) and the last slash (slash). + auto reference_token = reference_string.substr(start, slash - start); + + // check reference tokens are properly escaped + for (std::size_t pos = reference_token.find_first_of('~'); + pos != std::string::npos; + pos = reference_token.find_first_of('~', pos + 1)) + { + assert(reference_token[pos] == '~'); + + // ~ must be followed by 0 or 1 + if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 or + (reference_token[pos + 1] != '0' and + reference_token[pos + 1] != '1'))) + { + JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'")); + } + } + + // finally, store the reference token + unescape(reference_token); + result.push_back(reference_token); + } + + return result; + } + + /*! + @brief replace all occurrences of a substring by another string + + @param[in,out] s the string to manipulate; changed so that all + occurrences of @a f are replaced with @a t + @param[in] f the substring to replace with @a t + @param[in] t the string to replace @a f + + @pre The search string @a f must not be empty. **This precondition is + enforced with an assertion.** + + @since version 2.0.0 + */ + static void replace_substring(std::string& s, const std::string& f, + const std::string& t) + { + assert(not f.empty()); + for (auto pos = s.find(f); // find first occurrence of f + pos != std::string::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t, and + pos = s.find(f, pos + t.size())) // find next occurrence of f + {} + } + + /// escape "~" to "~0" and "/" to "~1" + static std::string escape(std::string s) + { + replace_substring(s, "~", "~0"); + replace_substring(s, "/", "~1"); + return s; + } + + /// unescape "~1" to tilde and "~0" to slash (order is important!) + static void unescape(std::string& s) + { + replace_substring(s, "~1", "/"); + replace_substring(s, "~0", "~"); + } + + /*! + @param[in] reference_string the reference string to the current value + @param[in] value the value to consider + @param[in,out] result the result object to insert values to + + @note Empty objects or arrays are flattened to `null`. + */ + static void flatten(const std::string& reference_string, + const BasicJsonType& value, + BasicJsonType& result) + { + switch (value.type()) + { + case detail::value_t::array: + { + if (value.m_value.array->empty()) + { + // flatten empty array as null + result[reference_string] = nullptr; + } + else + { + // iterate array and use index as reference string + for (std::size_t i = 0; i < value.m_value.array->size(); ++i) + { + flatten(reference_string + "/" + std::to_string(i), + value.m_value.array->operator[](i), result); + } + } + break; + } + + case detail::value_t::object: + { + if (value.m_value.object->empty()) + { + // flatten empty object as null + result[reference_string] = nullptr; + } + else + { + // iterate object and use keys as reference string + for (const auto& element : *value.m_value.object) + { + flatten(reference_string + "/" + escape(element.first), element.second, result); + } + } + break; + } + + default: + { + // add primitive value with its reference string + result[reference_string] = value; + break; + } + } + } + + /*! + @param[in] value flattened JSON + + @return unflattened JSON + + @throw parse_error.109 if array index is not a number + @throw type_error.314 if value is not an object + @throw type_error.315 if object values are not primitive + @throw type_error.313 if value cannot be unflattened + */ + static BasicJsonType + unflatten(const BasicJsonType& value) + { + if (JSON_HEDLEY_UNLIKELY(not value.is_object())) + { + JSON_THROW(detail::type_error::create(314, "only objects can be unflattened")); + } + + BasicJsonType result; + + // iterate the JSON object values + for (const auto& element : *value.m_value.object) + { + if (JSON_HEDLEY_UNLIKELY(not element.second.is_primitive())) + { + JSON_THROW(detail::type_error::create(315, "values in object must be primitive")); + } + + // assign value to reference pointed to by JSON pointer; Note that if + // the JSON pointer is "" (i.e., points to the whole value), function + // get_and_create returns a reference to result itself. An assignment + // will then create a primitive value. + json_pointer(element.first).get_and_create(result) = element.second; + } + + return result; + } + + /*! + @brief compares two JSON pointers for equality + + @param[in] lhs JSON pointer to compare + @param[in] rhs JSON pointer to compare + @return whether @a lhs is equal to @a rhs + + @complexity Linear in the length of the JSON pointer + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + */ + friend bool operator==(json_pointer const& lhs, + json_pointer const& rhs) noexcept + { + return lhs.reference_tokens == rhs.reference_tokens; + } + + /*! + @brief compares two JSON pointers for inequality + + @param[in] lhs JSON pointer to compare + @param[in] rhs JSON pointer to compare + @return whether @a lhs is not equal @a rhs + + @complexity Linear in the length of the JSON pointer + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + */ + friend bool operator!=(json_pointer const& lhs, + json_pointer const& rhs) noexcept + { + return not (lhs == rhs); + } + + /// the reference tokens + std::vector reference_tokens; +}; +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/json_ref.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/json_ref.hpp new file mode 100644 index 000000000..c8dec7330 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/json_ref.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include + +namespace nlohmann +{ +namespace detail +{ +template +class json_ref +{ + public: + using value_type = BasicJsonType; + + json_ref(value_type&& value) + : owned_value(std::move(value)), value_ref(&owned_value), is_rvalue(true) + {} + + json_ref(const value_type& value) + : value_ref(const_cast(&value)), is_rvalue(false) + {} + + json_ref(std::initializer_list init) + : owned_value(init), value_ref(&owned_value), is_rvalue(true) + {} + + template < + class... Args, + enable_if_t::value, int> = 0 > + json_ref(Args && ... args) + : owned_value(std::forward(args)...), value_ref(&owned_value), + is_rvalue(true) {} + + // class should be movable only + json_ref(json_ref&&) = default; + json_ref(const json_ref&) = delete; + json_ref& operator=(const json_ref&) = delete; + json_ref& operator=(json_ref&&) = delete; + ~json_ref() = default; + + value_type moved_or_copied() const + { + if (is_rvalue) + { + return std::move(*value_ref); + } + return *value_ref; + } + + value_type const& operator*() const + { + return *static_cast(value_ref); + } + + value_type const* operator->() const + { + return static_cast(value_ref); + } + + private: + mutable value_type owned_value = nullptr; + value_type* value_ref = nullptr; + const bool is_rvalue; +}; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/macro_scope.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/macro_scope.hpp new file mode 100644 index 000000000..2be7581d1 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/macro_scope.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include // pair +#include + +// This file contains all internal macro definitions +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 +#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 +#endif + +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdocumentation" +#endif + +// allow to disable exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/macro_unscope.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/macro_unscope.hpp new file mode 100644 index 000000000..80b293e7d --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/macro_unscope.hpp @@ -0,0 +1,21 @@ +#pragma once + +// restore GCC/clang diagnostic settings +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic pop +#endif +#if defined(__clang__) + #pragma GCC diagnostic pop +#endif + +// clean up +#undef JSON_INTERNAL_CATCH +#undef JSON_CATCH +#undef JSON_THROW +#undef JSON_TRY +#undef JSON_HAS_CPP_14 +#undef JSON_HAS_CPP_17 +#undef NLOHMANN_BASIC_JSON_TPL_DECLARATION +#undef NLOHMANN_BASIC_JSON_TPL + +#include diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/meta/cpp_future.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/meta/cpp_future.hpp new file mode 100644 index 000000000..948cd4fb0 --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/meta/cpp_future.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include // not +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type + +namespace nlohmann +{ +namespace detail +{ +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +template +using uncvref_t = typename std::remove_cv::type>::type; + +// implementation of C++14 index_sequence and affiliates +// source: https://stackoverflow.com/a/32223343 +template +struct index_sequence +{ + using type = index_sequence; + using value_type = std::size_t; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +template +struct merge_and_renumber; + +template +struct merge_and_renumber, index_sequence> + : index_sequence < I1..., (sizeof...(I1) + I2)... > {}; + +template +struct make_index_sequence + : merge_and_renumber < typename make_index_sequence < N / 2 >::type, + typename make_index_sequence < N - N / 2 >::type > {}; + +template<> struct make_index_sequence<0> : index_sequence<> {}; +template<> struct make_index_sequence<1> : index_sequence<0> {}; + +template +using index_sequence_for = make_index_sequence; + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static constexpr T value{}; +}; + +template +constexpr T static_const::value; +} // namespace detail +} // namespace nlohmann diff --git a/libraries/cppdap/third_party/json/include/nlohmann/detail/meta/detected.hpp b/libraries/cppdap/third_party/json/include/nlohmann/detail/meta/detected.hpp new file mode 100644 index 000000000..5b52460ac --- /dev/null +++ b/libraries/cppdap/third_party/json/include/nlohmann/detail/meta/detected.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +// http://en.cppreference.com/w/cpp/experimental/is_detected +namespace nlohmann +{ +namespace detail +{ +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template

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