Online Party self-syncing Expression Components
A set of scripts to make your own self-syncing ExpressionComponents with no need for OPE at all.
Free yourself from the bonds of Online Party Expressions and let your own mod decide how your expressions will play out! Includes example of GameMod Class and Superclass for your ExpressionComponent. I don't recommend changing any of pre-defined functions - only change the Class names. This set of scripts allows Hat_GhostPartyPlayerStateBase's meta data to be used as container for information about player's expression: CurrentExpression and Eyes.CurrentEyesOrigin (where player looks). Online Party Expressions has not been updated for around 2 years and people have troubles syncing expressions, so this mod makes up for it. In order for CurrentExpression and Eyes.CurrentEyesOrigin sync and to be able to Update (like Tick) your expression, your ExpressionComponent has to extend included Shara_ExpressionComponent Class (that should also have different Name). Attaching your ExpressionComponent (and using Init() afterwards in case you didn't know that should be done, lol) will allow ExpressionComponet to read meta data for Online Player's player state and change expression according to real player's current expression and also have Online Player look wherever real player looks. It should be noted that this mod does not fully replace Online Party Expressions - it only allows you to create your own ExpressionComponents that will sync even without OPE. OPE will still be required for older mods and for default Hat Kid's and Bow Kid's ExpressionComponents. The ExpressionComponent included in this distribution also has ability to change EyesIndex and FaceIndex depending on its MeshComponent (from GetOwnerMesh function). I've already included all EyesIndex/FaceIndex cases for base game and DLCs Meshes, however you can add your own as well. Check functions GetEyesIndex and GetFaceIndex. WARNING: Please DO NOT change EyesOriginKey and ExpressionKey const variables to prevent collecting garbage data inside player state meta data - even if all mods will use the same keys, the expressions will still be synced, because Superclass Shara_ExpressionComponent checks if it's exactly the current used ExpressionComponent or not. If not - nothing will be written and nothing will be modified. More info about what each function does is inside the script themselves.
class Shara_ExpressionComponent extends Hat_ExpressionComponent abstract; /* All functions are written by Shararamosh. Please change Class Name (Shara_ExpressionComponent) and GameMod Class Name (Shara_GameMod) to your ones when using it. This Class should be used as superclass of your expression component. Please don't edit anything besides Class Name unless you know what you're doing. Last edited: 02.04.2023 5:45 GMT+3. */ //Please don't change the const keys below - that way we won't have garbage meta data since all mods will use same meta data keys. const EyesOriginKey = 'Player_EyesOrigin'; const ExpressionKey = 'Player_Expression'; function Init(optional Actor DebugCaller) //We need something to Update OP's expression component and that is our ModInstance. { local Shara_GameMod ModInstance; //MODIFIED HAT_EXPRESSIONCOMPONENT CODE START!!! if (!bAttached) { if (class'Engine'.static.IsEditor()) `Broadcast(string(self)$": component not attached to owner. Owner: "$(DebugCaller != None ? DebugCaller : Owner)); return; } if (GetOwnerMesh() == None) { if (class'Engine'.static.IsEditor()) `Broadcast(string(self)$": Unable to get owner mesh for owner: "$(DebugCaller != None ? DebugCaller : Owner)); return; } if (!IsSimpleFaceExpression()) CreateMaterial_Eyes(GetEyesIndex()); CreateMaterial_Face(GetFaceIndex()); ResetEyesMaterial(); ResetFaceMaterial(); ResetExpression(); //MODIFIED HAT_EXPRESSIONCOMPONENT CODE STOP!!! if (Hat_GhostPartyPlayer(Owner) == None) return; ModInstance = class'Shara_GameMod'.static.GetModInstance(); if (ModInstance != None) ModInstance.AddGhostPartyExpression(self); } function MaterialInstance CreateMaterial_Eyes(int index) //Modified to NOT create unnecessary MaterialInstances. { local MaterialInterface Parent; if (index < 0) return None; Parent = GetMeshFirstMaterial(index); if (Parent == None) { if (class'Engine'.static.IsEditor()) `broadcast(self$": Unable to find eye material at index "$index$" on actor "$Owner$"!"); return None; } Eyes.Material = ConditionalInitMaterialInstanceNoMesh(Parent); return Eyes.Material; } function ResetEyesMaterial(optional MaterialInstance c) //Modified to work with GetEyesIndex. { if (IsSimpleFaceExpression()) { ResetFaceMaterial(c); return; } if (c == None) c = Eyes.Material; SetMaterialAllComponents(c, GetEyesIndex()); } function MaterialInstance CreateMaterial_Face(int index) //Modified to NOT create unnecessary MaterialInstances. { local MaterialInterface Parent; if (index < 0) return None; Parent = GetMeshFirstMaterial(index); if (Parent == None) { `broadcast(self$": Unable to find face material at index "$index$" on actor "$Owner$"!"); return None; } Face.Material = ConditionalInitMaterialInstanceNoMesh(Parent); return Face.Material; } function ResetFaceMaterial(optional MaterialInstance c) //Modified to work with GetFaceIndex(). { if (c == None) c = Face.Material; SetMaterialAllComponents(c, GetFaceIndex()); } function MeshComponent GetOwnerMesh() //Returning OP's SkeletalMeshComponent too. { local MeshComponent comp; local Hat_GhostPartyPlayer gpp; comp = Super.GetOwnerMesh(); if (comp == None) { gpp = Hat_GhostPartyPlayer(Owner); if (gpp != None) return gpp.SkeletalMeshComponent; } return comp; } simulated function int GetEyesIndex() //Returns EyesIndex that fits actual MeshComponent used by Owner. { local SkeletalMeshComponent comp; comp = SkeletalMeshComponent(GetOwnerMesh()); if (comp == None) return EyeIndex; switch(comp.SkeletalMesh) { case SkeletalMesh'HatInTime_Characters_HatKid.models.HatKidHead': return 4; case SkeletalMesh'HatInTime_Characters_Coop.models.bowkid_head_skm': return 0; case SkeletalMesh'HatinTime_GhostParty.SkeletalMeshes.OneMeshHatKid': return 6; case SkeletalMesh'HatinTime_GhostParty.SkeletalMeshes.bowkid_all': return 1; case SkeletalMesh'HatInTime_Characters_MuGirl.models.MuGirl': return 0; case SkeletalMesh'HatinTime_Characters_CoPartner.models.CoPartner': return 4; case SkeletalMesh'HatInTime_Costumes2.models.hk64_head': return -1; case SkeletalMesh'HatInTime_Costumes2.models.bow_kid_64_head': return -1; case SkeletalMesh'HatInTime_Costumes2.hk64_single': return -1; case SkeletalMesh'HatinTime_GhostParty.SkeletalMeshes.bow_kid_64_all': return -1; case SkeletalMesh'Vanessa_Tag_Cosmetics.Punk.CapEX_HKHead': return 4; case SkeletalMesh'Vanessa_Tag_Cosmetics.Punk.CapEX_BKHead': return 0; case SkeletalMesh'Vanessa_Tag_Cosmetics.hkHEAD': return 4; case SkeletalMesh'HatInTime_WireframeSkin.models.Wireframe_HatKid_Head': return 4; case SkeletalMesh'HatInTime_WireframeSkin.models.Wireframe_BowKid_Head': return 0; default: return EyeIndex; } } function bool IsSimpleFaceExpression() //Modified to work with GetEyesIndex and GetFaceIndex(). { local int EIndex; EIndex = GetEyesIndex(); if (EIndex < 0) return false; if (EIndex == GetFaceIndex()) return true; return false; } simulated function int GetFaceIndex() //Returns FaceIndex that fits actual MeshComponent used by Owner. { local SkeletalMeshComponent comp; comp = SkeletalMeshComponent(GetOwnerMesh()); if (comp == None) return FaceIndex; switch(comp.SkeletalMesh) { case SkeletalMesh'HatInTime_Characters_HatKid.models.HatKidHead': return 0; case SkeletalMesh'HatInTime_Characters_Coop.models.bowkid_head_skm': return 1; case SkeletalMesh'HatinTime_GhostParty.SkeletalMeshes.OneMeshHatKid': return 5; case SkeletalMesh'HatinTime_GhostParty.SkeletalMeshes.bowkid_all': return 2; case SkeletalMesh'HatInTime_Characters_MuGirl.models.MuGirl': return 1; case SkeletalMesh'HatinTime_Characters_CoPartner.models.CoPartner': return 3; case SkeletalMesh'HatInTime_Costumes2.models.hk64_head': return -1; case SkeletalMesh'HatInTime_Costumes2.models.bow_kid_64_head': return -1; case SkeletalMesh'HatInTime_Costumes2.hk64_single': return -1; case SkeletalMesh'HatinTime_GhostParty.SkeletalMeshes.bow_kid_64_all': return -1; case SkeletalMesh'Vanessa_Tag_Cosmetics.Punk.CapEX_HKHead': return 0; case SkeletalMesh'Vanessa_Tag_Cosmetics.Punk.CapEX_BKHead': return 1; case SkeletalMesh'Vanessa_Tag_Cosmetics.hkHEAD': return 0; case SkeletalMesh'HatInTime_WireframeSkin.models.Wireframe_HatKid_Head': return 0; case SkeletalMesh'HatInTime_WireframeSkin.models.Wireframe_BowKid_Head': return 1; default: return FaceIndex; } } function Update(float DeltaTime, optional bool Optimize = true) //In case of LP: writing expression info to meta data. In case of OP: reading expression info from meta data and applying it. { local EExpressionType OnlineExpression; if (!SetLocalPlayerExpressionMetaData()) { if (ReadOnlinePlayerExpressionMetaData(OnlineExpression)) { if (CurrentExpression != OnlineExpression) SetExpression(OnlineExpression); } } Super.Update(DeltaTime, Optimize); } function UpdateEyeSeek(float DeltaTime) //Updated and optimized some of the stuff. At the end it changes look direction for OP to the one from meta data. { local Rotator newRot, MyRotation; local float Rate; local Vector vMyOrigin, vOtherOrigin; local MeshComponent comp; local SkeletalMeshComponent skelComp; local StaticMeshComponent statComp; if (Eyes.NextSeek <= 0.0) { Eyes.NextSeek = RandRange(1.0, 2.2); Eyes.SeekObject = GetPointOfInterestTarget(); if (ForcedViewTarget != None) Eyes.SeekObject = ForcedViewTarget; if (Eyes.SeekObject == None) { if (EnableEyeSeek) { Eyes.NextSeekRest--; if (Eyes.NextSeekRest <= 0.0) { newRot = rot(0, 0, 0); Eyes.NextSeekRest = Rand(3)+2; } else { newRot.Pitch = RandRange(-8192, 8192); newRot.Yaw = RandRange(-8192, 8192); // Only deviate 50% away from our current origin newRot = class'Hat_Math'.static.RLerpShortest(Eyes.SeekOrigin, newRot, 0.5); } } Eyes.SeekOrigin = newRot; } } Rate = FMin(Abs(DeltaTime)*15.0, 1.0); if (Owner != None) { if (Eyes.ForcedLookSocket != '' || Eyes.ForcedLookBone != '' || Eyes.ForcedLookVector.X != 0.0 || Eyes.ForcedLookVector.Y != 0.0 || Eyes.ForcedLookVector.Z != 0.0) { vMyOrigin = GetOriginLocation(Owner); MyRotation = GetOriginRotation(Owner); vOtherOrigin = vMyOrigin+TransformVectorByRotation(Owner.Rotation, Normal(Eyes.ForcedLookVector)*200.0); if (Eyes.ForcedLookSocket != '' || Eyes.ForcedLookBone != '') { comp = GetOwnerMesh(); if (comp != None) { skelComp = SkeletalMeshComponent(comp); if (skelComp != None) { if (Eyes.ForcedLookSocket != '') { if (skelComp.GetSocketByName(Eyes.ForcedLookSocket) != None) skelComp.GetSocketWorldLocationAndRotation(Eyes.ForcedLookSocket, vOtherOrigin, newRot); } else if (Eyes.ForcedLookBone != '') { if (skelComp.MatchRefBone(Eyes.ForcedLookBone) != INDEX_NONE) skelComp.GetBoneLocation(Eyes.ForcedLookBone); } } else { statComp = StaticMeshComponent(comp); if (statComp != None && Eyes.ForcedLookSocket != '') { if (statComp.GetSocketByName(Eyes.ForcedLookSocket) != None) statComp.GetSocketWorldLocationAndRotation(Eyes.ForcedLookSocket, vOtherOrigin, newRot); } } } } Eyes.SeekOrigin = Rotator(vOtherOrigin-vMyOrigin)-MyRotation; Rate = DebugDrawEyeLines ? 1.0 : FMin(10.0*Abs(DeltaTime), 1.0); if (!Owner.bHidden && DebugDrawEyeLines) { Owner.DrawDebugLine(vOtherOrigin, vMyOrigin, 255, 25, 25, false); Owner.DrawDebugSphere(vOtherOrigin, 2.0, 32, 255, 25, 25, false); } } else if (Eyes.SeekObject != None) { vMyOrigin = GetOriginLocation(Owner); MyRotation = GetOriginRotation(Owner); vOtherOrigin = GetOriginLocation(Eyes.SeekObject); Rate = FMin(Abs(DeltaTime)*20.0, 1.0); if (Camera(Eyes.SeekObject) != None) Rate = 1.0; if (Eyes.SeekObject == ForcedViewTarget) Rate = 1.0; if (!Owner.bHidden && DebugDrawEyeLines) { Rate = 1.0; Owner.DrawDebugLine(vOtherOrigin, vMyOrigin, 255, 25, 25, false); Owner.DrawDebugSphere(vOtherOrigin, 2.0, 32, 255, 25, 25, false); } Eyes.SeekOrigin = Rotator(vOtherOrigin-vMyOrigin)-MyRotation; } if (!SetLocalPlayerEyesMetaData()) { if (ReadOnlinePlayerEyesMetaData(Eyes.SeekOrigin)) Rate = 1.0; } } if (Eyes.CurrentEyesOrigin.Yaw == Eyes.SeekOrigin.Yaw && Eyes.CurrentEyesOrigin.Pitch == Eyes.SeekOrigin.Pitch) return; Eyes.CurrentEyesOrigin = class'Hat_Math'.static.RLerpShortest(Eyes.CurrentEyesOrigin, Eyes.SeekOrigin, Rate); UpdateEyeOffset(); } static function Vector GetOriginLocation(Actor a) //Merged chunk of code that returns view point of supported Actor. { local Pawn p; local Hat_NPC npc; local Hat_InteractInterface ii; local Camera c; local Vector v; local Rotator r; if (a == None) return vect(0.0, 0.0, 0.0); p = Pawn(a); if (p != None) return p.GetPawnViewLocation(); npc = Hat_NPC(a); if (npc != None) return npc.GetPawnViewLocation(); ii = Hat_InteractInterface(a); if (ii != None) { ii.GetTargetedViewLocation(v); return v; } c = Camera(a); if (c != None) { c.GetCameraViewPoint(v, r); return v; } return a.Location; } static function Rotator GetOriginRotation(Actor a) //Merged chunk of code that returns view rotation of supported Actor. { local Hat_NPC npc; if (a == None) return rot(0, 0, 0); npc = Hat_NPC(a); if (npc != None) return npc.GetPawnViewRotation(); return a.Rotation; } simulated function bool SetLocalPlayerEyesMetaData() //Function that writes look direction into LP's meta data. Won't do anything if this particular ExpressionComponent is not ply.ExpressionComponent. { local Hat_Player ply; local Hat_GhostPartyPlayerStateBase PlayerState; local int PlayerIndex; local string s; ply = Hat_Player(Owner); if (ply == None) return false; if (ply.ExpressionComponent != self) //This ExpressionComponent is not actually used by this Player, so we don't modify meta data. return true; PlayerIndex = GetPawnPlayerIndex(ply); if (PlayerIndex < 0) return true; PlayerState = class'Hat_GhostPartyPlayerStateBase'.static.GetLocalPlayerState(PlayerIndex); if (PlayerState == None) return true; s = string(Eyes.CurrentEyesOrigin); if (PlayerState.GetPlayerStateMeta(EyesOriginKey) ~= s) return true; PlayerState.SetPlayerStateMeta(EyesOriginKey, s); return true; } simulated function ReadOnlinePlayerEyesMetaData(out Rotator EyesOrigin) //Function that reads look direction from OP's meta data. { local Hat_GhostPartyPlayer gpp; local string s; gpp = Hat_GhostPartyPlayer(Owner); if (gpp == None || gpp.PlayerState == None) return false; EyesOrigin = rot(0, 0, 0); s = gpp.PlayerState.GetPlayerStateMeta(EyesOriginKey); if (s == "") return true; EyesOrigin = Rotator(s); return true; } simulated function bool SetLocalPlayerExpressionMetaData() //Function that writes current expression into LP's meta data. { local Hat_Player ply; local Hat_GhostPartyPlayerStateBase PlayerState; local int PlayerIndex; local string s; ply = Hat_Player(Owner); if (ply == None) return false; if (ply.ExpressionComponent != self) return true; PlayerIndex = GetPawnPlayerIndex(ply); if (PlayerIndex < 0) return true; PlayerState = class'Hat_GhostPartyPlayerStateBase'.static.GetLocalPlayerState(PlayerIndex); if (PlayerState == None) return true; s = string(int(CurrentExpression)); if (PlayerState.GetPlayerStateMeta(ExpressionKey) ~= s) return true; PlayerState.SetPlayerStateMeta(ExpressionKey, s); return true; } simulated function bool ReadOnlinePlayerExpressionMetaData(out EExpressionType PlayerExpression) //Function that reads current expression from OP's meta data. { local Hat_GhostPartyPlayer gpp; local string s; local int n; gpp = Hat_GhostPartyPlayer(Owner); if (gpp == None || gpp.PlayerState == None) return false; s = gpp.PlayerState.GetPlayerStateMeta(ExpressionKey); if (s == "") return false; n = int(s); if (n < 0 || n > ExpressionCount-1) return false; PlayerExpression = EExpressionType(n); return true; } //The following functions are copied from Shara_SteamID_Tools Class. static function PlayerController GetPawnPlayerController(Pawn p, optional out Array<Pawn> IteratedPawns) //Copied from Shara_SteamID_Tools. Returns PlayerController for Pawn. Will check Pawn itself and DrivenVehicle for PlayerController. IteratedPawns is used to prevent infinite loop when iterating Vehicle's Driver and Pawn's DrivenVehicle. { local PlayerController pc; local Vehicle v; if (p == None) return None; pc = PlayerController(p.Controller); if (pc != None) return pc; if (p.PlayerReplicationInfo != None) { pc = PlayerController(p.PlayerReplicationInfo.Owner); if (pc != None) return pc; } if (IteratedPawns.Find(p) == INDEX_NONE) IteratedPawns.AddItem(p); else return None; v = Vehicle(p); if (v != None) { pc = GetPawnPlayerController(v.Driver, IteratedPawns); if (pc != None) return pc; } return GetPawnPlayerController(p.DrivenVehicle, IteratedPawns); } static function int GetPawnPlayerIndex(Pawn p) //Returns player index of Pawn by first finding PlayerController of it. { return GetPlayerIndex(GetPawnPlayerController(p)); } static function int GetPlayerIndex(PlayerController pc) //Returns player index of PlayerController by either casting to Hat_PlayerController_Base or GamePlayerController or by checking Engine.GamePlayers Array. { local GamePlayerController gpc; local Hat_PlayerController_Base hpcb; local Engine e; local int i; if (pc == None) return -1; gpc = GamePlayerController(pc); if (gpc != None) { hpcb = Hat_PlayerController_Base(gpc); if (hpcb != None) return hpcb.GetPlayerIndex(); return gpc.GetUIPlayerIndex(); } e = class'Engine'.static.GetEngine(); if (e == None) return -1; for (i = 0; i < e.GamePlayers.Length; i++) { if (e.GamePlayers[i] == None) continue; if (e.GamePlayers[i].Actor == pc) return i; } return -1; } static function MaterialInstance ConditionalInitMaterialInstanceNoMesh(MaterialInterface mat) //Creates MaterialInstance (or returns detected one) and sets mat as its Parent. Does not set this Instance as material to MeshComponent. From Shara_SkinColors_Tools. { local MaterialInstance inst; if (mat == None) return None; if (MaterialInstance(mat) != None) { if (MaterialInstance(mat).IsInMapOrTransientPackage()) return MaterialInstance(mat); if (MaterialInstanceConstant(mat) != None) { inst = new(None) class'MaterialInstanceConstant'; inst.SetParent(mat); return inst; } else if (MaterialInstanceTimeVarying(mat) != None) { inst = new(None) class'MaterialInstanceTimeVarying'; inst.SetParent(mat); return inst; } } else { inst = new(None) class'MaterialInstanceTimeVarying'; inst.SetParent(mat); return inst; } return None; }
class Shara_GameMod extends GameMod config(Mods); //Please change Class Name (Shara_GameMod) and expression component Class Name (Shara_ExpressionComponent) to your ones when using it. var private transient Array<Shara_ExpressionComponent> GhostPartyExpressions; simulated function AddGhostPartyExpression(Shara_ExpressionComponent e) //Adding OP's expression component to "updating" list. { if (IsObjectNone(e)) return; if (Hat_GhostPartyPlayer(e.Owner) == None) return; if (GhostPartyExpressions.Find(e) == INDEX_NONE) GhostPartyExpressions.AddItem(e); } simulated function IterateGhostPartyExpressions(float DeltaTime) { local class<Hat_Player> PlayerClass; local Hat_GhostPartyPlayer gpp; local int i; local bool DoUpdate; if (!IsOPARTExpressionUpdateOn()) //Don't need to update expressions if OPART already does it. DoUpdate = true; for (i = GhostPartyExpressions.Length-1; i > -1; i--) { if (IsObjectNone(GhostPartyExpressions[i])) //The Component may be destroyed. { GhostPartyExpressions.Remove(i, 1); continue; } if (!DoUpdate) continue; if (!GhostPartyExpressions[i].bAttached) //Is Component attached? continue; gpp = Hat_GhostPartyPlayer(GhostPartyExpressions[i].Owner); if (gpp == None) //Is Component owned by Online Player? continue; if (gpp.PlayerState == None) //Does Owner have Player State? continue; if (!gpp.IsTicking()) //Is Owner ticking? continue; if (gpp.WorldInfo != None && gpp.WorldInfo.Pauser != None && !gpp.bAlwaysTick) //Is game paused and Owner does not tick during pause? continue; if (gpp.PlayerState.UnreliableState.IsPaused) //Is Owner paused? { PlayerClass = gpp.PlayerVisualClass; if (PlayerClass == None) PlayerClass = class'Hat_Player_HatKid'; if (!PlayerClass.default.bAlwaysTick) //If Owner is paused and its PlayerClass does not allow it to tick during pause, we continue. continue; } GhostPartyExpressions[i].Update(Abs(gpp.WorldInfo != None ? DeltaTime/gpp.WorldInfo.TimeDilation*gpp.PlayerState.UnreliableState.CustomTimeDilation : DeltaTime), false); } } static function bool IsOPARTExpressionUpdateOn() { local class<GameMod> OPARTClass; if (!HasModInstalled("OnlineAnimationRepair.AnimationsRepair", OPARTClass)) return false; if (OPARTClass == None) return false; if (GetConfigValue(OPARTClass, 'mod_disabled') != 0) return false; if (GetConfigValue(OPARTClass, 'ExpressionUpdate') != 0) return false; return true; } static function bool HasModInstalled(string ModPath, optional out class<GameMod> ModClass) { local int i; local Array<GameModInfo> ModList; ModClass = None; if (ModPath == "" || ModPath == ".") return false; ModList = GetModList(); for (i = 0; i < ModList.Length; i++) { if (ModList[i].ModClass == None) continue; if (locs(PathName(ModList[i].ModClass)) != locs(ModPath)) continue; if (!ModList[i].IsSubscribed) continue; if (ModList[i].IsDownloading) continue; if (!ModList[i].IsEnabled) continue; ModClass = class<GameMod>(ModList[i].ModClass); return true; } return false; } simulated event Tick(float DeltaTime) { IterateGhostPartyExpressions(DeltaTime); } static function bool IsObjectNone(Object o) { if (o == None) return true; if (o.IsPendingKill()) return true; return false; } defaultproperties { bAlwaysTick = true }