diff --git a/EpicOnlineTransport/EpicOnlineTransport.csproj b/EpicOnlineTransport/EpicOnlineTransport.csproj
index 1134fb47..5655714a 100644
--- a/EpicOnlineTransport/EpicOnlineTransport.csproj
+++ b/EpicOnlineTransport/EpicOnlineTransport.csproj
@@ -4,6 +4,6 @@
-
+
diff --git a/EpicRerouter/EpicRerouter.csproj b/EpicRerouter/EpicRerouter.csproj
index a582e089..dbebee21 100644
--- a/EpicRerouter/EpicRerouter.csproj
+++ b/EpicRerouter/EpicRerouter.csproj
@@ -3,7 +3,7 @@
Exe
-
+
diff --git a/MirrorWeaver/MirrorWeaver.csproj b/MirrorWeaver/MirrorWeaver.csproj
index d74b3074..737bc8f8 100644
--- a/MirrorWeaver/MirrorWeaver.csproj
+++ b/MirrorWeaver/MirrorWeaver.csproj
@@ -3,7 +3,7 @@
Exe
-
+
diff --git a/QSB.sln.DotSettings b/QSB.sln.DotSettings
index 2338a938..5a30abd4 100644
--- a/QSB.sln.DotSettings
+++ b/QSB.sln.DotSettings
@@ -7,16 +7,7 @@
Required
Required
Required
- FileScoped
- False
- False
- USE_TABS_ONLY
NEXT_LINE_SHIFTED_2
- Tab
- 1
- 1
- True
- False
True
True
True
diff --git a/QSB/Animation/NPC/CharacterAnimManager.cs b/QSB/Animation/NPC/CharacterAnimManager.cs
index 5e223dda..78a034d6 100644
--- a/QSB/Animation/NPC/CharacterAnimManager.cs
+++ b/QSB/Animation/NPC/CharacterAnimManager.cs
@@ -9,9 +9,6 @@ internal class CharacterAnimManager : WorldObjectManager
{
public override WorldObjectScene WorldObjectScene => WorldObjectScene.Both;
- public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct)
- {
- QSBWorldSync.Init();
+ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) =>
QSBWorldSync.Init();
- }
}
\ No newline at end of file
diff --git a/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs b/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs
index 2d15a198..cc93e9c6 100644
--- a/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs
+++ b/QSB/Animation/NPC/Patches/CharacterAnimationPatches.cs
@@ -115,23 +115,4 @@ public class CharacterAnimationPatches : QSBPatch
return false;
}
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(KidRockController), nameof(KidRockController.Update))]
- public static bool UpdateReplacement(KidRockController __instance)
- {
- if (!QSBWorldSync.AllObjectsReady)
- {
- return true;
- }
-
- var qsbObj = QSBWorldSync.GetWorldObjects().First(x => x.GetDialogueTree() == __instance._dialogueTree);
-
- if (!__instance._throwingRock && !qsbObj.InConversation() && Time.time > __instance._nextThrowTime)
- {
- __instance.StartRockThrow();
- }
-
- return false;
- }
}
\ No newline at end of file
diff --git a/QSB/Animation/NPC/WorldObjects/QSBCharacterAnimController.cs b/QSB/Animation/NPC/WorldObjects/QSBCharacterAnimController.cs
deleted file mode 100644
index b372d94b..00000000
--- a/QSB/Animation/NPC/WorldObjects/QSBCharacterAnimController.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using QSB.WorldSync;
-
-namespace QSB.Animation.NPC.WorldObjects;
-
-internal class QSBCharacterAnimController : WorldObject
-{
- public override void SendInitialState(uint to)
- {
- // todo SendInitialState
- }
-
- public CharacterDialogueTree GetDialogueTree()
- => AttachedObject._dialogueTree;
-
- public bool InConversation()
- => AttachedObject._inConversation;
-}
\ No newline at end of file
diff --git a/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs b/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs
index ff55aaf8..776decd2 100644
--- a/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs
+++ b/QSB/Animation/NPC/WorldObjects/QSBSolanumAnimController.cs
@@ -11,6 +11,4 @@ internal class QSBSolanumAnimController : WorldObject
{
private QSBSolanumTrigger _trigger;
public QSBSolanumTrigger Trigger => _trigger ??= QSBWorldSync.GetWorldObjects().First();
-
- public override void SendInitialState(uint to) { }
}
\ No newline at end of file
diff --git a/QSB/Animation/Player/AnimationSync.cs b/QSB/Animation/Player/AnimationSync.cs
index 2b0069f4..e7e1d18a 100644
--- a/QSB/Animation/Player/AnimationSync.cs
+++ b/QSB/Animation/Player/AnimationSync.cs
@@ -6,6 +6,7 @@ using QSB.Animation.Player.Thrusters;
using QSB.Messaging;
using QSB.Player;
using QSB.Utility;
+using QSB.WorldSync;
using System;
using UnityEngine;
@@ -17,8 +18,6 @@ public class AnimationSync : PlayerSyncObject
private AnimatorOverrideController _unsuitedAnimController;
private GameObject _suitedGraphics;
private GameObject _unsuitedGraphics;
- private PlayerCharacterController _playerController;
- private CrouchSync _crouchSync;
public AnimatorMirror Mirror { get; private set; }
public bool InSuitedUpState { get; set; }
@@ -31,16 +30,26 @@ public class AnimationSync : PlayerSyncObject
InvisibleAnimator = gameObject.GetRequiredComponent();
NetworkAnimator = gameObject.GetRequiredComponent();
NetworkAnimator.enabled = false;
+ RequestInitialStatesMessage.SendInitialState += SendInitialState;
}
+ protected void OnDestroy() => RequestInitialStatesMessage.SendInitialState -= SendInitialState;
+
+ ///
+ /// This wipes the NetworkAnimator's fields, so it assumes the parameters have changed.
+ /// Basically just forces it to set all its dirty flags.
+ ///
+ private void SendInitialState(uint to) => NetworkAnimator.Invoke("Awake");
+
+ public void Reset() => InSuitedUpState = false;
+
private void InitCommon(Transform modelRoot)
{
try
{
-
if (modelRoot == null)
{
- DebugLog.ToConsole($"Error - Trying to InitCommon with null body!", MessageType.Error);
+ DebugLog.ToConsole("Error - Trying to InitCommon with null body!", MessageType.Error);
return;
}
@@ -48,11 +57,11 @@ public class AnimationSync : PlayerSyncObject
Mirror = modelRoot.gameObject.AddComponent();
if (isLocalPlayer)
{
- Mirror.Init(VisibleAnimator, InvisibleAnimator);
+ Mirror.Init(VisibleAnimator, InvisibleAnimator, NetworkAnimator);
}
else
{
- Mirror.Init(InvisibleAnimator, VisibleAnimator);
+ Mirror.Init(InvisibleAnimator, VisibleAnimator, null);
}
NetworkAnimator.enabled = true;
@@ -74,10 +83,6 @@ public class AnimationSync : PlayerSyncObject
public void InitLocal(Transform body)
{
InitCommon(body);
-
- _playerController = body.parent.GetComponent();
-
- InitCrouchSync();
InitAccelerationSync();
}
@@ -85,7 +90,6 @@ public class AnimationSync : PlayerSyncObject
{
InitCommon(body);
SetSuitState(QSBSceneManager.CurrentScene == OWScene.EyeOfTheUniverse);
- InitCrouchSync();
InitAccelerationSync();
ThrusterManager.CreateRemotePlayerVFX(Player);
@@ -100,12 +104,6 @@ public class AnimationSync : PlayerSyncObject
Player.JetpackAcceleration.Init(thrusterModel);
}
- private void InitCrouchSync()
- {
- _crouchSync = this.GetRequiredComponent();
- _crouchSync.Init(_playerController, VisibleAnimator);
- }
-
public void SetSuitState(bool suitedUp)
{
if (!Player.IsReady)
@@ -176,12 +174,12 @@ public class AnimationSync : PlayerSyncObject
// Avoids "jumping" when putting on suit
if (VisibleAnimator != null)
{
- VisibleAnimator.SetTrigger("Grounded");
+ VisibleAnimator.SetBool("Grounded", true);
}
if (InvisibleAnimator != null)
{
- InvisibleAnimator.SetTrigger("Grounded");
+ InvisibleAnimator.SetBool("Grounded", true);
}
if (NetworkAnimator == null)
@@ -196,4 +194,4 @@ public class AnimationSync : PlayerSyncObject
Mirror.RebuildFloatParams();
NetworkAnimator.Invoke("Awake");
}
-}
\ No newline at end of file
+}
diff --git a/QSB/Animation/Player/AnimatorMirror.cs b/QSB/Animation/Player/AnimatorMirror.cs
index bbc1c525..736d5b2e 100644
--- a/QSB/Animation/Player/AnimatorMirror.cs
+++ b/QSB/Animation/Player/AnimatorMirror.cs
@@ -1,4 +1,5 @@
-using OWML.Common;
+using Mirror;
+using OWML.Common;
using QSB.Utility;
using System.Collections.Generic;
using System.Linq;
@@ -12,10 +13,17 @@ public class AnimatorMirror : MonoBehaviour
private Animator _from;
private Animator _to;
+ private NetworkAnimator _networkAnimator;
private readonly Dictionary _floatParams = new();
- public void Init(Animator from, Animator to)
+ ///
+ /// Initializes the Animator Mirror
+ ///
+ /// The Animator to take the values from.
+ /// The Animator to set the values on to.
+ /// The NetworkAnimator to sync triggers through. Set only if you have auth over "", otherwise set to null.
+ public void Init(Animator from, Animator to, NetworkAnimator netAnimator)
{
if (from == null)
{
@@ -44,6 +52,8 @@ public class AnimatorMirror : MonoBehaviour
_to.runtimeAnimatorController = _from.runtimeAnimatorController;
}
+ _networkAnimator = netAnimator;
+
RebuildFloatParams();
}
@@ -61,6 +71,7 @@ public class AnimatorMirror : MonoBehaviour
}
SyncParams();
+ SyncLayerWeights();
SmoothFloats();
}
@@ -74,13 +85,49 @@ public class AnimatorMirror : MonoBehaviour
_floatParams[fromParam.name].Target = _from.GetFloat(fromParam.name);
break;
+ case AnimatorControllerParameterType.Int:
+ _to.SetInteger(fromParam.name, _from.GetInteger(fromParam.name));
+ break;
+
case AnimatorControllerParameterType.Bool:
_to.SetBool(fromParam.name, _from.GetBool(fromParam.name));
break;
+
+ case AnimatorControllerParameterType.Trigger:
+ if (_from.GetBool(fromParam.name) && !_to.GetBool(fromParam.name))
+ {
+ if (_networkAnimator != null)
+ {
+ _networkAnimator.SetTrigger(fromParam.name);
+ }
+
+ _to.SetTrigger(fromParam.name);
+ }
+
+ if (!_from.GetBool(fromParam.name) && _to.GetBool(fromParam.name))
+ {
+ if (_networkAnimator != null)
+ {
+ _networkAnimator.ResetTrigger(fromParam.name);
+ }
+
+ _to.ResetTrigger(fromParam.name);
+ }
+
+ break;
}
}
}
+ private void SyncLayerWeights()
+ {
+ for (var i = 0; i < _from.layerCount; i++)
+ {
+ var weight = _from.GetLayerWeight(i);
+ _to.SetLayerWeight(i, weight);
+ }
+ }
+
private void SmoothFloats()
{
foreach (var floatParam in _floatParams)
@@ -98,4 +145,4 @@ public class AnimatorMirror : MonoBehaviour
_floatParams.Add(param.name, new AnimFloatParam());
}
}
-}
\ No newline at end of file
+}
diff --git a/QSB/Animation/Player/CrouchSync.cs b/QSB/Animation/Player/CrouchSync.cs
deleted file mode 100644
index 33beebc6..00000000
--- a/QSB/Animation/Player/CrouchSync.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Mirror;
-using QSB.Utility.VariableSync;
-using UnityEngine;
-
-namespace QSB.Animation.Player;
-
-public class CrouchSync : NetworkBehaviour
-{
- public AnimFloatParam CrouchParam { get; } = new AnimFloatParam();
-
- private const float CrouchSmoothTime = 0.05f;
- public const int CrouchLayerIndex = 1;
-
- private PlayerCharacterController _playerController;
- private Animator _bodyAnim;
-
- public FloatVariableSyncer CrouchVariableSyncer;
-
- public void Init(PlayerCharacterController playerController, Animator bodyAnim)
- {
- _playerController = playerController;
- _bodyAnim = bodyAnim;
- }
-
- public void Update()
- {
- if (isLocalPlayer)
- {
- SyncLocalCrouch();
- return;
- }
-
- SyncRemoteCrouch();
- }
-
- private void SyncLocalCrouch()
- {
- if (_playerController == null)
- {
- return;
- }
-
- var jumpChargeFraction = _playerController.GetJumpCrouchFraction();
- CrouchVariableSyncer.Value = jumpChargeFraction;
- }
-
- private void SyncRemoteCrouch()
- {
- if (_bodyAnim == null)
- {
- return;
- }
-
- CrouchParam.Target = CrouchVariableSyncer.Value;
- CrouchParam.Smooth(CrouchSmoothTime);
- var jumpChargeFraction = CrouchParam.Current;
- _bodyAnim.SetLayerWeight(CrouchLayerIndex, jumpChargeFraction);
- }
-}
\ No newline at end of file
diff --git a/QSB/Animation/Player/Messages/AnimationTriggerMessage.cs b/QSB/Animation/Player/Messages/AnimationTriggerMessage.cs
deleted file mode 100644
index c7166e4c..00000000
--- a/QSB/Animation/Player/Messages/AnimationTriggerMessage.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using QSB.Messaging;
-using QSB.Player;
-using QSB.WorldSync;
-
-namespace QSB.Animation.Player.Messages;
-
-internal class AnimationTriggerMessage : QSBMessage
-{
- public AnimationTriggerMessage(string name) : base(name) { }
-
- public override bool ShouldReceive => QSBWorldSync.AllObjectsReady;
-
- public override void OnReceiveRemote()
- {
- var animationSync = QSBPlayerManager.GetPlayer(From).AnimationSync;
- if (animationSync == null)
- {
- return;
- }
-
- if (animationSync.VisibleAnimator == null)
- {
- return;
- }
-
- animationSync.VisibleAnimator.SetTrigger(Data);
- }
-}
\ No newline at end of file
diff --git a/QSB/Animation/Player/Patches/PlayerAnimationPatches.cs b/QSB/Animation/Player/Patches/PlayerAnimationPatches.cs
deleted file mode 100644
index e99b52e7..00000000
--- a/QSB/Animation/Player/Patches/PlayerAnimationPatches.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using HarmonyLib;
-using QSB.Animation.Player.Messages;
-using QSB.Messaging;
-using QSB.Patches;
-using QSB.Utility;
-using UnityEngine;
-
-namespace QSB.Animation.Player.Patches;
-
-[HarmonyPatch]
-internal class PlayerAnimationPatches : QSBPatch
-{
- public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(PlayerAnimController), nameof(PlayerAnimController.LateUpdate))]
- public static bool LateUpdateReplacement(
- PlayerAnimController __instance)
- {
- var isGrounded = __instance._playerController.IsGrounded();
- var isAttached = PlayerState.IsAttached();
- var isInZeroG = PlayerState.InZeroG();
- var isFlying = __instance._playerJetpack.GetLocalAcceleration().y > 0f;
- var movementVector = Vector3.zero;
- if (!isAttached)
- {
- movementVector = __instance._playerController.GetRelativeGroundVelocity();
- }
-
- if (Mathf.Abs(movementVector.x) < 0.05f)
- {
- movementVector.x = 0f;
- }
-
- if (Mathf.Abs(movementVector.z) < 0.05f)
- {
- movementVector.z = 0f;
- }
-
- if (isFlying)
- {
- __instance._ungroundedTime = Time.time;
- }
-
- var freefallMagnitude = 0f;
- var timeInFreefall = 0f;
- var lastGroundBody = __instance._playerController.GetLastGroundBody();
- if (!isGrounded && !isAttached && !isInZeroG && lastGroundBody != null)
- {
- freefallMagnitude = (__instance._playerController.GetAttachedOWRigidbody().GetVelocity() - lastGroundBody.GetPointVelocity(__instance._playerController.transform.position)).magnitude;
- timeInFreefall = Time.time - __instance._ungroundedTime;
- }
-
- __instance._animator.SetFloat("RunSpeedX", movementVector.x / 3f);
- __instance._animator.SetFloat("RunSpeedY", movementVector.z / 3f);
- __instance._animator.SetFloat("TurnSpeed", __instance._playerController.GetTurning());
- __instance._animator.SetBool("Grounded", isGrounded || isAttached || PlayerState.IsRecentlyDetached());
- __instance._animator.SetLayerWeight(CrouchSync.CrouchLayerIndex, __instance._playerController.GetJumpCrouchFraction());
- __instance._animator.SetFloat("FreefallSpeed", freefallMagnitude / 15f * (timeInFreefall / 3f));
- __instance._animator.SetBool("InZeroG", isInZeroG || isFlying);
- __instance._animator.SetBool("UsingJetpack", isInZeroG && PlayerState.IsWearingSuit());
- if (__instance._justBecameGrounded)
- {
- if (__instance._justTookFallDamage)
- {
- __instance._animator.SetTrigger("LandHard");
- new AnimationTriggerMessage("LandHard").Send();
- }
- else
- {
- __instance._animator.SetTrigger("Land");
- new AnimationTriggerMessage("Land").Send();
- }
- }
-
- if (isGrounded)
- {
- var leftFootLift = __instance._animator.GetFloat("LeftFootLift");
- if (!__instance._leftFootGrounded && leftFootLift < 0.333f)
- {
- __instance._leftFootGrounded = true;
- __instance.RaiseEvent(nameof(__instance.OnLeftFootGrounded));
- }
- else if (__instance._leftFootGrounded && leftFootLift > 0.666f)
- {
- __instance._leftFootGrounded = false;
- __instance.RaiseEvent(nameof(__instance.OnLeftFootLift));
- }
-
- var rightFootLift = __instance._animator.GetFloat("RightFootLift");
- if (!__instance._rightFootGrounded && rightFootLift < 0.333f)
- {
- __instance._rightFootGrounded = true;
- __instance.RaiseEvent(nameof(__instance.OnRightFootGrounded));
- }
- else if (__instance._rightFootGrounded && rightFootLift > 0.666f)
- {
- __instance._rightFootGrounded = false;
- __instance.RaiseEvent(nameof(__instance.OnRightFootLift));
- }
- }
-
- __instance._justBecameGrounded = false;
- __instance._justTookFallDamage = false;
- var usingTool = Locator.GetToolModeSwapper().GetToolMode() != ToolMode.None;
- if ((usingTool && !__instance._rightArmHidden) || (!usingTool && __instance._rightArmHidden))
- {
- __instance._rightArmHidden = usingTool;
- for (var i = 0; i < __instance._rightArmObjects.Length; i++)
- {
- __instance._rightArmObjects[i].layer = (!__instance._rightArmHidden) ? __instance._defaultLayer : __instance._probeOnlyLayer;
- }
- }
-
- return false;
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(PlayerAnimController), nameof(PlayerAnimController.OnPlayerJump))]
- public static bool OnPlayerJumpReplacement(PlayerAnimController __instance)
- {
- __instance._ungroundedTime = Time.time;
- if (!__instance.isActiveAndEnabled)
- {
- return false;
- }
-
- __instance._animator.SetTrigger("Jump");
- new AnimationTriggerMessage("Jump").Send();
- return false;
- }
-}
\ No newline at end of file
diff --git a/QSB/Animation/Player/PlayerHeadRotationSync.cs b/QSB/Animation/Player/PlayerHeadRotationSync.cs
index d71b0f35..cc41f8d5 100644
--- a/QSB/Animation/Player/PlayerHeadRotationSync.cs
+++ b/QSB/Animation/Player/PlayerHeadRotationSync.cs
@@ -39,6 +39,7 @@ public class PlayerHeadRotationSync : MonoBehaviour
var bone = _attachedAnimator.GetBoneTransform(HumanBodyBones.Head);
// Get the camera's local rotation with respect to the player body
var lookLocalRotation = Quaternion.Inverse(_attachedAnimator.transform.rotation) * _lookBase.rotation;
+ // no idea why this rotation is like this, but this is the only way it looks right
bone.localRotation = Quaternion.Euler(-lookLocalRotation.eulerAngles.y, -lookLocalRotation.eulerAngles.z, lookLocalRotation.eulerAngles.x);
}
}
\ No newline at end of file
diff --git a/QSB/AssetBundles/qsb_network b/QSB/AssetBundles/qsb_network
index 3332e0fc..5af36f0d 100644
Binary files a/QSB/AssetBundles/qsb_network and b/QSB/AssetBundles/qsb_network differ
diff --git a/QSB/AssetBundles/qsb_network_big b/QSB/AssetBundles/qsb_network_big
index 22247a5b..4688aa52 100644
Binary files a/QSB/AssetBundles/qsb_network_big and b/QSB/AssetBundles/qsb_network_big differ
diff --git a/QSB/AuthoritySync/AuthWorldObject.cs b/QSB/AuthoritySync/AuthWorldObject.cs
new file mode 100644
index 00000000..f70d64c7
--- /dev/null
+++ b/QSB/AuthoritySync/AuthWorldObject.cs
@@ -0,0 +1,39 @@
+using Cysharp.Threading.Tasks;
+using QSB.Messaging;
+using QSB.Player;
+using QSB.WorldSync;
+using System.Threading;
+using UnityEngine;
+
+namespace QSB.AuthoritySync;
+
+///
+/// helper implementation of the interface
+///
+public abstract class AuthWorldObject : WorldObject, IAuthWorldObject
+ where T : MonoBehaviour
+{
+ public uint Owner { get; set; }
+ public abstract bool CanOwn { get; }
+
+ public override void SendInitialState(uint to) =>
+ ((IAuthWorldObject)this).SendMessage(new AuthWorldObjectMessage(Owner) { To = to });
+
+ public override async UniTask Init(CancellationToken ct) =>
+ QSBPlayerManager.OnRemovePlayer += OnPlayerLeave;
+
+ public override void OnRemoval() =>
+ QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave;
+
+ private void OnPlayerLeave(PlayerInfo player)
+ {
+ if (!QSBCore.IsHost)
+ {
+ return;
+ }
+ if (Owner == player.PlayerId)
+ {
+ ((IAuthWorldObject)this).SendMessage(new AuthWorldObjectMessage(CanOwn ? QSBPlayerManager.LocalPlayerId : 0));
+ }
+ }
+}
diff --git a/QSB/AuthoritySync/AuthWorldObjectMessage.cs b/QSB/AuthoritySync/AuthWorldObjectMessage.cs
new file mode 100644
index 00000000..08f9cb95
--- /dev/null
+++ b/QSB/AuthoritySync/AuthWorldObjectMessage.cs
@@ -0,0 +1,45 @@
+using QSB.Messaging;
+using QSB.Player;
+
+namespace QSB.AuthoritySync;
+
+///
+/// request or release ownership of a world object
+///
+public class AuthWorldObjectMessage : QSBWorldObjectMessage
+{
+ public AuthWorldObjectMessage(uint owner) : base(owner) { }
+
+ public override bool ShouldReceive
+ {
+ get
+ {
+ if (!base.ShouldReceive)
+ {
+ return false;
+ }
+
+ // Deciding if to change the object's owner
+ // Message
+ // | = 0 | > 0 |
+ // = 0 | No | Yes |
+ // > 0 | Yes | No |
+ // if Obj==Message then No
+ // Obj
+
+ return (WorldObject.Owner == 0 || Data == 0) && WorldObject.Owner != Data;
+ }
+ }
+
+ public override void OnReceiveLocal() => WorldObject.Owner = Data;
+
+ public override void OnReceiveRemote()
+ {
+ WorldObject.Owner = Data;
+ if (WorldObject.Owner == 0 && WorldObject.CanOwn)
+ {
+ // object has no owner, but is still active for this player. request ownership
+ WorldObject.SendMessage(new AuthWorldObjectMessage(QSBPlayerManager.LocalPlayerId));
+ }
+ }
+}
diff --git a/QSB/AuthoritySync/IAuthWorldObject.cs b/QSB/AuthoritySync/IAuthWorldObject.cs
new file mode 100644
index 00000000..7d706feb
--- /dev/null
+++ b/QSB/AuthoritySync/IAuthWorldObject.cs
@@ -0,0 +1,18 @@
+using QSB.WorldSync;
+
+namespace QSB.AuthoritySync;
+
+///
+/// a world object that has an owner
+///
+public interface IAuthWorldObject : IWorldObject
+{
+ ///
+ /// 0 = owned by no one
+ ///
+ public uint Owner { get; set; }
+ ///
+ /// can the world object have authority
+ ///
+ public bool CanOwn { get; }
+}
diff --git a/QSB/AuthoritySync/IAuthWorldObject_Extensions.cs b/QSB/AuthoritySync/IAuthWorldObject_Extensions.cs
new file mode 100644
index 00000000..e0339625
--- /dev/null
+++ b/QSB/AuthoritySync/IAuthWorldObject_Extensions.cs
@@ -0,0 +1,32 @@
+using QSB.Messaging;
+using QSB.Player;
+
+namespace QSB.AuthoritySync;
+
+public static class IAuthWorldObject_Extensions
+{
+ ///
+ /// try and gain authority over the object
+ ///
+ public static void RequestOwnership(this IAuthWorldObject @this)
+ {
+ if (@this.Owner != 0)
+ {
+ return;
+ }
+ @this.SendMessage(new AuthWorldObjectMessage(QSBPlayerManager.LocalPlayerId));
+ }
+
+ ///
+ /// release authority over the object,
+ /// potentially to giving it to someone else
+ ///
+ public static void ReleaseOwnership(this IAuthWorldObject @this)
+ {
+ if (@this.Owner != QSBPlayerManager.LocalPlayerId)
+ {
+ return;
+ }
+ @this.SendMessage(new AuthWorldObjectMessage(0));
+ }
+}
diff --git a/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs b/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs
index 0b2cd47d..4c4f5f7c 100644
--- a/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs
+++ b/QSB/CampfireSync/Messages/BurnSlideReelMessage.cs
@@ -6,13 +6,16 @@ using QSB.WorldSync;
namespace QSB.CampfireSync.Messages;
+///
+/// TODO: initial state on campfire and item
+///
internal class BurnSlideReelMessage : QSBWorldObjectMessage
{
public BurnSlideReelMessage(QSBCampfire campfire) : base(campfire.ObjectId) { }
public override void OnReceiveRemote()
{
- var campfire = QSBWorldSync.GetWorldObject(Data).AttachedObject;
+ var campfire = Data.GetWorldObject().AttachedObject;
var fromPlayer = QSBPlayerManager.GetPlayer(From);
WorldObject.DropItem(
campfire._burnedSlideReelSocket.position,
diff --git a/QSB/ConversationSync/ConversationManager.cs b/QSB/ConversationSync/ConversationManager.cs
index 2b1a78f7..f394e6f2 100644
--- a/QSB/ConversationSync/ConversationManager.cs
+++ b/QSB/ConversationSync/ConversationManager.cs
@@ -45,7 +45,7 @@ public class ConversationManager : WorldObjectManager
QSBWorldSync.Init();
}
- public uint GetPlayerTalkingToTree(CharacterDialogueTree tree) =>
+ public uint GetPlayerTalkingToTree(CharacterDialogueTree tree) =>
QSBPlayerManager.PlayerList.FirstOrDefault(x => x.CurrentCharacterDialogueTree?.AttachedObject == tree)
?.PlayerId ?? uint.MaxValue;
@@ -62,7 +62,7 @@ public class ConversationManager : WorldObjectManager
new ConversationMessage(ConversationType.CloseCharacter, id).Send();
public void SendConvState(QSBCharacterDialogueTree tree, bool state)
- => tree.SendMessage(new ConversationStartEndMessage(state));
+ => tree.SendMessage(new ConversationStartEndMessage(QSBPlayerManager.LocalPlayerId, state));
public void DisplayPlayerConversationBox(uint playerId, string text)
{
@@ -109,4 +109,4 @@ public class ConversationManager : WorldObjectManager
newBox.SetActive(true);
return newBox;
}
-}
\ No newline at end of file
+}
diff --git a/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs b/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs
index ce88fc4b..1a1d8eb2 100644
--- a/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs
+++ b/QSB/ConversationSync/Messages/ConversationStartEndMessage.cs
@@ -5,21 +5,21 @@ using QSB.Utility;
namespace QSB.ConversationSync.Messages;
-public class ConversationStartEndMessage : QSBWorldObjectMessage
+public class ConversationStartEndMessage : QSBWorldObjectMessage
{
- public ConversationStartEndMessage(bool start) : base(start) { }
+ public ConversationStartEndMessage(uint playerId, bool start) : base((playerId, start)) { }
public override void OnReceiveRemote()
{
- if (Data)
+ if (Data.start)
{
- QSBPlayerManager.GetPlayer(From).CurrentCharacterDialogueTree = WorldObject;
+ QSBPlayerManager.GetPlayer(Data.playerId).CurrentCharacterDialogueTree = WorldObject;
WorldObject.AttachedObject.GetInteractVolume().DisableInteraction();
WorldObject.AttachedObject.RaiseEvent(nameof(CharacterDialogueTree.OnStartConversation));
}
else
{
- QSBPlayerManager.GetPlayer(From).CurrentCharacterDialogueTree = null;
+ QSBPlayerManager.GetPlayer(Data.playerId).CurrentCharacterDialogueTree = null;
WorldObject.AttachedObject.GetInteractVolume().EnableInteraction();
WorldObject.AttachedObject.RaiseEvent(nameof(CharacterDialogueTree.OnEndConversation));
}
diff --git a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs
index f3eb5da8..17e8d329 100644
--- a/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs
+++ b/QSB/ConversationSync/WorldObjects/QSBCharacterDialogueTree.cs
@@ -1,9 +1,6 @@
-using QSB.WorldSync;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using QSB.ConversationSync.Messages;
+using QSB.Messaging;
+using QSB.WorldSync;
namespace QSB.ConversationSync.WorldObjects;
@@ -11,6 +8,11 @@ public class QSBCharacterDialogueTree : WorldObject
{
public override void SendInitialState(uint to)
{
- // todo : implement this
+ var playerId = ConversationManager.Instance.GetPlayerTalkingToTree(AttachedObject);
+ if (playerId != uint.MaxValue)
+ {
+ this.SendMessage(new ConversationStartEndMessage(playerId, true) { To = to });
+ }
+ // TODO: maybe also sync the dialogue box and player box?
}
}
diff --git a/QSB/DeathSync/RespawnOnDeath.cs b/QSB/DeathSync/RespawnOnDeath.cs
index c990d24d..9968c1f9 100644
--- a/QSB/DeathSync/RespawnOnDeath.cs
+++ b/QSB/DeathSync/RespawnOnDeath.cs
@@ -1,7 +1,9 @@
using OWML.Common;
+using QSB.Localization;
using QSB.Player;
using QSB.Player.TransformSync;
using QSB.RespawnSync;
+using QSB.ShipSync;
using QSB.Utility;
using QSB.WorldSync;
using System.Linq;
@@ -26,6 +28,7 @@ public class RespawnOnDeath : MonoBehaviour
private PlayerSpacesuit _spaceSuit;
private SuitPickupVolume[] _suitPickupVolumes;
private Vector3 _deathPositionRelative;
+ private GUIStyle _deadTextStyle;
public Transform DeathClosestAstroObject { get; private set; }
public Vector3 DeathPositionWorld
@@ -47,6 +50,11 @@ public class RespawnOnDeath : MonoBehaviour
_suitPickupVolumes = FindObjectsOfType();
_fluidDetector = Locator.GetPlayerCamera().GetComponentInChildren();
_playerSpawnPoint = GetSpawnPoint();
+ _deadTextStyle = new();
+ _deadTextStyle.font = (Font)Resources.Load(@"fonts\english - latin\SpaceMono-Regular_Dynamic");
+ _deadTextStyle.alignment = TextAnchor.MiddleCenter;
+ _deadTextStyle.normal.textColor = Color.white;
+ _deadTextStyle.fontSize = 20;
}
public void ResetPlayer()
@@ -122,4 +130,31 @@ public class RespawnOnDeath : MonoBehaviour
spawnPoint.GetSpawnLocation() == SpawnLocation.TimberHearth
&& spawnPoint.IsShipSpawn() == false);
}
+
+ void OnGUI()
+ {
+ if (PlayerTransformSync.LocalInstance == null || ShipManager.Instance.ShipCockpitUI == null)
+ {
+ return;
+ }
+
+ if (QSBPlayerManager.LocalPlayer.IsDead)
+ {
+ GUI.contentColor = Color.white;
+
+ var width = 200;
+ var height = 100;
+
+ // it is good day to be not dead
+
+ var secondText = ShipManager.Instance.IsShipWrecked
+ ? string.Format(QSBLocalization.Current.WaitingForAllToDie, QSBPlayerManager.PlayerList.Count(x => !x.IsDead))
+ : QSBLocalization.Current.WaitingForRespawn;
+
+ GUI.Label(
+ new Rect((Screen.width / 2) - (width / 2), (Screen.height / 2) - (height / 2) + (height * 2), width, height),
+ $"{QSBLocalization.Current.YouAreDead}\n{secondText}",
+ _deadTextStyle);
+ }
+ }
}
\ No newline at end of file
diff --git a/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs b/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs
index 5f2642b1..14a616e9 100644
--- a/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs
+++ b/QSB/EchoesOfTheEye/AirlockSync/AirlockManager.cs
@@ -10,6 +10,9 @@ internal class AirlockManager : WorldObjectManager
public override WorldObjectScene WorldObjectScene => WorldObjectScene.SolarSystem;
public override bool DlcOnly => true;
- public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) =>
+ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct)
+ {
QSBWorldSync.Init();
-}
+ QSBWorldSync.Init();
+ }
+}
diff --git a/QSB/EchoesOfTheEye/AirlockSync/Messages/AirlockInitialStateMessage.cs b/QSB/EchoesOfTheEye/AirlockSync/Messages/AirlockInitialStateMessage.cs
new file mode 100644
index 00000000..665f6421
--- /dev/null
+++ b/QSB/EchoesOfTheEye/AirlockSync/Messages/AirlockInitialStateMessage.cs
@@ -0,0 +1,17 @@
+using QSB.EchoesOfTheEye.AirlockSync.WorldObjects;
+using QSB.Messaging;
+
+namespace QSB.EchoesOfTheEye.AirlockSync.Messages;
+
+internal class AirlockInitialStateMessage : QSBWorldObjectMessage
+{
+ public AirlockInitialStateMessage(bool innerDoorOpen, bool outerDoorOpen, bool pressurized) : base((innerDoorOpen, outerDoorOpen, pressurized)) { }
+
+ public override void OnReceiveRemote()
+ {
+ var airlock = WorldObject.AttachedObject;
+ airlock._innerDoor.SetOpenImmediate(Data.innerDoorOpen);
+ airlock._outerDoor.SetOpenImmediate(Data.outerDoorOpen);
+ airlock.SetPressurization(Data.pressurized);
+ }
+}
diff --git a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs
index ed84ea35..93350633 100644
--- a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs
+++ b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBAirlockInterface.cs
@@ -1,4 +1,5 @@
using QSB.EchoesOfTheEye.AirlockSync.VariableSync;
+using System;
using System.Collections.Generic;
using UnityEngine;
@@ -9,4 +10,14 @@ internal class QSBAirlockInterface : QSBRotatingElements LightSensors => AttachedObject._lightSensors;
protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.AirlockPrefab;
+
+ public override string ReturnLabel()
+ {
+ var baseString = $"{this}{Environment.NewLine}CurrentRotation:{AttachedObject._currentRotation}";
+ foreach (var element in AttachedObject._rotatingElements)
+ {
+ baseString += $"{Environment.NewLine}localRotation:{element.localRotation}";
+ }
+ return baseString;
+ }
}
diff --git a/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBGhostAirlock.cs b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBGhostAirlock.cs
new file mode 100644
index 00000000..711db9b5
--- /dev/null
+++ b/QSB/EchoesOfTheEye/AirlockSync/WorldObjects/QSBGhostAirlock.cs
@@ -0,0 +1,17 @@
+using QSB.EchoesOfTheEye.AirlockSync.Messages;
+using QSB.Messaging;
+using QSB.WorldSync;
+
+namespace QSB.EchoesOfTheEye.AirlockSync.WorldObjects;
+
+internal class QSBGhostAirlock : WorldObject
+{
+ public override void SendInitialState(uint to)
+ => this.SendMessage(
+ new AirlockInitialStateMessage(
+ AttachedObject._innerDoor.IsOpen(),
+ AttachedObject._outerDoor.IsOpen(),
+ AttachedObject._pressurized
+ )
+ );
+}
diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs
index 4177a6c2..b50b1e90 100644
--- a/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs
+++ b/QSB/EchoesOfTheEye/AlarmTotemSync/AlarmTotemManager.cs
@@ -15,7 +15,7 @@ public class AlarmTotemManager : WorldObjectManager
public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct)
{
- QSBWorldSync.Init();
+ // QSBWorldSync.Init();
QSBWorldSync.Init();
_qsbAlarmSequenceController = new GameObject(nameof(QSBAlarmSequenceController))
diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs
index bebdf5f1..93169d62 100644
--- a/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs
+++ b/QSB/EchoesOfTheEye/AlarmTotemSync/Patches/AlarmTotemPatches.cs
@@ -12,6 +12,7 @@ public class AlarmTotemPatches : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
+ /*
[HarmonyPrefix]
[HarmonyPatch(typeof(AlarmTotem), nameof(AlarmTotem.OnSectorOccupantAdded))]
private static void OnSectorOccupantAdded(AlarmTotem __instance, SectorDetector sectorDetector)
@@ -80,6 +81,7 @@ public class AlarmTotemPatches : QSBPatch
return false;
}
+ */
[HarmonyPrefix]
[HarmonyPatch(typeof(AlarmBell), nameof(AlarmBell.OnEntry))]
diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs
index e5e1196f..0a58ff8d 100644
--- a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs
+++ b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmBell.cs
@@ -2,7 +2,4 @@
namespace QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
-public class QSBAlarmBell : WorldObject
-{
- public override void SendInitialState(uint to) { }
-}
+public class QSBAlarmBell : WorldObject { }
diff --git a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs
index e6801c82..69ff7f9b 100644
--- a/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs
+++ b/QSB/EchoesOfTheEye/AlarmTotemSync/WorldObjects/QSBAlarmTotem.cs
@@ -8,6 +8,9 @@ using System.Threading;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
+///
+/// TODO: make this not NRE (by not doing enable sync) and then readd it back in
+///
public class QSBAlarmTotem : WorldObject
{
public readonly List VisibleFor = new();
diff --git a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs
index a3b4a90f..1f5fe10b 100644
--- a/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs
+++ b/QSB/EchoesOfTheEye/DreamLantern/WorldObjects/QSBDreamLantern.cs
@@ -4,6 +4,9 @@ using QSB.WorldSync;
namespace QSB.EchoesOfTheEye.DreamLantern.WorldObjects;
+///
+/// TODO: lanterns held by ghosts should only be controlled by the host (to prevent it from visually freaking out)
+///
public class QSBDreamLantern : WorldObject
{
public override void SendInitialState(uint to)
diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs
index 56fdd832..37391ee6 100644
--- a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs
+++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBDreamRaft.cs
@@ -6,8 +6,6 @@ namespace QSB.EchoesOfTheEye.DreamRafts.WorldObjects;
public class QSBDreamRaft : LinkedWorldObject
{
- public override void SendInitialState(uint to) { }
-
protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.RaftPrefab;
protected override bool SpawnWithServerAuthority => false;
}
diff --git a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs
index 6f5e5f59..f4385f4e 100644
--- a/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs
+++ b/QSB/EchoesOfTheEye/DreamRafts/WorldObjects/QSBSealRaft.cs
@@ -6,8 +6,6 @@ namespace QSB.EchoesOfTheEye.DreamRafts.WorldObjects;
public class QSBSealRaft : LinkedWorldObject
{
- public override void SendInitialState(uint to) { }
-
protected override GameObject NetworkObjectPrefab => QSBNetworkManager.singleton.RaftPrefab;
protected override bool SpawnWithServerAuthority => false;
}
diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs
index 7ed84448..72d9ef70 100644
--- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs
+++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostBrain.cs
@@ -21,7 +21,7 @@ public class QSBGhostBrain : WorldObject, IGhostObject
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState
}
public override async UniTask Init(CancellationToken ct)
diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs
index 6be64ff1..228ad983 100644
--- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs
+++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostController.cs
@@ -17,7 +17,7 @@ public class QSBGhostController : WorldObject, IGhostObject
{
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState
}
public QSBGhostEffects _effects;
diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs
index df1f136d..bb31271f 100644
--- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs
+++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostEffects.cs
@@ -16,7 +16,7 @@ public class QSBGhostEffects : WorldObject, IGhostObject
{
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState
}
public override bool ShouldDisplayDebug() => false;
diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs
index 804c2922..437eae15 100644
--- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs
+++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostGrabController.cs
@@ -11,7 +11,7 @@ public class QSBGhostGrabController : WorldObject
{
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState
}
public void GrabPlayer(float speed, GhostPlayer player, bool remote = false)
diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs
index ad91635f..5226c33e 100644
--- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs
+++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostNodeMap.cs
@@ -11,6 +11,6 @@ internal class QSBGhostNodeMap : WorldObject
{
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState??
}
}
diff --git a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs
index 952f92e0..af8b8d10 100644
--- a/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs
+++ b/QSB/EchoesOfTheEye/Ghosts/WorldObjects/QSBGhostSensors.cs
@@ -15,7 +15,7 @@ public class QSBGhostSensors : WorldObject, IGhostObject
{
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState
}
public override string ReturnLabel() => "";
diff --git a/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs b/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs
index 590ea09c..8703f72e 100644
--- a/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs
+++ b/QSB/EchoesOfTheEye/GrappleTotemSync/WorldObjects/QSBGrappleTotem.cs
@@ -2,7 +2,4 @@
namespace QSB.EchoesOfTheEye.GrappleTotemSync.WorldObjects;
-public class QSBGrappleTotem : WorldObject
-{
- public override void SendInitialState(uint to) { }
-}
+public class QSBGrappleTotem : WorldObject { }
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatedByMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatedByMessage.cs
deleted file mode 100644
index d9cdcfc8..00000000
--- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatedByMessage.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using QSB.EchoesOfTheEye.LightSensorSync.WorldObjects;
-using QSB.Messaging;
-using System.Linq;
-
-namespace QSB.EchoesOfTheEye.LightSensorSync.Messages;
-
-///
-/// always sent by host
-///
-internal class IlluminatedByMessage : QSBWorldObjectMessage
-{
- public IlluminatedByMessage(uint[] illuminatedBy) : base(illuminatedBy) { }
-
- public override void OnReceiveRemote()
- {
- foreach (var added in Data.Except(WorldObject._illuminatedBy))
- {
- WorldObject.SetIlluminated(added, true);
- }
-
- foreach (var removed in WorldObject._illuminatedBy.Except(Data))
- {
- WorldObject.SetIlluminated(removed, false);
- }
- }
-}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs
index 4a347a2b..f557cbf6 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/IlluminatingLanternsMessage.cs
@@ -14,12 +14,6 @@ internal class IlluminatingLanternsMessage : QSBWorldObjectMessage x.GetWorldObject().AttachedObject));
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatedByMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatedByMessage.cs
deleted file mode 100644
index a86b0cba..00000000
--- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatedByMessage.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using QSB.Messaging;
-using QSB.Player;
-using System.Linq;
-
-namespace QSB.EchoesOfTheEye.LightSensorSync.Messages;
-
-///
-/// always sent by host
-///
-internal class PlayerIlluminatedByMessage : QSBMessage<(uint playerId, uint[] illuminatedBy)>
-{
- public PlayerIlluminatedByMessage(uint playerId, uint[] illuminatedBy) : base((playerId, illuminatedBy)) { }
-
- public override void OnReceiveRemote()
- {
- var qsbPlayerLightSensor = QSBPlayerManager.GetPlayer(Data.playerId).QSBPlayerLightSensor;
-
- foreach (var added in Data.illuminatedBy.Except(qsbPlayerLightSensor._illuminatedBy))
- {
- qsbPlayerLightSensor.SetIlluminated(added, true);
- }
-
- foreach (var removed in qsbPlayerLightSensor._illuminatedBy.Except(Data.illuminatedBy))
- {
- qsbPlayerLightSensor.SetIlluminated(removed, false);
- }
- }
-}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs
index 6e6a7d5a..4cc78e69 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerIlluminatingLanternsMessage.cs
@@ -19,12 +19,6 @@ internal class PlayerIlluminatingLanternsMessage : QSBMessage<(uint playerId, in
{
var lightSensor = (SingleLightSensor)QSBPlayerManager.GetPlayer(Data.playerId).LightSensor;
- if (lightSensor.enabled)
- {
- // sensor is enabled, so this will already be synced
- return;
- }
-
lightSensor._illuminatingDreamLanternList.Clear();
lightSensor._illuminatingDreamLanternList.AddRange(
Data.lanterns.Select(x => x.GetWorldObject().AttachedObject));
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs
index acd6abfe..09e3f171 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/PlayerSetIlluminatedMessage.cs
@@ -6,8 +6,24 @@ namespace QSB.EchoesOfTheEye.LightSensorSync.Messages;
internal class PlayerSetIlluminatedMessage : QSBMessage<(uint playerId, bool illuminated)>
{
public PlayerSetIlluminatedMessage(uint playerId, bool illuminated) : base((playerId, illuminated)) { }
- public override void OnReceiveLocal() => OnReceiveRemote();
- public override void OnReceiveRemote() =>
- QSBPlayerManager.GetPlayer(Data.playerId).QSBPlayerLightSensor.SetIlluminated(From, Data.illuminated);
+ public override void OnReceiveRemote()
+ {
+ var lightSensor = (SingleLightSensor)QSBPlayerManager.GetPlayer(Data.playerId).LightSensor;
+
+ if (lightSensor._illuminated == Data.illuminated)
+ {
+ return;
+ }
+
+ lightSensor._illuminated = Data.illuminated;
+ if (Data.illuminated)
+ {
+ lightSensor.OnDetectLight.Invoke();
+ }
+ else
+ {
+ lightSensor.OnDetectDarkness.Invoke();
+ }
+ }
}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs b/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs
index c4407f0c..74e517d6 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/Messages/SetIlluminatedMessage.cs
@@ -6,6 +6,22 @@ namespace QSB.EchoesOfTheEye.LightSensorSync.Messages;
internal class SetIlluminatedMessage : QSBWorldObjectMessage
{
public SetIlluminatedMessage(bool illuminated) : base(illuminated) { }
- public override void OnReceiveLocal() => OnReceiveRemote();
- public override void OnReceiveRemote() => WorldObject.SetIlluminated(From, Data);
+
+ public override void OnReceiveRemote()
+ {
+ if (WorldObject.AttachedObject._illuminated == Data)
+ {
+ return;
+ }
+
+ WorldObject.AttachedObject._illuminated = Data;
+ if (Data)
+ {
+ WorldObject.AttachedObject.OnDetectLight.Invoke();
+ }
+ else
+ {
+ WorldObject.AttachedObject.OnDetectDarkness.Invoke();
+ }
+ }
}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs b/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs
index 948eca3d..783e7d16 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/Patches/LightSensorPatches.cs
@@ -1,13 +1,23 @@
using HarmonyLib;
+using QSB.AuthoritySync;
using QSB.EchoesOfTheEye.LightSensorSync.Messages;
using QSB.EchoesOfTheEye.LightSensorSync.WorldObjects;
using QSB.Messaging;
using QSB.Patches;
+using QSB.Player;
+using QSB.Tools.FlashlightTool;
+using QSB.Tools.ProbeTool;
+using QSB.Utility;
using QSB.WorldSync;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
+/*
+ * For those who come here,
+ * leave while you still can.
+ */
+
namespace QSB.EchoesOfTheEye.LightSensorSync.Patches;
[HarmonyPatch(typeof(SingleLightSensor))]
@@ -15,92 +25,26 @@ internal class LightSensorPatches : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
- [HarmonyPrefix]
- [HarmonyPatch(nameof(SingleLightSensor.Start))]
- private static bool Start(SingleLightSensor __instance)
- {
- if (!QSBWorldSync.AllObjectsReady)
- {
- return true;
- }
-
- var isPlayerLightSensor = LightSensorManager.IsPlayerLightSensor(__instance);
- var qsbPlayerLightSensor = isPlayerLightSensor ? __instance.GetComponent() : null;
- var qsbLightSensor = isPlayerLightSensor ? null : __instance.GetWorldObject();
-
- if (__instance._lightDetector != null)
- {
- __instance._lightSources = new List();
- __instance._lightSourceMask = LightSourceType.VOLUME_ONLY;
- if (__instance._detectFlashlight)
- {
- __instance._lightSourceMask |= LightSourceType.FLASHLIGHT;
- }
-
- if (__instance._detectProbe)
- {
- __instance._lightSourceMask |= LightSourceType.PROBE;
- }
-
- if (__instance._detectDreamLanterns)
- {
- __instance._lightSourceMask |= LightSourceType.DREAM_LANTERN;
- }
-
- if (__instance._detectSimpleLanterns)
- {
- __instance._lightSourceMask |= LightSourceType.SIMPLE_LANTERN;
- }
-
- __instance._lightDetector.OnLightVolumeEnter += __instance.OnLightSourceEnter;
- __instance._lightDetector.OnLightVolumeExit += __instance.OnLightSourceExit;
- }
- else
- {
- Debug.LogError("LightSensor has no LightSourceDetector", __instance);
- }
-
- if (__instance._sector != null)
- {
- __instance.enabled = false;
- __instance._lightDetector.GetShape().enabled = false;
- if (__instance._startIlluminated)
- {
- if (isPlayerLightSensor)
- {
- qsbPlayerLightSensor._locallyIlluminated = true;
- new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, true).Send();
- }
- else
- {
- qsbLightSensor._locallyIlluminated = true;
- qsbLightSensor.OnDetectLocalLight?.Invoke();
- qsbLightSensor.SendMessage(new SetIlluminatedMessage(true));
- }
- }
- }
-
- return false;
- }
-
[HarmonyPrefix]
[HarmonyPatch(nameof(SingleLightSensor.OnSectorOccupantsUpdated))]
private static bool OnSectorOccupantsUpdated(SingleLightSensor __instance)
{
+ if (LightSensorManager.IsPlayerLightSensor(__instance))
+ {
+ return true;
+ }
if (!QSBWorldSync.AllObjectsReady)
{
return true;
}
-
- var isPlayerLightSensor = LightSensorManager.IsPlayerLightSensor(__instance);
- var qsbPlayerLightSensor = isPlayerLightSensor ? __instance.GetComponent() : null;
- var qsbLightSensor = isPlayerLightSensor ? null : __instance.GetWorldObject();
+ var qsbLightSensor = __instance.GetWorldObject();
var containsAnyOccupants = __instance._sector.ContainsAnyOccupants(DynamicOccupant.Player | DynamicOccupant.Probe);
if (containsAnyOccupants && !__instance.enabled)
{
__instance.enabled = true;
__instance._lightDetector.GetShape().enabled = true;
+ qsbLightSensor.RequestOwnership();
if (__instance._preserveStateWhileDisabled)
{
__instance._fixedUpdateFrameDelayCount = 10;
@@ -110,118 +54,233 @@ internal class LightSensorPatches : QSBPatch
{
__instance.enabled = false;
__instance._lightDetector.GetShape().enabled = false;
+ qsbLightSensor.ReleaseOwnership();
if (!__instance._preserveStateWhileDisabled)
{
- if (isPlayerLightSensor)
+ if (__instance._illuminated)
{
- if (qsbPlayerLightSensor._locallyIlluminated)
+ Delay.RunFramesLater(10, () =>
{
- qsbPlayerLightSensor._locallyIlluminated = false;
- new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, false).Send();
- }
+ // no one else took ownership, so we can safely make not illuminated
+ // ie turn off when no one else is there
+ if (qsbLightSensor.Owner == 0)
+ {
+ __instance._illuminated = false;
+ __instance.OnDetectDarkness.Invoke();
+ qsbLightSensor.SendMessage(new SetIlluminatedMessage(false));
+ }
+ });
}
- else
+
+ if (qsbLightSensor._locallyIlluminated)
{
- if (qsbLightSensor._locallyIlluminated)
- {
- qsbLightSensor._locallyIlluminated = false;
- qsbLightSensor.OnDetectLocalDarkness?.Invoke();
- qsbLightSensor.SendMessage(new SetIlluminatedMessage(false));
- }
+ qsbLightSensor._locallyIlluminated = false;
+ qsbLightSensor.OnDetectLocalDarkness?.Invoke();
}
}
}
-
return false;
}
///
/// to prevent allocating a new list every frame
///
- private static readonly List _prevIlluminatingDreamLanternList = new();
+ private static readonly List _illuminatingDreamLanternList = new();
[HarmonyPrefix]
[HarmonyPatch(nameof(SingleLightSensor.ManagedFixedUpdate))]
private static bool ManagedFixedUpdate(SingleLightSensor __instance)
{
+ if (LightSensorManager.IsPlayerLightSensor(__instance))
+ {
+ return true;
+ }
if (!QSBWorldSync.AllObjectsReady)
{
return true;
}
-
- var isPlayerLightSensor = LightSensorManager.IsPlayerLightSensor(__instance);
- var qsbPlayerLightSensor = isPlayerLightSensor ? __instance.GetComponent() : null;
- var qsbLightSensor = isPlayerLightSensor ? null : __instance.GetWorldObject();
+ var qsbLightSensor = __instance.GetWorldObject();
if (__instance._fixedUpdateFrameDelayCount > 0)
{
__instance._fixedUpdateFrameDelayCount--;
+ return false;
}
-
+ var illuminated = __instance._illuminated;
+ var locallyIlluminated = qsbLightSensor._locallyIlluminated;
if (__instance._illuminatingDreamLanternList != null)
{
- _prevIlluminatingDreamLanternList.Clear();
- _prevIlluminatingDreamLanternList.AddRange(__instance._illuminatingDreamLanternList);
+ _illuminatingDreamLanternList.Clear();
+ _illuminatingDreamLanternList.AddRange(__instance._illuminatingDreamLanternList);
}
-
- var illuminated = __instance._illuminated;
__instance.UpdateIllumination();
- bool locallyIlluminated;
- if (isPlayerLightSensor)
+ if (qsbLightSensor.Owner == QSBPlayerManager.LocalPlayerId)
{
- locallyIlluminated = qsbPlayerLightSensor._locallyIlluminated;
- qsbPlayerLightSensor._locallyIlluminated = __instance._illuminated;
- }
- else
- {
- locallyIlluminated = qsbLightSensor._locallyIlluminated;
- qsbLightSensor._locallyIlluminated = __instance._illuminated;
- }
-
- __instance._illuminated = illuminated;
-
- if (isPlayerLightSensor)
- {
- if (!locallyIlluminated && qsbPlayerLightSensor._locallyIlluminated)
+ if (!illuminated && __instance._illuminated)
{
- qsbPlayerLightSensor._locallyIlluminated = true;
- new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, true).Send();
- }
- else if (locallyIlluminated && !qsbPlayerLightSensor._locallyIlluminated)
- {
- qsbPlayerLightSensor._locallyIlluminated = false;
- new PlayerSetIlluminatedMessage(qsbPlayerLightSensor.PlayerId, false).Send();
- }
- }
- else
- {
- if (!locallyIlluminated && qsbLightSensor._locallyIlluminated)
- {
- qsbLightSensor._locallyIlluminated = true;
- qsbLightSensor.OnDetectLocalLight?.Invoke();
+ __instance.OnDetectLight.Invoke();
qsbLightSensor.SendMessage(new SetIlluminatedMessage(true));
}
- else if (locallyIlluminated && !qsbLightSensor._locallyIlluminated)
+ else if (illuminated && !__instance._illuminated)
{
- qsbLightSensor._locallyIlluminated = false;
- qsbLightSensor.OnDetectLocalDarkness?.Invoke();
+ __instance.OnDetectDarkness.Invoke();
qsbLightSensor.SendMessage(new SetIlluminatedMessage(false));
}
- }
-
- if (__instance._illuminatingDreamLanternList != null
- && !__instance._illuminatingDreamLanternList.SequenceEqual(_prevIlluminatingDreamLanternList))
- {
- if (isPlayerLightSensor)
- {
- new PlayerIlluminatingLanternsMessage(qsbPlayerLightSensor.PlayerId, __instance._illuminatingDreamLanternList).Send();
- }
- else
+ if (__instance._illuminatingDreamLanternList != null &&
+ !__instance._illuminatingDreamLanternList.SequenceEqual(_illuminatingDreamLanternList))
{
qsbLightSensor.SendMessage(new IlluminatingLanternsMessage(__instance._illuminatingDreamLanternList));
}
}
+ if (!locallyIlluminated && qsbLightSensor._locallyIlluminated)
+ {
+ qsbLightSensor.OnDetectLocalLight?.Invoke();
+ }
+ else if (locallyIlluminated && !qsbLightSensor._locallyIlluminated)
+ {
+ qsbLightSensor.OnDetectLocalDarkness?.Invoke();
+ }
+ return false;
+ }
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(SingleLightSensor.UpdateIllumination))]
+ private static bool UpdateIllumination(SingleLightSensor __instance)
+ {
+ if (LightSensorManager.IsPlayerLightSensor(__instance))
+ {
+ return true;
+ }
+ if (!QSBWorldSync.AllObjectsReady)
+ {
+ return true;
+ }
+ var qsbLightSensor = __instance.GetWorldObject();
+
+ __instance._illuminated = false;
+ qsbLightSensor._locallyIlluminated = false;
+ __instance._illuminatingDreamLanternList?.Clear();
+ if (__instance._lightSources == null || __instance._lightSources.Count == 0)
+ {
+ return false;
+ }
+ var sensorWorldPos = __instance.transform.TransformPoint(__instance._localSensorOffset);
+ var sensorWorldDir = Vector3.zero;
+ if (__instance._directionalSensor)
+ {
+ sensorWorldDir = __instance.transform.TransformDirection(__instance._localDirection).normalized;
+ }
+ foreach (var lightSource in __instance._lightSources)
+ {
+ if ((__instance._lightSourceMask & lightSource.GetLightSourceType()) == lightSource.GetLightSourceType() &&
+ lightSource.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance))
+ {
+ switch (lightSource.GetLightSourceType())
+ {
+ case LightSourceType.UNDEFINED:
+ {
+ var owlight = lightSource as OWLight2;
+ var occludableLight = owlight.GetLight().shadows != LightShadows.None &&
+ owlight.GetLight().shadowStrength > 0.5f;
+ if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight))
+ {
+ __instance._illuminated = true;
+ }
+ break;
+ }
+ case LightSourceType.FLASHLIGHT:
+ {
+ if (lightSource is QSBFlashlight qsbFlashlight)
+ {
+ var position = qsbFlashlight.Player.Camera.transform.position;
+ var vector3 = __instance.transform.position - position;
+ if (Vector3.Angle(qsbFlashlight.Player.Camera.transform.forward, vector3) <= __instance._maxSpotHalfAngle &&
+ !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ else
+ {
+ var position = Locator.GetPlayerCamera().transform.position;
+ var vector3 = __instance.transform.position - position;
+ if (Vector3.Angle(Locator.GetPlayerCamera().transform.forward, vector3) <= __instance._maxSpotHalfAngle &&
+ !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ qsbLightSensor._locallyIlluminated = true;
+ }
+ }
+ break;
+ }
+ case LightSourceType.PROBE:
+ {
+ if (lightSource is QSBProbe qsbProbe)
+ {
+ var probe = qsbProbe;
+ if (probe != null &&
+ probe.IsLaunched() &&
+ !probe.IsRetrieving() &&
+ probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ else
+ {
+ var probe = Locator.GetProbe();
+ if (probe != null &&
+ probe.IsLaunched() &&
+ !probe.IsRetrieving() &&
+ probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ qsbLightSensor._locallyIlluminated = true;
+ }
+ }
+ break;
+ }
+ case LightSourceType.DREAM_LANTERN:
+ {
+ var dreamLanternController = lightSource as DreamLanternController;
+ if (dreamLanternController.IsLit() &&
+ dreamLanternController.IsFocused(__instance._lanternFocusThreshold) &&
+ dreamLanternController.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(dreamLanternController.GetLightPosition(), sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminatingDreamLanternList.Add(dreamLanternController);
+ __instance._illuminated = true;
+
+ var dreamLanternItem = dreamLanternController.GetComponent();
+ qsbLightSensor._locallyIlluminated |= QSBPlayerManager.LocalPlayer.HeldItem?.AttachedObject == dreamLanternItem;
+ }
+ break;
+ }
+ case LightSourceType.SIMPLE_LANTERN:
+ foreach (var owlight in lightSource.GetLights())
+ {
+ var occludableLight = owlight.GetLight().shadows != LightShadows.None &&
+ owlight.GetLight().shadowStrength > 0.5f;
+ var maxDistance = Mathf.Min(__instance._maxSimpleLanternDistance, __instance._maxDistance);
+ if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, maxDistance) &&
+ !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight))
+ {
+ __instance._illuminated = true;
+
+ var simpleLanternItem = (SimpleLanternItem)lightSource;
+ qsbLightSensor._locallyIlluminated |= QSBPlayerManager.LocalPlayer.HeldItem?.AttachedObject == simpleLanternItem;
+ }
+ }
+ break;
+ case LightSourceType.VOLUME_ONLY:
+ __instance._illuminated = true;
+ break;
+ }
+ }
+ }
return false;
}
}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/Patches/PlayerLightSensorPatches.cs b/QSB/EchoesOfTheEye/LightSensorSync/Patches/PlayerLightSensorPatches.cs
new file mode 100644
index 00000000..1260ce50
--- /dev/null
+++ b/QSB/EchoesOfTheEye/LightSensorSync/Patches/PlayerLightSensorPatches.cs
@@ -0,0 +1,197 @@
+using HarmonyLib;
+using QSB.EchoesOfTheEye.LightSensorSync.Messages;
+using QSB.Messaging;
+using QSB.Patches;
+using QSB.Player;
+using QSB.Tools.FlashlightTool;
+using QSB.Tools.ProbeTool;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+/*
+ * For those who come here,
+ * leave while you still can.
+ */
+
+namespace QSB.EchoesOfTheEye.LightSensorSync.Patches;
+
+///
+/// remote player light sensors are disabled, so these will only run for the local player light sensor
+///
+[HarmonyPatch(typeof(SingleLightSensor))]
+internal class PlayerLightSensorPatches : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
+
+ ///
+ /// to prevent allocating a new list every frame
+ ///
+ private static readonly List _illuminatingDreamLanternList = new();
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(SingleLightSensor.ManagedFixedUpdate))]
+ private static bool ManagedFixedUpdate(SingleLightSensor __instance)
+ {
+ if (!LightSensorManager.IsPlayerLightSensor(__instance))
+ {
+ return true;
+ }
+
+ if (__instance._fixedUpdateFrameDelayCount > 0)
+ {
+ __instance._fixedUpdateFrameDelayCount--;
+ return false;
+ }
+ var illuminated = __instance._illuminated;
+ if (__instance._illuminatingDreamLanternList != null)
+ {
+ _illuminatingDreamLanternList.Clear();
+ _illuminatingDreamLanternList.AddRange(__instance._illuminatingDreamLanternList);
+ }
+ __instance.UpdateIllumination();
+ if (!illuminated && __instance._illuminated)
+ {
+ __instance.OnDetectLight.Invoke();
+ new PlayerSetIlluminatedMessage(QSBPlayerManager.LocalPlayerId, true).Send();
+ }
+ else if (illuminated && !__instance._illuminated)
+ {
+ __instance.OnDetectDarkness.Invoke();
+ new PlayerSetIlluminatedMessage(QSBPlayerManager.LocalPlayerId, false).Send();
+ }
+ if (__instance._illuminatingDreamLanternList != null &&
+ !__instance._illuminatingDreamLanternList.SequenceEqual(_illuminatingDreamLanternList))
+ {
+ new PlayerIlluminatingLanternsMessage(QSBPlayerManager.LocalPlayerId, __instance._illuminatingDreamLanternList).Send();
+ }
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(SingleLightSensor.UpdateIllumination))]
+ private static bool UpdateIllumination(SingleLightSensor __instance)
+ {
+ if (!LightSensorManager.IsPlayerLightSensor(__instance))
+ {
+ return true;
+ }
+
+ __instance._illuminated = false;
+ __instance._illuminatingDreamLanternList?.Clear();
+ if (__instance._lightSources == null || __instance._lightSources.Count == 0)
+ {
+ return false;
+ }
+ var sensorWorldPos = __instance.transform.TransformPoint(__instance._localSensorOffset);
+ var sensorWorldDir = Vector3.zero;
+ if (__instance._directionalSensor)
+ {
+ sensorWorldDir = __instance.transform.TransformDirection(__instance._localDirection).normalized;
+ }
+ foreach (var lightSource in __instance._lightSources)
+ {
+ if ((__instance._lightSourceMask & lightSource.GetLightSourceType()) == lightSource.GetLightSourceType() &&
+ lightSource.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance))
+ {
+ switch (lightSource.GetLightSourceType())
+ {
+ case LightSourceType.UNDEFINED:
+ {
+ var owlight = lightSource as OWLight2;
+ var occludableLight = owlight.GetLight().shadows != LightShadows.None &&
+ owlight.GetLight().shadowStrength > 0.5f;
+ if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight))
+ {
+ __instance._illuminated = true;
+ }
+ break;
+ }
+ case LightSourceType.FLASHLIGHT:
+ {
+ if (lightSource is QSBFlashlight qsbFlashlight)
+ {
+ var position = qsbFlashlight.Player.Camera.transform.position;
+ var vector3 = __instance.transform.position - position;
+ if (Vector3.Angle(qsbFlashlight.Player.Camera.transform.forward, vector3) <= __instance._maxSpotHalfAngle &&
+ !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ else
+ {
+ var position = Locator.GetPlayerCamera().transform.position;
+ var vector3 = __instance.transform.position - position;
+ if (Vector3.Angle(Locator.GetPlayerCamera().transform.forward, vector3) <= __instance._maxSpotHalfAngle &&
+ !__instance.CheckOcclusion(position, sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ break;
+ }
+ case LightSourceType.PROBE:
+ {
+ if (lightSource is QSBProbe qsbProbe)
+ {
+ var probe = qsbProbe;
+ if (probe != null &&
+ probe.IsLaunched() &&
+ !probe.IsRetrieving() &&
+ probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ else
+ {
+ var probe = Locator.GetProbe();
+ if (probe != null &&
+ probe.IsLaunched() &&
+ !probe.IsRetrieving() &&
+ probe.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(probe.GetLightSourcePosition(), sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ break;
+ }
+ case LightSourceType.DREAM_LANTERN:
+ {
+ var dreamLanternController = lightSource as DreamLanternController;
+ if (dreamLanternController.IsLit() &&
+ dreamLanternController.IsFocused(__instance._lanternFocusThreshold) &&
+ dreamLanternController.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, __instance._maxDistance) &&
+ !__instance.CheckOcclusion(dreamLanternController.GetLightPosition(), sensorWorldPos, sensorWorldDir))
+ {
+ __instance._illuminatingDreamLanternList.Add(dreamLanternController);
+ __instance._illuminated = true;
+ }
+ break;
+ }
+ case LightSourceType.SIMPLE_LANTERN:
+ foreach (var owlight in lightSource.GetLights())
+ {
+ var occludableLight = owlight.GetLight().shadows != LightShadows.None &&
+ owlight.GetLight().shadowStrength > 0.5f;
+ var maxDistance = Mathf.Min(__instance._maxSimpleLanternDistance, __instance._maxDistance);
+ if (owlight.CheckIlluminationAtPoint(sensorWorldPos, __instance._sensorRadius, maxDistance) &&
+ !__instance.CheckOcclusion(owlight.transform.position, sensorWorldPos, sensorWorldDir, occludableLight))
+ {
+ __instance._illuminated = true;
+ }
+ }
+ break;
+ case LightSourceType.VOLUME_ONLY:
+ __instance._illuminated = true;
+ break;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs b/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs
index 01773432..5c7a7e42 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/QSBPlayerLightSensor.cs
@@ -2,74 +2,49 @@
using QSB.Messaging;
using QSB.Player;
using QSB.WorldSync;
-using System;
-using System.Collections.Generic;
using System.Linq;
using UnityEngine;
+/*
+ * For those who come here,
+ * leave while you still can.
+ */
+
namespace QSB.EchoesOfTheEye.LightSensorSync;
///
-/// stores a bit of extra data needed for player light sensor sync
-/// todo you might be able to remove when you simplify light sensor after the fake sector thingy
+/// only purpose is to handle initial state sync.
+///
+/// we don't have to worry about start illuminated or sectors.
+/// authority is always given to local player light sensor.
+///
+/// 2 uses:
+/// - AlarmTotem.CheckPlayerVisible
+/// - GhostSensors.FixedUpdate_Sensors
///
[RequireComponent(typeof(SingleLightSensor))]
public class QSBPlayerLightSensor : MonoBehaviour
{
private SingleLightSensor _lightSensor;
- [NonSerialized]
- public uint PlayerId;
-
- internal bool _locallyIlluminated;
- internal readonly List _illuminatedBy = new();
+ private uint _playerId;
private void Awake()
{
_lightSensor = GetComponent();
- PlayerId = QSBPlayerManager.PlayerList.First(x => x.LightSensor == _lightSensor).PlayerId;
+ _playerId = QSBPlayerManager.PlayerList.First(x => x.LightSensor == _lightSensor).PlayerId;
RequestInitialStatesMessage.SendInitialState += SendInitialState;
- QSBPlayerManager.OnRemovePlayer += OnPlayerLeave;
}
- private void OnDestroy()
- {
+ private void OnDestroy() =>
RequestInitialStatesMessage.SendInitialState -= SendInitialState;
- QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave;
- }
private void SendInitialState(uint to)
{
- new PlayerIlluminatedByMessage(PlayerId, _illuminatedBy.ToArray()) { To = to }.Send();
+ new PlayerSetIlluminatedMessage(_playerId, _lightSensor._illuminated) { To = to }.Send();
if (_lightSensor._illuminatingDreamLanternList != null)
{
- new PlayerIlluminatingLanternsMessage(PlayerId, _lightSensor._illuminatingDreamLanternList) { To = to }.Send();
- }
- }
-
- private void OnPlayerLeave(PlayerInfo player) => SetIlluminated(player.PlayerId, false);
-
- public void SetIlluminated(uint playerId, bool locallyIlluminated)
- {
- var illuminated = _illuminatedBy.Count > 0;
- if (locallyIlluminated)
- {
- _illuminatedBy.SafeAdd(playerId);
- }
- else
- {
- _illuminatedBy.QuickRemove(playerId);
- }
-
- if (!illuminated && _illuminatedBy.Count > 0)
- {
- _lightSensor._illuminated = true;
- _lightSensor.OnDetectLight.Invoke();
- }
- else if (illuminated && _illuminatedBy.Count == 0)
- {
- _lightSensor._illuminated = false;
- _lightSensor.OnDetectDarkness.Invoke();
+ new PlayerIlluminatingLanternsMessage(_playerId, _lightSensor._illuminatingDreamLanternList) { To = to }.Send();
}
}
}
diff --git a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs
index 51f3e502..7a4f3ef2 100644
--- a/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs
+++ b/QSB/EchoesOfTheEye/LightSensorSync/WorldObjects/QSBLightSensor.cs
@@ -1,60 +1,60 @@
using Cysharp.Threading.Tasks;
+using QSB.AuthoritySync;
using QSB.EchoesOfTheEye.LightSensorSync.Messages;
using QSB.Messaging;
-using QSB.Player;
+using QSB.Utility;
using QSB.WorldSync;
using System;
-using System.Collections.Generic;
using System.Threading;
+/*
+ * For those who come here,
+ * leave while you still can.
+ */
+
namespace QSB.EchoesOfTheEye.LightSensorSync.WorldObjects;
///
-/// todo: simplify this after we do fake sectors (we dont need to store a list, we can just do it locally and then sync if it's disabled)
+/// BUG: this breaks in zone2.
+/// the sector it's enabled in is bigger than the sector the zone2 walls are enabled in :(
+/// maybe this can be fixed by making the collision group use the same sector.
///
-internal class QSBLightSensor : WorldObject
+internal class QSBLightSensor : AuthWorldObject
{
internal bool _locallyIlluminated;
public Action OnDetectLocalLight;
public Action OnDetectLocalDarkness;
- internal readonly List _illuminatedBy = new();
+
+ public override bool CanOwn => AttachedObject.enabled;
public override void SendInitialState(uint to)
{
- this.SendMessage(new IlluminatedByMessage(_illuminatedBy.ToArray()) { To = to });
+ base.SendInitialState(to);
+
+ this.SendMessage(new SetIlluminatedMessage(AttachedObject._illuminated) { To = to });
if (AttachedObject._illuminatingDreamLanternList != null)
{
this.SendMessage(new IlluminatingLanternsMessage(AttachedObject._illuminatingDreamLanternList) { To = to });
}
}
- public override async UniTask Init(CancellationToken ct) => QSBPlayerManager.OnRemovePlayer += OnPlayerLeave;
- public override void OnRemoval() => QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave;
- private void OnPlayerLeave(PlayerInfo player) => SetIlluminated(player.PlayerId, false);
-
- public void SetIlluminated(uint playerId, bool locallyIlluminated)
+ public override async UniTask Init(CancellationToken ct)
{
- var illuminated = _illuminatedBy.Count > 0;
- if (locallyIlluminated)
- {
- _illuminatedBy.SafeAdd(playerId);
- }
- else
- {
- _illuminatedBy.QuickRemove(playerId);
- }
+ await base.Init(ct);
- if (!illuminated && _illuminatedBy.Count > 0)
+ // do this stuff here instead of Start, since world objects won't be ready by that point
+ Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, () =>
{
- AttachedObject._illuminated = true;
- AttachedObject.OnDetectLight.Invoke();
- }
- else if (illuminated && _illuminatedBy.Count == 0)
- {
- AttachedObject._illuminated = false;
- AttachedObject.OnDetectDarkness.Invoke();
- }
+ if (AttachedObject._sector != null)
+ {
+ if (AttachedObject._startIlluminated)
+ {
+ _locallyIlluminated = true;
+ OnDetectLocalLight?.Invoke();
+ }
+ }
+ });
}
}
diff --git a/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs b/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs
index 744fa558..eb7a4d75 100644
--- a/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs
+++ b/QSB/EchoesOfTheEye/Prisoner/Messages/CellevatorCallMessage.cs
@@ -1,13 +1,10 @@
using QSB.EchoesOfTheEye.Prisoner.WorldObjects;
using QSB.Messaging;
-using QSB.Patches;
namespace QSB.EchoesOfTheEye.Prisoner.Messages;
internal class CellevatorCallMessage : QSBWorldObjectMessage
{
public CellevatorCallMessage(int floorIndex) : base(floorIndex) { }
-
- public override void OnReceiveRemote() =>
- QSBPatch.RemoteCall(() => WorldObject.AttachedObject.CallElevatorToFloor(Data));
+ public override void OnReceiveRemote() => WorldObject.AttachedObject.CallElevatorToFloor(Data);
}
diff --git a/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs b/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs
index 61ba4533..55bb5284 100644
--- a/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs
+++ b/QSB/EchoesOfTheEye/Prisoner/Patches/CellevatorPatches.cs
@@ -13,25 +13,26 @@ public class CellevatorPatches : QSBPatch
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
[HarmonyPrefix]
- [HarmonyPatch(nameof(PrisonCellElevator.CallElevatorToFloor))]
- public static void CallElevatorToFloor(PrisonCellElevator __instance, int floorIndex)
+ [HarmonyPatch(nameof(PrisonCellElevator.CallToTopFloor))]
+ public static void CallToTopFloor(PrisonCellElevator __instance)
{
- if (Remote)
- {
- return;
- }
-
- if (__instance._targetFloorIndex == floorIndex)
- {
- return;
- }
-
if (!QSBWorldSync.AllObjectsReady)
{
return;
}
-
__instance.GetWorldObject()
- .SendMessage(new CellevatorCallMessage(floorIndex));
+ .SendMessage(new CellevatorCallMessage(1));
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PrisonCellElevator.CallToBottomFloor))]
+ public static void CallToBottomFloor(PrisonCellElevator __instance)
+ {
+ if (!QSBWorldSync.AllObjectsReady)
+ {
+ return;
+ }
+ __instance.GetWorldObject()
+ .SendMessage(new CellevatorCallMessage(0));
}
}
diff --git a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs
index 2a6debd9..2330eb69 100644
--- a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs
+++ b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerBrain.cs
@@ -14,7 +14,7 @@ internal class QSBPrisonerBrain : WorldObject, IGhostObject
{
public override void SendInitialState(uint to)
{
-
+ // todo SendInitialState
}
public override async UniTask Init(CancellationToken ct)
diff --git a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs
index 1134d439..552af46d 100644
--- a/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs
+++ b/QSB/EchoesOfTheEye/Prisoner/WorldObjects/QSBPrisonerMarker.cs
@@ -5,7 +5,5 @@ namespace QSB.EchoesOfTheEye.Prisoner.WorldObjects;
internal class QSBPrisonerMarker : WorldObject
{
- public override void SendInitialState(uint to) { }
-
public Transform Transform => AttachedObject.transform;
}
diff --git a/QSB/EchoesOfTheEye/QSBRotatingElements.cs b/QSB/EchoesOfTheEye/QSBRotatingElements.cs
index 082755ec..05b08c69 100644
--- a/QSB/EchoesOfTheEye/QSBRotatingElements.cs
+++ b/QSB/EchoesOfTheEye/QSBRotatingElements.cs
@@ -15,8 +15,6 @@ internal abstract class QSBRotatingElements : LinkedWorldObject
where T : MonoBehaviour
where U : NetworkBehaviour
{
- public override void SendInitialState(uint to) { }
-
protected abstract IEnumerable LightSensors { get; }
private QSBLightSensor[] _qsbLightSensors;
private int _litSensors;
diff --git a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs
index 5950afa9..dc5307d0 100644
--- a/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs
+++ b/QSB/EchoesOfTheEye/RaftSync/TransformSync/RaftTransformSync.cs
@@ -75,32 +75,30 @@ public class RaftTransformSync : UnsectoredRigidbodySync, ILinkedNetworkBehaviou
protected override void ApplyToAttached()
{
var targetPos = ReferenceTransform.FromRelPos(transform.position);
+ var targetRot = ReferenceTransform.FromRelRot(transform.rotation);
- if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime)
+ var onRaft = Locator.GetPlayerController().GetGroundBody() == AttachedRigidbody;
+ if (onRaft)
{
- _lastSetPositionTime = Time.unscaledTime;
-
- var targetRot = ReferenceTransform.FromRelRot(transform.rotation);
-
- var onRaft = false;
- var localPos = Vector3.zero;
- var localRot = Quaternion.identity;
- if (Locator.GetPlayerController().GetGroundBody() == AttachedRigidbody)
+ if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime)
{
- onRaft = true;
- localPos = AttachedRigidbody.transform.InverseTransformPoint(Locator.GetPlayerTransform().position);
- localRot = AttachedRigidbody.transform.InverseTransformRotation(Locator.GetPlayerTransform().rotation);
- }
+ _lastSetPositionTime = Time.unscaledTime;
+ var playerBody = Locator.GetPlayerBody();
+ var relPos = AttachedTransform.ToRelPos(playerBody.GetPosition());
+ var relRot = AttachedTransform.ToRelRot(playerBody.GetRotation());
+
+ AttachedRigidbody.SetPosition(targetPos);
+ AttachedRigidbody.SetRotation(targetRot);
+
+ playerBody.SetPosition(AttachedTransform.FromRelPos(relPos));
+ playerBody.SetRotation(AttachedTransform.FromRelRot(relRot));
+ }
+ }
+ else
+ {
AttachedRigidbody.SetPosition(targetPos);
AttachedRigidbody.SetRotation(targetRot);
-
- if (onRaft)
- {
- var playerTransform = Locator.GetPlayerBody().transform;
- playerTransform.position = AttachedRigidbody.transform.TransformPoint(localPos);
- playerTransform.rotation = AttachedRigidbody.transform.TransformRotation(localRot);
- }
}
var targetVelocity = ReferenceRigidbody.FromRelVel(Velocity, targetPos);
diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs
index ff82c68d..bd1d6ddf 100644
--- a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs
+++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaft.cs
@@ -52,9 +52,4 @@ public class QSBRaft : LinkedWorldObject, IQS
NetworkBehaviour.netIdentity.UpdateAuthQueue(AuthQueueAction.Force);
}
}
-
- public override void SendInitialState(uint to)
- {
- // not really needed. things work fine without it
- }
}
diff --git a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs
index cc8eba4c..ef1dc465 100644
--- a/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs
+++ b/QSB/EchoesOfTheEye/RaftSync/WorldObjects/QSBRaftDock.cs
@@ -8,8 +8,6 @@ public class QSBRaftDock : WorldObject, IQSBDropTarget
{
IItemDropTarget IQSBDropTarget.AttachedObject => AttachedObject;
- public override void SendInitialState(uint to) { }
-
public void OnPressInteract() =>
QSBPatch.RemoteCall(AttachedObject.OnPressInteract);
}
diff --git a/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs b/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs
index c1b59f8a..292e0bfe 100644
--- a/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs
+++ b/QSB/EchoesOfTheEye/SlideProjectors/WorldObjects/QSBSlideProjector.cs
@@ -29,7 +29,6 @@ public class QSBSlideProjector : WorldObject
///
public void SetUser(uint user)
{
- DebugLog.DebugWrite($"{this} - user = {user}");
AttachedObject._interactReceiver.SetInteractionEnabled(user == 0 || user == _user);
_user = user;
}
diff --git a/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs b/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs
index cde9260c..69ceb8f5 100644
--- a/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs
+++ b/QSB/EyeOfTheUniverse/InstrumentSync/WorldObjects/QSBQuantumInstrument.cs
@@ -1,16 +1,10 @@
using QSB.EyeOfTheUniverse.MaskSync;
using QSB.WorldSync;
-using System.Linq;
namespace QSB.EyeOfTheUniverse.InstrumentSync.WorldObjects;
internal class QSBQuantumInstrument : WorldObject
{
- public override void SendInitialState(uint to)
- {
- // not needed since mid-game join is impossible here
- }
-
public void Gather()
{
var maskZoneController = QSBWorldSync.GetUnityObject();
@@ -29,4 +23,4 @@ internal class QSBQuantumInstrument : WorldObject
AttachedObject.Gather();
}
-}
\ No newline at end of file
+}
diff --git a/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs b/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs
index 7974a771..cdce3755 100644
--- a/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs
+++ b/QSB/EyeOfTheUniverse/Tomb/EyeTombWatcher.cs
@@ -7,35 +7,30 @@ namespace QSB.EyeOfTheUniverse.Tomb;
internal class EyeTombWatcher : MonoBehaviour
{
- private EyeTombController tomb;
- private bool _observedGrave;
+ private EyeTombController _tomb;
- private void Start()
+ private void Awake()
{
- tomb = GetComponent();
- tomb._graveObserveTrigger.OnGainFocus += OnObserveGrave;
+ _tomb = GetComponent();
+ _tomb._graveObserveTrigger.OnGainFocus += OnObserveGrave;
+ enabled = false;
}
- private void OnDestroy()
- => tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave;
+ private void OnDestroy() =>
+ _tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave;
private void OnObserveGrave()
{
- _observedGrave = true;
- tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave;
+ _tomb._graveObserveTrigger.OnGainFocus -= OnObserveGrave;
+ enabled = true;
}
-
+
private void FixedUpdate()
{
- if (!_observedGrave)
- {
- return;
- }
-
var canShowStage = true;
foreach (var player in QSBPlayerManager.PlayerList)
{
- var playerToStage = tomb._stageRoot.transform.position - player.Body.transform.position;
+ var playerToStage = _tomb._stageRoot.transform.position - player.Body.transform.position;
var playerLookDirection = player.Body.transform.forward;
var angle = Vector3.Angle(playerLookDirection, playerToStage);
if (angle < 70)
@@ -46,9 +41,9 @@ internal class EyeTombWatcher : MonoBehaviour
if (canShowStage)
{
- tomb._stageRoot.SetActive(true);
+ _tomb._stageRoot.SetActive(true);
new ShowStageMessage().Send();
- enabled = false;
+ Destroy(this);
}
}
}
diff --git a/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs b/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs
index ba1e0e6b..16f404c9 100644
--- a/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs
+++ b/QSB/EyeOfTheUniverse/Tomb/Messages/ShowStageMessage.cs
@@ -1,6 +1,6 @@
using QSB.Messaging;
-using QSB.Utility;
using QSB.WorldSync;
+using UnityEngine;
namespace QSB.EyeOfTheUniverse.Tomb.Messages;
@@ -10,5 +10,6 @@ internal class ShowStageMessage : QSBMessage
{
var tomb = QSBWorldSync.GetUnityObject();
tomb._stageRoot.SetActive(true);
+ Object.Destroy(tomb.GetComponent());
}
}
diff --git a/QSB/EyeOfTheUniverse/Tomb/TombManager.cs b/QSB/EyeOfTheUniverse/Tomb/TombManager.cs
index 4e31fd11..bfcb73a5 100644
--- a/QSB/EyeOfTheUniverse/Tomb/TombManager.cs
+++ b/QSB/EyeOfTheUniverse/Tomb/TombManager.cs
@@ -11,11 +11,8 @@ internal class TombManager : WorldObjectManager
public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct)
{
- if (QSBCore.IsHost)
- {
- // sike!! no worldobjects here
- var tomb = QSBWorldSync.GetUnityObject();
- tomb.gameObject.AddComponent();
- }
+ // sike!! no worldobjects here
+ var tomb = QSBWorldSync.GetUnityObject();
+ tomb.gameObject.AddComponent();
}
}
diff --git a/QSB/GameVendor.cs b/QSB/GameVendor.cs
new file mode 100644
index 00000000..d13fef44
--- /dev/null
+++ b/QSB/GameVendor.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace QSB;
+
+[Flags]
+public enum GameVendor
+{
+ None = 0,
+ Epic = 1,
+ Steam = 2,
+ Gamepass = 4
+}
+
diff --git a/QSB/ItemSync/ItemState.cs b/QSB/ItemSync/ItemState.cs
new file mode 100644
index 00000000..db974f6a
--- /dev/null
+++ b/QSB/ItemSync/ItemState.cs
@@ -0,0 +1,41 @@
+using QSB.Player;
+using UnityEngine;
+
+namespace QSB.ItemSync;
+
+///
+/// used for initial state sync.
+/// we have to store this separately because it's not saved in the item itself, unfortunately.
+///
+public class ItemState
+{
+ ///
+ /// if this is false, there's no need to sync initial state for this item
+ ///
+ public bool HasBeenInteractedWith;
+
+ public ItemStateType State;
+
+ // on ground
+ public Transform Parent;
+ public Vector3 LocalPosition;
+ public Vector3 WorldPosition => Parent.TransformPoint(LocalPosition);
+ public Vector3 LocalNormal;
+ public Vector3 WorldNormal => Parent.TransformDirection(LocalNormal);
+ public Sector Sector;
+ public IItemDropTarget CustomDropTarget;
+ public OWRigidbody Rigidbody;
+
+ // held
+ public PlayerInfo HoldingPlayer;
+
+ // socketed
+ public OWItemSocket Socket;
+}
+
+public enum ItemStateType
+{
+ OnGround,
+ Held,
+ Socketed
+}
diff --git a/QSB/ItemSync/Messages/DropItemMessage.cs b/QSB/ItemSync/Messages/DropItemMessage.cs
index 5577f18f..bf550fef 100644
--- a/QSB/ItemSync/Messages/DropItemMessage.cs
+++ b/QSB/ItemSync/Messages/DropItemMessage.cs
@@ -58,6 +58,14 @@ internal class DropItemMessage : QSBWorldObjectMessage().AttachedObject : null;
WorldObject.DropItem(worldPos, worldNormal, parent, sector, customDropTarget);
+ WorldObject.ItemState.HasBeenInteractedWith = true;
+ WorldObject.ItemState.State = ItemStateType.OnGround;
+ WorldObject.ItemState.LocalPosition = Data.localPosition;
+ WorldObject.ItemState.Parent = parent;
+ WorldObject.ItemState.LocalNormal = Data.localNormal;
+ WorldObject.ItemState.Sector = sector;
+ WorldObject.ItemState.CustomDropTarget = customDropTarget;
+ WorldObject.ItemState.Rigidbody = parent.GetComponent();
var player = QSBPlayerManager.GetPlayer(From);
player.HeldItem = null;
diff --git a/QSB/ItemSync/Messages/MoveToCarryMessage.cs b/QSB/ItemSync/Messages/MoveToCarryMessage.cs
index 933b2196..225d1df4 100644
--- a/QSB/ItemSync/Messages/MoveToCarryMessage.cs
+++ b/QSB/ItemSync/Messages/MoveToCarryMessage.cs
@@ -5,13 +5,15 @@ using QSB.Utility;
namespace QSB.ItemSync.Messages;
-internal class MoveToCarryMessage : QSBWorldObjectMessage
+internal class MoveToCarryMessage : QSBWorldObjectMessage
{
+ public MoveToCarryMessage(uint playerHolding) : base(playerHolding) { }
+
public override void OnReceiveRemote()
{
WorldObject.StoreLocation();
- var player = QSBPlayerManager.GetPlayer(From);
+ var player = QSBPlayerManager.GetPlayer(Data);
var itemType = WorldObject.GetItemType();
player.HeldItem = WorldObject;
@@ -30,6 +32,9 @@ internal class MoveToCarryMessage : QSBWorldObjectMessage
};
WorldObject.PickUpItem(itemSocket);
+ WorldObject.ItemState.HasBeenInteractedWith = true;
+ WorldObject.ItemState.State = ItemStateType.Held;
+ WorldObject.ItemState.HoldingPlayer = player;
switch (itemType)
{
diff --git a/QSB/ItemSync/Messages/SocketItemMessage.cs b/QSB/ItemSync/Messages/SocketItemMessage.cs
index 73a9abf0..3b824ee9 100644
--- a/QSB/ItemSync/Messages/SocketItemMessage.cs
+++ b/QSB/ItemSync/Messages/SocketItemMessage.cs
@@ -27,6 +27,9 @@ internal class SocketItemMessage : QSBMessage<(SocketMessageType Type, int Socke
var qsbItem = Data.ItemId.GetWorldObject();
qsbItemSocket.PlaceIntoSocket(qsbItem);
+ qsbItem.ItemState.HasBeenInteractedWith = true;
+ qsbItem.ItemState.State = ItemStateType.Socketed;
+ qsbItem.ItemState.Socket = qsbItemSocket.AttachedObject;
var player = QSBPlayerManager.GetPlayer(From);
player.HeldItem = null;
diff --git a/QSB/ItemSync/Patches/ItemToolPatches.cs b/QSB/ItemSync/Patches/ItemToolPatches.cs
index 3ae88049..56acc639 100644
--- a/QSB/ItemSync/Patches/ItemToolPatches.cs
+++ b/QSB/ItemSync/Patches/ItemToolPatches.cs
@@ -20,7 +20,10 @@ internal class ItemToolPatches : QSBPatch
{
var qsbItem = item.GetWorldObject();
QSBPlayerManager.LocalPlayer.HeldItem = qsbItem;
- qsbItem.SendMessage(new MoveToCarryMessage());
+ qsbItem.ItemState.HasBeenInteractedWith = true;
+ qsbItem.ItemState.State = ItemStateType.Held;
+ qsbItem.ItemState.HoldingPlayer = QSBPlayerManager.LocalPlayer;
+ qsbItem.SendMessage(new MoveToCarryMessage(QSBPlayerManager.LocalPlayer.PlayerId));
}
[HarmonyPrefix]
@@ -29,6 +32,9 @@ internal class ItemToolPatches : QSBPatch
{
var item = __instance._heldItem;
QSBPlayerManager.LocalPlayer.HeldItem = null;
+ var qsbItem = item.GetWorldObject();
+ qsbItem.ItemState.State = ItemStateType.Socketed;
+ qsbItem.ItemState.Socket = socket;
new SocketItemMessage(SocketMessageType.Socket, socket, item).Send();
}
@@ -38,6 +44,7 @@ internal class ItemToolPatches : QSBPatch
{
var item = socket.GetSocketedItem();
var qsbItem = item.GetWorldObject();
+ qsbItem.ItemState.HasBeenInteractedWith = true;
QSBPlayerManager.LocalPlayer.HeldItem = qsbItem;
new SocketItemMessage(SocketMessageType.StartUnsocket, socket, item).Send();
}
@@ -92,6 +99,14 @@ internal class ItemToolPatches : QSBPatch
qsbItem.SendMessage(new DropItemMessage(hit.point, hit.normal, parent, sector, customDropTarget, targetRigidbody));
+ qsbItem.ItemState.State = ItemStateType.OnGround;
+ qsbItem.ItemState.Parent = parent;
+ qsbItem.ItemState.LocalPosition = parent.InverseTransformPoint(hit.point);
+ qsbItem.ItemState.LocalNormal = parent.InverseTransformDirection(hit.normal);
+ qsbItem.ItemState.Sector = sector;
+ qsbItem.ItemState.CustomDropTarget = customDropTarget;
+ qsbItem.ItemState.Rigidbody = targetRigidbody;
+
return false;
}
}
diff --git a/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs b/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs
index d43c932f..7162b010 100644
--- a/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs
+++ b/QSB/ItemSync/WorldObjects/Items/IQSBItem.cs
@@ -5,6 +5,8 @@ namespace QSB.ItemSync.WorldObjects.Items;
public interface IQSBItem : IWorldObject
{
+ ItemState ItemState { get; }
+
ItemType GetItemType();
void PickUpItem(Transform itemSocket);
void DropItem(Vector3 position, Vector3 normal, Transform parent, Sector sector, IItemDropTarget customDropTarget);
diff --git a/QSB/ItemSync/WorldObjects/Items/QSBItem.cs b/QSB/ItemSync/WorldObjects/Items/QSBItem.cs
index 96fe48e3..45503a03 100644
--- a/QSB/ItemSync/WorldObjects/Items/QSBItem.cs
+++ b/QSB/ItemSync/WorldObjects/Items/QSBItem.cs
@@ -1,5 +1,7 @@
using Cysharp.Threading.Tasks;
+using QSB.ItemSync.Messages;
using QSB.ItemSync.WorldObjects.Sockets;
+using QSB.Messaging;
using QSB.Patches;
using QSB.Player;
using QSB.SectorSync.WorldObjects;
@@ -12,12 +14,24 @@ namespace QSB.ItemSync.WorldObjects.Items;
public class QSBItem : WorldObject, IQSBItem
where T : OWItem
{
+ public ItemState ItemState { get; } = new();
+
private Transform _lastParent;
private Vector3 _lastPosition;
private Quaternion _lastRotation;
private QSBSector _lastSector;
private QSBItemSocket _lastSocket;
+ public override string ReturnLabel()
+ {
+ return $"{ToString()}" +
+ $"\r\nState:{ItemState.State}" +
+ $"\r\nParent:{ItemState.Parent?.name}" +
+ $"\r\nLocalPosition:{ItemState.LocalPosition}" +
+ $"\r\nLocalNormal:{ItemState.LocalNormal}" +
+ $"\r\nHoldingPlayer:{ItemState.HoldingPlayer?.PlayerId}";
+ }
+
public override async UniTask Init(CancellationToken ct)
{
await UniTask.WaitUntil(() => QSBWorldSync.AllObjectsAdded, cancellationToken: ct);
@@ -61,6 +75,7 @@ public class QSBItem : WorldObject, IQSBItem
}
else
{
+ // TODO at some point we should probably call the proper drop item code to account for funny overrides
AttachedObject.transform.parent = _lastParent;
AttachedObject.transform.localPosition = _lastPosition;
AttachedObject.transform.localRotation = _lastRotation;
@@ -72,7 +87,30 @@ public class QSBItem : WorldObject, IQSBItem
public override void SendInitialState(uint to)
{
- // todo SendInitialState
+ if (!ItemState.HasBeenInteractedWith)
+ {
+ return;
+ }
+
+ switch (ItemState.State)
+ {
+ case ItemStateType.Held:
+ ((IQSBItem)this).SendMessage(new MoveToCarryMessage(ItemState.HoldingPlayer.PlayerId));
+ break;
+ case ItemStateType.Socketed:
+ new SocketItemMessage(SocketMessageType.Socket, ItemState.Socket, AttachedObject).Send();
+ break;
+ case ItemStateType.OnGround:
+ ((IQSBItem)this).SendMessage(
+ new DropItemMessage(
+ ItemState.WorldPosition,
+ ItemState.WorldNormal,
+ ItemState.Parent,
+ ItemState.Sector,
+ ItemState.CustomDropTarget,
+ ItemState.Rigidbody));
+ break;
+ }
}
public ItemType GetItemType() => AttachedObject.GetItemType();
diff --git a/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs b/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs
index 965212ef..10ffd92a 100644
--- a/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs
+++ b/QSB/ItemSync/WorldObjects/Items/QSBVisionTorchItem.cs
@@ -1,3 +1,6 @@
namespace QSB.ItemSync.WorldObjects.Items;
+///
+/// TODO: SYNC THIS SHIT LMAOOOOOO
+///
internal class QSBVisionTorchItem : QSBItem { }
\ No newline at end of file
diff --git a/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs b/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs
index 4e66cf17..de5985c0 100644
--- a/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs
+++ b/QSB/ItemSync/WorldObjects/QSBOtherDropTarget.cs
@@ -17,9 +17,7 @@ public class QSBOtherDropTarget : WorldObject, IQSBDropTarget
{
if (AttachedObject is not IItemDropTarget)
{
- throw new ArgumentException("QSBDropTarget.AttachedObject is not an IItemDropTarget!");
+ throw new ArgumentException("QSBOtherDropTarget.AttachedObject is not an IItemDropTarget!");
}
}
-
- public override void SendInitialState(uint to) { }
}
diff --git a/QSB/Localization/Translation.cs b/QSB/Localization/Translation.cs
index 2b5df85d..72addba5 100644
--- a/QSB/Localization/Translation.cs
+++ b/QSB/Localization/Translation.cs
@@ -13,9 +13,12 @@ public class Translation
public string ProductUserID;
public string Connect;
public string Cancel;
+ public string HostExistingOrNewOrCopy;
+ public string HostNewOrCopy;
public string HostExistingOrNew;
public string ExistingSave;
public string NewSave;
+ public string CopySave;
public string DisconnectAreYouSure;
public string Yes;
public string No;
@@ -41,5 +44,10 @@ public class Translation
public string TimeSyncWaitForAllToReady;
public string TimeSyncWaitForAllToDie;
public string GalaxyMapEveryoneNotPresent;
+ public string YouAreDead;
+ public string WaitingForRespawn;
+ public string WaitingForAllToDie;
+ public string AttachToShip;
+ public string DetachFromShip;
public Dictionary DeathMessages;
}
diff --git a/QSB/Menus/FourChoicePopupMenu.cs b/QSB/Menus/FourChoicePopupMenu.cs
new file mode 100644
index 00000000..5a7544cd
--- /dev/null
+++ b/QSB/Menus/FourChoicePopupMenu.cs
@@ -0,0 +1,381 @@
+using System;
+using UnityEngine;
+using UnityEngine.EventSystems;
+using UnityEngine.UI;
+
+namespace QSB.Menus;
+
+[RequireComponent(typeof(Canvas))]
+public class FourChoicePopupMenu : Menu
+{
+ public Text _labelText;
+ public SubmitAction _cancelAction;
+ public SubmitAction _ok1Action;
+ public SubmitAction _ok2Action;
+ public SubmitAction _ok3Action;
+ public ButtonWithHotkeyImageElement _cancelButton;
+ public ButtonWithHotkeyImageElement _confirmButton1;
+ public ButtonWithHotkeyImageElement _confirmButton2;
+ public ButtonWithHotkeyImageElement _confirmButton3;
+ public Canvas _rootCanvas;
+
+ protected Canvas _popupCanvas;
+ protected GameObject _blocker;
+ protected bool _closeMenuOnOk = true;
+ protected IInputCommands _ok1Command;
+ protected IInputCommands _ok2Command;
+ protected IInputCommands _ok3Command;
+ protected IInputCommands _cancelCommand;
+ protected bool _usingGamepad;
+
+ public event PopupConfirmEvent OnPopupConfirm1;
+ public event PopupConfirmEvent OnPopupConfirm2;
+ public event PopupConfirmEvent OnPopupConfirm3;
+ public event PopupValidateEvent OnPopupValidate;
+ public event PopupCancelEvent OnPopupCancel;
+
+ public override Selectable GetSelectOnActivate()
+ {
+ _usingGamepad = OWInput.UsingGamepad();
+ return _usingGamepad ? null : _selectOnActivate;
+ }
+
+ public virtual void SetUpPopup(
+ string message,
+ IInputCommands ok1Command,
+ IInputCommands ok2Command,
+ IInputCommands ok3Command,
+ IInputCommands cancelCommand,
+ ScreenPrompt ok1Prompt,
+ ScreenPrompt ok2Prompt,
+ ScreenPrompt ok3Prompt,
+ ScreenPrompt cancelPrompt,
+ bool closeMenuOnOk = true,
+ bool setCancelButtonActive = true)
+ {
+ _labelText.text = message;
+ SetUpPopupCommands(ok1Command, ok2Command, ok3Command, cancelCommand, ok1Prompt, ok2Prompt, ok3Prompt, cancelPrompt);
+ if (!(_cancelAction != null))
+ {
+ Debug.LogWarning("PopupMenu.SetUpPopup Cancel button not set!");
+ return;
+ }
+
+ _cancelAction.gameObject.SetActive(setCancelButtonActive);
+ if (setCancelButtonActive)
+ {
+ _selectOnActivate = _cancelAction.GetRequiredComponent();
+ return;
+ }
+
+ _selectOnActivate = _ok1Action.GetRequiredComponent();
+ }
+
+ public virtual void SetUpPopupCommands(
+ IInputCommands ok1Command,
+ IInputCommands ok2Command,
+ IInputCommands ok3Command,
+ IInputCommands cancelCommand,
+ ScreenPrompt ok1Prompt,
+ ScreenPrompt ok2Prompt,
+ ScreenPrompt ok3Prompt,
+ ScreenPrompt cancelPrompt)
+ {
+ _ok1Command = ok1Command;
+ _ok2Command = ok2Command;
+ _ok3Command = ok3Command;
+ _cancelCommand = cancelCommand;
+ _confirmButton1.SetPrompt(ok1Prompt, InputMode.Menu);
+ _confirmButton2.SetPrompt(ok2Prompt, InputMode.Menu);
+ _confirmButton3.SetPrompt(ok3Prompt, InputMode.Menu);
+ _cancelButton.SetPrompt(cancelPrompt, InputMode.Menu);
+ }
+
+ public virtual void ResetPopup()
+ {
+ _labelText.text = "";
+ _ok1Command = null;
+ _ok2Command = null;
+ _ok3Command = null;
+ _cancelCommand = null;
+ _cancelButton.SetPrompt(null, InputMode.Menu);
+ _confirmButton1.SetPrompt(null, InputMode.Menu);
+ _confirmButton2.SetPrompt(null, InputMode.Menu);
+ _confirmButton3.SetPrompt(null, InputMode.Menu);
+ _selectOnActivate = null;
+ }
+
+ public virtual void CloseMenuOnOk(bool value)
+ {
+ _closeMenuOnOk = value;
+ }
+
+ public virtual bool EventsHaveListeners()
+ {
+ return OnPopupCancel != null
+ || OnPopupConfirm1 != null
+ || OnPopupConfirm2 != null
+ || OnPopupConfirm3 != null;
+ }
+
+ public override void InitializeMenu()
+ {
+ base.InitializeMenu();
+ if (_cancelAction != null)
+ {
+ _cancelAction.OnSubmitAction += InvokeCancel;
+ }
+
+ _ok1Action.OnSubmitAction += InvokeOk1;
+ _ok2Action.OnSubmitAction += InvokeOk2;
+ _ok3Action.OnSubmitAction += InvokeOk3;
+ _popupCanvas = gameObject.GetAddComponent
-
+
diff --git a/QSB/QSBCore.cs b/QSB/QSBCore.cs
index 9dd3efe5..b8b9429f 100644
--- a/QSB/QSBCore.cs
+++ b/QSB/QSBCore.cs
@@ -5,7 +5,9 @@ using OWML.ModHelper;
using QSB.Localization;
using QSB.Menus;
using QSB.Patches;
+using QSB.Player;
using QSB.QuantumSync;
+using QSB.SaveSync;
using QSB.Utility;
using QSB.WorldSync;
using System;
@@ -48,13 +50,18 @@ public class QSBCore : ModBehaviour
public static AssetBundle ConversationAssetBundle { get; private set; }
public static AssetBundle DebugAssetBundle { get; private set; }
public static bool IsHost => NetworkServer.active;
- public static bool IsInMultiplayer => QSBNetworkManager.singleton.isNetworkActive;
+ public static bool IsInMultiplayer;
public static string QSBVersion => Helper.Manifest.Version;
public static string GameVersion =>
// ignore the last patch numbers like the title screen does
Application.version.Split('.').Take(3).Join(delimiter: ".");
public static bool DLCInstalled => EntitlementsManager.IsDlcOwned() == EntitlementsManager.AsyncOwnershipStatus.Owned;
public static bool IncompatibleModsAllowed { get; private set; }
+ public static GameVendor GameVendor { get; private set; } = GameVendor.None;
+ public static bool IsStandalone => GameVendor is GameVendor.Epic or GameVendor.Steam;
+ public static IProfileManager ProfileManager => IsStandalone
+ ? QSBStandaloneProfileManager.SharedInstance
+ : QSBMSStoreProfileManager.SharedInstance;
public static IMenuAPI MenuApi { get; private set; }
public static DebugSettings DebugSettings { get; private set; } = new();
public static Storage Storage { get; private set; } = new();
@@ -67,9 +74,42 @@ public class QSBCore : ModBehaviour
// incompatible mods
"Raicuparta.NomaiVR",
"xen.NewHorizons",
- "Vesper.AutoResume"
+ "Vesper.AutoResume",
+ "Vesper.OuterWildsMMO",
+ "_nebula.StopTime",
+ "Leadpogrommer.PeacefulGhosts",
+ "PacificEngine.OW_Randomizer",
+ "xen.DayDream"
};
+ private static void DetermineGameVendor()
+ {
+ var gameAssemblyTypes = typeof(AstroObject).Assembly.GetTypes();
+ var isEpic = gameAssemblyTypes.Any(x => x.Name == "EpicEntitlementRetriever");
+ var isSteam = gameAssemblyTypes.Any(x => x.Name == "SteamEntitlementRetriever");
+ var isUWP = gameAssemblyTypes.Any(x => x.Name == "MSStoreEntitlementRetriever");
+
+ if (isEpic && !isSteam && !isUWP)
+ {
+ GameVendor = GameVendor.Epic;
+ }
+ else if (!isEpic && isSteam && !isUWP)
+ {
+ GameVendor = GameVendor.Steam;
+ }
+ else if (!isEpic && !isSteam && isUWP)
+ {
+ GameVendor = GameVendor.Gamepass;
+ }
+ else
+ {
+ // ???
+ DebugLog.ToConsole($"FATAL - Could not determine game vendor.", MessageType.Fatal);
+ }
+
+ DebugLog.DebugWrite($"Determined game vendor as {GameVendor}", MessageType.Info);
+ }
+
public void Awake()
{
EpicRerouter.ModSide.Interop.Go();
@@ -77,6 +117,11 @@ public class QSBCore : ModBehaviour
// no, we cant localize this - languages are loaded after the splash screen
UIHelper.ReplaceUI(UITextType.PleaseUseController,
"Quantum Space Buddies is best experienced with friends...");
+
+ DetermineGameVendor();
+
+ QSBPatchManager.Init();
+ QSBPatchManager.DoPatchType(QSBPatchTypes.OnModStart);
}
public void Start()
@@ -133,7 +178,6 @@ public class QSBCore : ModBehaviour
return;
}
- QSBPatchManager.Init();
DeterministicManager.Init();
QSBLocalization.Init();
@@ -145,7 +189,7 @@ public class QSBCore : ModBehaviour
QSBPatchManager.OnPatchType += OnPatchType;
QSBPatchManager.OnUnpatchType += OnUnpatchType;
- QSBPatchManager.DoPatchType(QSBPatchTypes.OnModStart);
+ StartCoroutine(QSBPlayerManager.ValidatePlayers());
}
private static void OnPatchType(QSBPatchTypes type)
diff --git a/QSB/QSBNetworkManager.cs b/QSB/QSBNetworkManager.cs
index acf2cbb2..78fb5359 100644
--- a/QSB/QSBNetworkManager.cs
+++ b/QSB/QSBNetworkManager.cs
@@ -21,6 +21,7 @@ using QSB.Patches;
using QSB.Player;
using QSB.Player.Messages;
using QSB.Player.TransformSync;
+using QSB.SaveSync;
using QSB.ShipSync;
using QSB.ShipSync.TransformSync;
using QSB.Syncs.Occasional;
@@ -101,7 +102,7 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
base.Awake();
InitPlayerName();
- StandaloneProfileManager.SharedInstance.OnProfileSignInComplete += _ => InitPlayerName();
+ QSBCore.ProfileManager.OnProfileSignInComplete += _ => InitPlayerName();
playerPrefab = QSBCore.NetworkAssetBundle.LoadAsset("Assets/Prefabs/NETWORK_Player_Body.prefab");
playerPrefab.GetRequiredComponent().SetValue("m_AssetId", 1.ToGuid().ToString("N"));
@@ -156,15 +157,22 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
{
try
{
- var titleScreenManager = FindObjectOfType();
- var profileManager = titleScreenManager._profileManager;
- if (profileManager.GetType().Name == "MSStoreProfileManager")
+ if (!QSBCore.IsStandalone)
{
- PlayerName = (string)profileManager.GetType().GetProperty("userDisplayName").GetValue(profileManager);
+ PlayerName = QSBMSStoreProfileManager.SharedInstance.userDisplayName;
}
else
{
- PlayerName = StandaloneProfileManager.SharedInstance.currentProfile.profileName;
+ var currentProfile = QSBStandaloneProfileManager.SharedInstance.currentProfile;
+
+ if (currentProfile == null)
+ {
+ // probably havent created a profile yet
+ Delay.RunWhen(() => QSBStandaloneProfileManager.SharedInstance.currentProfile != null, () => InitPlayerName());
+ return;
+ }
+
+ PlayerName = currentProfile.profileName;
}
}
catch (Exception ex)
diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs
index f4a3b0f0..9f9011a2 100644
--- a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs
+++ b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs
@@ -12,6 +12,11 @@ using UnityEngine;
namespace QSB.QuantumSync.WorldObjects;
+///
+/// TODO: just use OnSectorOccupantsUpdated instead of this shape bullshit
+///
+/// TODO: make it part of the ad-hoc owner interface
+///
internal abstract class QSBQuantumObject : WorldObject, IQSBQuantumObject
where T : QuantumObject
{
diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs
index 8529cdbe..5c880afc 100644
--- a/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs
+++ b/QSB/QuantumSync/WorldObjects/QSBQuantumSocket.cs
@@ -5,6 +5,4 @@ namespace QSB.QuantumSync.WorldObjects;
internal class QSBQuantumSocket : WorldObject
{
public override bool ShouldDisplayDebug() => false;
-
- public override void SendInitialState(uint to) { }
}
\ No newline at end of file
diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs
index f7ca85e5..241f9ad1 100644
--- a/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs
+++ b/QSB/QuantumSync/WorldObjects/QSBQuantumState.cs
@@ -13,6 +13,4 @@ internal class QSBQuantumState : WorldObject
}
public override bool ShouldDisplayDebug() => false;
-
- public override void SendInitialState(uint to) { }
}
\ No newline at end of file
diff --git a/QSB/RespawnSync/RespawnHUDMarker.cs b/QSB/RespawnSync/RespawnHUDMarker.cs
index a260eef9..f9653a5a 100644
--- a/QSB/RespawnSync/RespawnHUDMarker.cs
+++ b/QSB/RespawnSync/RespawnHUDMarker.cs
@@ -29,7 +29,7 @@ public class RespawnHUDMarker : HUDDistanceMarker
{
var isVisible = _canvasMarker.IsVisible();
var shouldBeVisible = RespawnManager.Instance.RespawnNeeded
- && !ShipManager.Instance.ShipCockpitUI._shipDamageCtrlr.IsDestroyed();
+ && !ShipManager.Instance.IsShipWrecked;
if (shouldBeVisible != isVisible)
{
diff --git a/QSB/RespawnSync/RespawnManager.cs b/QSB/RespawnSync/RespawnManager.cs
index ab2aa021..82a84a7d 100644
--- a/QSB/RespawnSync/RespawnManager.cs
+++ b/QSB/RespawnSync/RespawnManager.cs
@@ -67,6 +67,7 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart
return;
}
+ QSBPatchManager.DoUnpatchType(QSBPatchTypes.RespawnTime);
QSBPlayerManager.ShowAllPlayers();
QSBPlayerManager.LocalPlayer.UpdateStatesFromObjects();
QSBPlayerManager.PlayerList.ForEach(x => x.IsDead = false);
@@ -138,15 +139,16 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart
public void OnPlayerDeath(PlayerInfo player)
{
+ player.IsDead = true;
+
if (_playersPendingRespawn.Contains(player))
{
DebugLog.ToConsole($"Warning - Received death message for player who is already in _playersPendingRespawn!", OWML.Common.MessageType.Warning);
- return;
}
-
- player.IsDead = true;
-
- _playersPendingRespawn.Add(player);
+ else
+ {
+ _playersPendingRespawn.Add(player);
+ }
var deadPlayersCount = QSBPlayerManager.PlayerList.Count(x => x.IsDead);
@@ -161,15 +163,16 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart
public void OnPlayerRespawn(PlayerInfo player)
{
+ player.IsDead = false;
+
if (!_playersPendingRespawn.Contains(player))
{
DebugLog.ToConsole($"Warning - Received respawn message for player who is not in _playersPendingRespawn!", OWML.Common.MessageType.Warning);
- return;
}
-
- player.IsDead = false;
-
- _playersPendingRespawn.Remove(player);
+ else
+ {
+ _playersPendingRespawn.Remove(player);
+ }
player.SetVisible(true, 1);
}
@@ -177,6 +180,14 @@ internal class RespawnManager : MonoBehaviour, IAddComponentOnStart
public void RespawnSomePlayer()
{
var playerToRespawn = _playersPendingRespawn.First();
+
+ if (!playerToRespawn.IsDead)
+ {
+ DebugLog.ToConsole($"Warning - Tried to respawn player {playerToRespawn.PlayerId} who isn't dead!", OWML.Common.MessageType.Warning);
+ _playersPendingRespawn.Remove(playerToRespawn);
+ return;
+ }
+
new PlayerRespawnMessage(playerToRespawn.PlayerId).Send();
}
}
\ No newline at end of file
diff --git a/QSB/RespawnSync/ShipRecoveryPoint.cs b/QSB/RespawnSync/ShipRecoveryPoint.cs
index ce1ca50a..f42bc528 100644
--- a/QSB/RespawnSync/ShipRecoveryPoint.cs
+++ b/QSB/RespawnSync/ShipRecoveryPoint.cs
@@ -59,7 +59,7 @@ internal class ShipRecoveryPoint : MonoBehaviour
_playerResources = Locator.GetPlayerTransform().GetComponent();
}
- if (RespawnManager.Instance.RespawnNeeded && !ShipManager.Instance.ShipCockpitUI._shipDamageCtrlr.IsDestroyed())
+ if (RespawnManager.Instance.RespawnNeeded && !ShipManager.Instance.IsShipWrecked)
{
_interactVolume.EnableSingleInteraction(true, _respawnIndex);
_interactVolume.SetKeyCommandVisible(true, _respawnIndex);
@@ -127,7 +127,7 @@ internal class ShipRecoveryPoint : MonoBehaviour
}
else if (inputCommand == _interactVolume.GetInteractionAt(_respawnIndex).inputCommand)
{
- if (!RespawnManager.Instance.RespawnNeeded || ShipManager.Instance.ShipCockpitUI._shipDamageCtrlr.IsDestroyed())
+ if (!RespawnManager.Instance.RespawnNeeded || ShipManager.Instance.IsShipWrecked)
{
return;
}
diff --git a/QSB/SaveSync/Patches/InGameProfileMenuManagerPatches.cs b/QSB/SaveSync/Patches/InGameProfileMenuManagerPatches.cs
new file mode 100644
index 00000000..f57636c9
--- /dev/null
+++ b/QSB/SaveSync/Patches/InGameProfileMenuManagerPatches.cs
@@ -0,0 +1,33 @@
+using HarmonyLib;
+using QSB.Patches;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(InGameProfileMenuManager))]
+internal class InGameProfileMenuManagerPatches : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(InGameProfileMenuManager.InitializeOnAwake))]
+ public static bool InitializeOnAwake(InGameProfileMenuManager __instance)
+ {
+ if (!__instance._initialized)
+ {
+ TextTranslation.Get().OnLanguageChanged += __instance.UpdateLanguage;
+ __instance.UpdateLanguage();
+ __instance._profileManager = QSBCore.ProfileManager;
+ __instance._profileManager.OnProfileSignInComplete += __instance.OnProfileSignInComplete;
+ __instance._profileManager.OnProfileSignOutComplete += __instance.OnProfileSignOutComplete;
+ __instance._profileManager.OnProfileReadDone += __instance.OnProfileReadDone;
+ __instance._returnToGameSubmitAction.OnSubmitAction += __instance.OnResumeGameBtnSubmit;
+ __instance._returnToTitleSubmitAction.OnSubmitAction += __instance.OnTitleSubmitAction;
+ LoadManager.OnStartSceneLoad += __instance.OnStartSceneLoad;
+ LoadManager.OnCompleteSceneLoad += __instance.OnCompleteSceneLoad;
+ GlobalMessenger.AddListener("PlayerResurrection", new Callback(__instance.OnPlayerResurrection));
+ __instance._initialized = true;
+ }
+
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/Patches/PlayerDataPatches.cs b/QSB/SaveSync/Patches/PlayerDataPatches.cs
new file mode 100644
index 00000000..bdf88b09
--- /dev/null
+++ b/QSB/SaveSync/Patches/PlayerDataPatches.cs
@@ -0,0 +1,54 @@
+using HarmonyLib;
+using QSB.Patches;
+using UnityEngine;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(PlayerData))]
+internal class PlayerDataPatches : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerData.ResetGame))]
+ public static bool ResetGame()
+ {
+ PlayerData._currentGameSave = new GameSave();
+ QSBCore.ProfileManager.SaveGame(PlayerData._currentGameSave, null, null, null);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerData.SaveCurrentGame))]
+ public static bool SaveCurrentGame()
+ {
+ PlayerData._currentGameSave.version = Application.version;
+ QSBCore.ProfileManager.SaveGame(PlayerData._currentGameSave, null, null, null);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerData.SaveInputSettings))]
+ public static bool SaveInputSettings()
+ {
+ QSBCore.ProfileManager.SaveGame(null, null, null, PlayerData.inputJSON);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerData.SaveSettings))]
+ public static bool SaveSettings()
+ {
+ QSBCore.ProfileManager.SaveGame(null, PlayerData._settingsSave, PlayerData._graphicsSettings, PlayerData.inputJSON);
+ return false;
+ }
+
+ // this is actually still StandaloneProfileManager in the gamepass dll. game bug?
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(PlayerData.IsBusy))]
+ public static bool IsBusy(ref bool __result)
+ {
+ __result = QSBCore.ProfileManager.isBusyWithFileOps;
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/Patches/ProfileManagerUpdaterPatches.cs b/QSB/SaveSync/Patches/ProfileManagerUpdaterPatches.cs
new file mode 100644
index 00000000..da1d832b
--- /dev/null
+++ b/QSB/SaveSync/Patches/ProfileManagerUpdaterPatches.cs
@@ -0,0 +1,19 @@
+using HarmonyLib;
+using QSB.Patches;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(ProfileManagerUpdater))]
+internal class ProfileManagerUpdaterPatches : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileManagerUpdater.Start))]
+ public static bool Start(ProfileManagerUpdater __instance)
+ {
+ __instance._profileManager = QSBCore.ProfileManager;
+ __instance.enabled = true;
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/Patches/ProfileMenuManagerPatches.cs b/QSB/SaveSync/Patches/ProfileMenuManagerPatches.cs
new file mode 100644
index 00000000..73948444
--- /dev/null
+++ b/QSB/SaveSync/Patches/ProfileMenuManagerPatches.cs
@@ -0,0 +1,212 @@
+using HarmonyLib;
+using QSB.Patches;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(ProfileMenuManager))]
+internal class ProfileMenuManagerPatches : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+ public override GameVendor PatchVendor => GameVendor.Epic | GameVendor.Steam;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.OnCreateProfileConfirm))]
+ public static bool OnCreateProfileConfirm(ProfileMenuManager __instance)
+ {
+ __instance._inputPopupActivated = false;
+ var inputPopup = __instance._createProfileAction.GetInputPopup();
+ inputPopup.OnPopupValidate -= __instance.OnCreateProfileValidate;
+ inputPopup.OnInputPopupValidateChar -= __instance.OnValidateChar;
+ __instance._createProfileAction.OnSubmitAction -= __instance.OnCreateProfileConfirm;
+ QSBStandaloneProfileManager.SharedInstance.TryCreateProfile(__instance._createProfileAction.GetInputString());
+ inputPopup.CloseMenuOnOk(true);
+ __instance.PopulateProfiles();
+ __instance.SetCurrentProfileLabel();
+ inputPopup.EnableMenu(false);
+ if (__instance._firstTimeProfileCreation)
+ {
+ __instance._firstTimeProfileCreation = false;
+ __instance.UpdatePopupPrompts();
+ }
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.OnCreateProfileValidate))]
+ public static bool OnCreateProfileValidate(ProfileMenuManager __instance, ref bool __result)
+ {
+ var inputPopup = __instance._createProfileAction.GetInputPopup();
+ __result = QSBStandaloneProfileManager.SharedInstance.ValidateProfileName(inputPopup.GetInputText());
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.OnDeleteProfile))]
+ public static bool OnDeleteProfile(ProfileMenuManager __instance)
+ {
+ if (__instance._lastSelectedProfileAction != null)
+ {
+ __instance._deleteProfileConfirmPopup = null;
+ QSBStandaloneProfileManager.SharedInstance.DeleteProfile(__instance._lastSelectedProfileAction.GetLabelText());
+ __instance.PopulateProfiles();
+ __instance._lastSelectedProfileAction = null;
+ Locator.GetMenuInputModule().SelectOnNextUpdate(__instance._createProfileButton);
+ }
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.OnSwitchProfile))]
+ public static bool OnSwitchProfile(ProfileMenuManager __instance)
+ {
+ if (__instance._lastSelectedProfileAction != null)
+ {
+ __instance._switchProfileConfirmPopup = null;
+ if (QSBStandaloneProfileManager.SharedInstance.SwitchProfile(__instance._lastSelectedProfileAction.GetLabelText()))
+ {
+ __instance.PopulateProfiles();
+ __instance.SetCurrentProfileLabel();
+ __instance._lastSelectedProfileAction = null;
+ Locator.GetMenuInputModule().SelectOnNextUpdate(__instance._createProfileButton);
+ return false;
+ }
+
+ QSBStandaloneProfileManager.SharedInstance.OnBackupDataRestored += __instance.OnSwitchProfileDataRecoveryCompleted;
+ }
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.OnSwitchProfileDataRecoveryCompleted))]
+ public static bool OnSwitchProfileDataRecoveryCompleted(ProfileMenuManager __instance)
+ {
+ QSBStandaloneProfileManager.SharedInstance.OnBackupDataRestored -= __instance.OnSwitchProfileDataRecoveryCompleted;
+ __instance.PopulateProfiles();
+ __instance.SetCurrentProfileLabel();
+ __instance._lastSelectedProfileAction = null;
+ Locator.GetMenuInputModule().SelectOnNextUpdate(__instance._createProfileButton);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.OnValidateChar))]
+ public static bool OnValidateChar(ProfileMenuManager __instance, char c, ref bool __result)
+ {
+ __result = __instance._createProfileAction.GetInputPopup().GetInputText().Length < QSBStandaloneProfileManager.SharedInstance.profileNameCharacterLimit
+ && QSBStandaloneProfileManager.SharedInstance.IsValidCharacterForProfileName(c);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.PopulateProfiles))]
+ public static bool PopulateProfiles(ProfileMenuManager __instance)
+ {
+ if (__instance._listProfileElements == null)
+ {
+ __instance._listProfileElements = new List();
+ }
+ else
+ {
+ for (int i = 0; i < __instance._listProfileElements.Count; i++)
+ {
+ TwoButtonActionElement requiredComponent = __instance._listProfileElements[i].GetRequiredComponent();
+ __instance.ClearProfileElementListeners(requiredComponent);
+ Object.Destroy(__instance._listProfileElements[i]);
+ }
+ __instance._listProfileElements.Clear();
+ }
+
+ if (__instance._listProfileUIElementLookup == null)
+ {
+ __instance._listProfileUIElementLookup = new List();
+ }
+ else
+ {
+ __instance._listProfileUIElementLookup.Clear();
+ }
+
+ var array = QSBStandaloneProfileManager.SharedInstance.profiles.ToArray();
+ var profileName = QSBStandaloneProfileManager.SharedInstance.currentProfile.profileName;
+ var num = 0;
+ Selectable selectable = null;
+ for (int j = 0; j < array.Length; j++)
+ {
+ if (!(array[j].profileName == profileName))
+ {
+ GameObject gameObject = Object.Instantiate(__instance._profileItemTemplate);
+ gameObject.gameObject.SetActive(true);
+ gameObject.transform.SetParent(__instance._profileListRoot.transform);
+ gameObject.transform.localScale = new Vector3(1f, 1f, 1f);
+ Text[] componentsInChildren = gameObject.gameObject.GetComponentsInChildren();
+ for (int k = 0; k < componentsInChildren.Length; k++)
+ {
+ __instance._fontController.AddTextElement(componentsInChildren[k], true, true, false);
+ }
+
+ num++;
+ TwoButtonActionElement requiredComponent2 = gameObject.GetRequiredComponent();
+ Selectable requiredComponent3 = requiredComponent2.GetRequiredComponent();
+ __instance.SetUpProfileElementListeners(requiredComponent2);
+ requiredComponent2.SetLabelText(array[j].profileName);
+ Text component = requiredComponent2.GetButtonOne().GetComponent();
+ if (component != null)
+ {
+ __instance._fontController.AddTextElement(component, true, true, false);
+ }
+
+ component = requiredComponent2.GetButtonTwo().GetComponent();
+ if (component != null)
+ {
+ __instance._fontController.AddTextElement(component, true, true, false);
+ }
+
+ if (num == 1)
+ {
+ Navigation navigation = __instance._createProfileButton.navigation;
+ navigation.selectOnDown = gameObject.GetRequiredComponent();
+ __instance._createProfileButton.navigation = navigation;
+ Navigation navigation2 = requiredComponent3.navigation;
+ navigation2.selectOnUp = __instance._createProfileButton;
+ requiredComponent3.navigation = navigation2;
+ }
+ else
+ {
+ Navigation navigation3 = requiredComponent3.navigation;
+ Navigation navigation4 = selectable.navigation;
+ navigation3.selectOnUp = selectable;
+ navigation3.selectOnDown = null;
+ navigation4.selectOnDown = requiredComponent3;
+ requiredComponent3.navigation = navigation3;
+ selectable.navigation = navigation4;
+ }
+
+ __instance._listProfileElements.Add(gameObject);
+ selectable = requiredComponent3;
+ ProfileMenuManager.ProfileElementLookup profileElementLookup = new ProfileMenuManager.ProfileElementLookup();
+ profileElementLookup.profileName = array[j].profileName;
+ profileElementLookup.lastModifiedTime = array[j].lastModifiedTime;
+ profileElementLookup.confirmSwitchAction = requiredComponent2.GetSubmitActionOne() as SubmitActionConfirm;
+ profileElementLookup.confirmDeleteAction = requiredComponent2.GetSubmitActionTwo() as SubmitActionConfirm;
+ __instance._listProfileUIElementLookup.Add(profileElementLookup);
+ }
+ }
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(ProfileMenuManager.SetCurrentProfileLabel))]
+ public static bool SetCurrentProfileLabel(ProfileMenuManager __instance)
+ {
+ __instance._currenProfileLabel.text = UITextLibrary.GetString(UITextType.MenuProfile)
+ + " "
+ + QSBStandaloneProfileManager.SharedInstance.currentProfile.profileName;
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/Patches/TitleScreenManagerPatchesCommon.cs b/QSB/SaveSync/Patches/TitleScreenManagerPatchesCommon.cs
new file mode 100644
index 00000000..afdfbb8b
--- /dev/null
+++ b/QSB/SaveSync/Patches/TitleScreenManagerPatchesCommon.cs
@@ -0,0 +1,26 @@
+using HarmonyLib;
+using QSB.Patches;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(TitleScreenManager))]
+internal class TitleScreenManagerPatchesCommon : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.Awake))]
+ public static bool Awake(TitleScreenManager __instance)
+ {
+ __instance._profileManager = QSBCore.ProfileManager;
+ __instance._profileManager.PreInitialize();
+ LoadManager.OnStartSceneLoad += __instance.OnStartSceneLoad;
+ LoadManager.OnCompleteSceneLoad += __instance.OnCompleteSceneLoad;
+ MenuStackManager.SharedInstance.OnMenuPush += __instance.OnMenuPush;
+ MenuStackManager.SharedInstance.OnMenuPop += __instance.OnMenuPop;
+ __instance._resumeGameTextSetter = __instance._resumeGameObject.GetComponentInChildren();
+ __instance.InitializePopupPrompts();
+
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/Patches/TitleScreenManagerPatchesGamepass.cs b/QSB/SaveSync/Patches/TitleScreenManagerPatchesGamepass.cs
new file mode 100644
index 00000000..4f6fbe39
--- /dev/null
+++ b/QSB/SaveSync/Patches/TitleScreenManagerPatchesGamepass.cs
@@ -0,0 +1,63 @@
+using HarmonyLib;
+using OWML.Utils;
+using QSB.Patches;
+using UnityEngine.UI;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(TitleScreenManager))]
+internal class TitleScreenManagerPatchesGamepass : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+ public override GameVendor PatchVendor => GameVendor.Gamepass;
+
+ [HarmonyPrefix]
+ [HarmonyPatch("SetUserAccountDisplayInfo")]
+ public static bool SetUserAccountDisplayInfo(TitleScreenManager __instance)
+ {
+ var text = __instance.GetValue("_gamertagDisplay");
+ text.text = ""; // no idea why, mobius be like
+ text.text = QSBMSStoreProfileManager.SharedInstance.userDisplayName;
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.InitializeProfileManagerCallbacks))]
+ public static bool InitializeProfileManagerCallbacks(TitleScreenManager __instance)
+ {
+ QSBMSStoreProfileManager.SharedInstance.OnBrokenDataExists += __instance.OnBrokenDataExists;
+
+ __instance._profileManager.OnProfileSignInStart += __instance.OnProfileSignInStart;
+ __instance._profileManager.OnProfileSignInComplete += __instance.OnProfileSignInComplete;
+ __instance._profileManager.OnProfileSignOutStart += __instance.OnProfileSignOutStart;
+ __instance._profileManager.OnProfileSignOutComplete += __instance.OnProfileSignOutComplete;
+ __instance._profileManager.OnProfileReadDone += __instance.OnProfileManagerReadDone;
+ __instance._profileManager.Initialize();
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.OnDestroy))]
+ public static bool OnDestroy(TitleScreenManager __instance)
+ {
+ QSBMSStoreProfileManager.SharedInstance.OnBrokenDataExists -= __instance.OnBrokenDataExists;
+
+ __instance._profileManager.OnProfileSignInStart -= __instance.OnProfileSignInStart;
+ __instance._profileManager.OnProfileSignInComplete -= __instance.OnProfileSignInComplete;
+ __instance._profileManager.OnProfileSignOutStart -= __instance.OnProfileSignOutStart;
+ __instance._profileManager.OnProfileSignOutComplete -= __instance.OnProfileSignOutComplete;
+ __instance._profileManager.OnProfileReadDone -= __instance.OnProfileManagerReadDone;
+ LoadManager.OnStartSceneLoad -= __instance.OnStartSceneLoad;
+ LoadManager.OnCompleteSceneLoad -= __instance.OnCompleteSceneLoad;
+ TextTranslation.Get().OnLanguageChanged -= __instance.OnLanguageChanged;
+ __instance._newGameAction.OnSubmitAction -= __instance.OnNewGameSubmit;
+ __instance._newGameAction.OnPostSetupPopup -= __instance.OnNewGameSetupPopup;
+ __instance._resetGameAction.OnSubmitAction -= __instance.OnResetGameSubmit;
+ __instance._accountPickerSubmitAction.OnAccountPickerSubmitEvent -= __instance.OnAccountPickerSubmitEvent;
+ MenuStackManager.SharedInstance.OnMenuPush -= __instance.OnMenuPush;
+ MenuStackManager.SharedInstance.OnMenuPop -= __instance.OnMenuPop;
+
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/Patches/TitleScreenManagerPatchesStandalone.cs b/QSB/SaveSync/Patches/TitleScreenManagerPatchesStandalone.cs
new file mode 100644
index 00000000..3bae3f7a
--- /dev/null
+++ b/QSB/SaveSync/Patches/TitleScreenManagerPatchesStandalone.cs
@@ -0,0 +1,89 @@
+using HarmonyLib;
+using QSB.Patches;
+
+namespace QSB.SaveSync.Patches;
+
+[HarmonyPatch(typeof(TitleScreenManager))]
+internal class TitleScreenManagerPatchesStandalone : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnModStart;
+ public override GameVendor PatchVendor => GameVendor.Epic | GameVendor.Steam;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.OnBrokenDataExists))]
+ public static bool OnBrokenDataExists(TitleScreenManager __instance)
+ {
+ __instance._titleMenuRaycastBlocker.blocksRaycasts = false;
+ __instance._inputModule.EnableInputs();
+ __instance._waitingOnBrokenDataResponse = true;
+ var flag = QSBStandaloneProfileManager.SharedInstance.BackupExistsForBrokenData();
+ var text = UITextLibrary.GetString(UITextType.SaveRestore_CorruptedMsg);
+ if (flag)
+ {
+ text = text + " " + UITextLibrary.GetString(UITextType.SaveRestore_LoadPreviousMsg);
+ }
+
+ __instance._okCancelPopup.ResetPopup();
+ __instance._okCancelPopup.SetUpPopup(text, InputLibrary.confirm, InputLibrary.cancel, __instance._confirmActionPrompt, __instance._cancelActionPrompt, true, flag);
+ __instance._okCancelPopup.OnPopupConfirm += __instance.OnUserConfirmRestoreData;
+ __instance._okCancelPopup.OnPopupCancel += __instance.OnUserCancelRestoreData;
+ __instance._okCancelPopup.EnableMenu(true);
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.OnUserConfirmRestoreData))]
+ public static bool OnUserConfirmRestoreData(TitleScreenManager __instance)
+ {
+ __instance._waitingOnBrokenDataResponse = false;
+ QSBStandaloneProfileManager.SharedInstance.RestoreCurrentProfileBackup();
+ __instance.OnProfileManagerReadDone();
+ __instance._okCancelPopup.OnPopupConfirm -= __instance.OnUserConfirmRestoreData;
+ __instance._okCancelPopup.OnPopupCancel -= __instance.OnUserCancelRestoreData;
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.InitializeProfileManagerCallbacks))]
+ public static bool InitializeProfileManagerCallbacks(TitleScreenManager __instance)
+ {
+ QSBStandaloneProfileManager.SharedInstance.OnNoProfilesExist += __instance.OnNoStandaloneProfilesExist;
+ QSBStandaloneProfileManager.SharedInstance.OnUpdatePlayerProfiles += __instance.OnUpdatePlayerProfiles;
+ QSBStandaloneProfileManager.SharedInstance.OnBrokenDataExists += __instance.OnBrokenDataExists;
+
+ __instance._profileManager.OnProfileSignInStart += __instance.OnProfileSignInStart;
+ __instance._profileManager.OnProfileSignInComplete += __instance.OnProfileSignInComplete;
+ __instance._profileManager.OnProfileSignOutStart += __instance.OnProfileSignOutStart;
+ __instance._profileManager.OnProfileSignOutComplete += __instance.OnProfileSignOutComplete;
+ __instance._profileManager.OnProfileReadDone += __instance.OnProfileManagerReadDone;
+ __instance._profileManager.Initialize();
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(nameof(TitleScreenManager.OnDestroy))]
+ public static bool OnDestroy(TitleScreenManager __instance)
+ {
+ QSBStandaloneProfileManager.SharedInstance.OnNoProfilesExist -= __instance.OnNoStandaloneProfilesExist;
+ QSBStandaloneProfileManager.SharedInstance.OnUpdatePlayerProfiles -= __instance.OnUpdatePlayerProfiles;
+ QSBStandaloneProfileManager.SharedInstance.OnBrokenDataExists -= __instance.OnBrokenDataExists;
+
+ __instance._profileManager.OnProfileSignInStart -= __instance.OnProfileSignInStart;
+ __instance._profileManager.OnProfileSignInComplete -= __instance.OnProfileSignInComplete;
+ __instance._profileManager.OnProfileSignOutStart -= __instance.OnProfileSignOutStart;
+ __instance._profileManager.OnProfileSignOutComplete -= __instance.OnProfileSignOutComplete;
+ __instance._profileManager.OnProfileReadDone -= __instance.OnProfileManagerReadDone;
+ LoadManager.OnStartSceneLoad -= __instance.OnStartSceneLoad;
+ LoadManager.OnCompleteSceneLoad -= __instance.OnCompleteSceneLoad;
+ TextTranslation.Get().OnLanguageChanged -= __instance.OnLanguageChanged;
+ __instance._newGameAction.OnSubmitAction -= __instance.OnNewGameSubmit;
+ __instance._newGameAction.OnPostSetupPopup -= __instance.OnNewGameSetupPopup;
+ __instance._resetGameAction.OnSubmitAction -= __instance.OnResetGameSubmit;
+ __instance._accountPickerSubmitAction.OnAccountPickerSubmitEvent -= __instance.OnAccountPickerSubmitEvent;
+ MenuStackManager.SharedInstance.OnMenuPush -= __instance.OnMenuPush;
+ MenuStackManager.SharedInstance.OnMenuPop -= __instance.OnMenuPop;
+
+ return false;
+ }
+}
diff --git a/QSB/SaveSync/QSBMSStoreProfileManager.cs b/QSB/SaveSync/QSBMSStoreProfileManager.cs
new file mode 100644
index 00000000..3780b4d7
--- /dev/null
+++ b/QSB/SaveSync/QSBMSStoreProfileManager.cs
@@ -0,0 +1,429 @@
+using Microsoft.Xbox;
+using Newtonsoft.Json;
+using System;
+using System.IO;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+using UnityEngine;
+using UnityEngine.InputSystem;
+
+namespace QSB.SaveSync;
+
+internal class QSBMSStoreProfileManager : IProfileManager
+{
+ private const string OW_SAVE_CONTAINER_NAME = "GameSave";
+ private const string OW_GAME_SAVE_BLOB_NAME = "Outer Wilds Converted";
+ private const string OW_GAME_SETTINGS_BLOB_NAME = "PCGameSettings";
+
+ private static QSBMSStoreProfileManager _sharedInstance;
+ private QSBX1SaveData _saveData;
+ private const string c_containerName = "OuterWildsConnectedStorage";
+ private GameSave _pendingGameSave;
+ private SettingsSave _pendingSettingsSave;
+ private GraphicSettings _pendingGfxSettingsSave;
+ private string _pendingInputActionsSave = "";
+ private JsonSerializer _jsonSerializer;
+ private bool _initialized;
+ private int _fileOpsBusyLocks;
+ private bool _preInitialized;
+ private bool _isLoadingGameBlob;
+ private bool _isLoadingSettingsBlob;
+
+ public static QSBMSStoreProfileManager SharedInstance
+ {
+ get
+ {
+ if (_sharedInstance == null)
+ {
+ _sharedInstance = new QSBMSStoreProfileManager();
+ }
+
+ return _sharedInstance;
+ }
+ }
+
+ public GameSave currentProfileGameSave => _saveData.gameSave;
+ public GameSave currentProfileMultiplayerGameSave => _saveData.gameMultSave;
+ public SettingsSave currentProfileGameSettings => _saveData.settings;
+ public GraphicSettings currentProfileGraphicsSettings => _saveData.gfxSettings;
+ public string currentProfileInputJSON => _saveData.inputActionsJson;
+ public bool isInitialized { get; }
+ public bool isBusyWithFileOps => _fileOpsBusyLocks > 0;
+ public bool hasPendingSaveOperation => _pendingGameSave != null || _pendingSettingsSave != null || _pendingGfxSettingsSave != null || _pendingInputActionsSave != null;
+ public bool saveSystemAvailable { get; private set; }
+ public string userDisplayName => Gdk.Helpers.currentGamertag;
+
+ public delegate void BrokenDataExistsEvent();
+
+ public event BrokenDataExistsEvent OnBrokenDataExists;
+ public event ProfileDataSavedEvent OnProfileDataSaved;
+ public event ProfileReadDoneEvent OnProfileReadDone;
+ public event ProfileSignInCompleteEvent OnProfileSignInComplete;
+ public event ProfileSignInStartEvent OnProfileSignInStart;
+ public event ProfileSignOutCompleteEvent OnProfileSignOutComplete;
+ public event ProfileSignOutStartEvent OnProfileSignOutStart;
+
+ public void Initialize()
+ {
+ if (!_initialized)
+ {
+ Gdk.Helpers.SignIn();
+ SpinnerUI.Show();
+ Debug.Log("MSStoreProfileManager.Initialize");
+ Gdk.Helpers.OnGameSaveSucceeded += OnGameSaveComplete;
+ Gdk.Helpers.OnGameSaveFailed += OnGameSaveFailed;
+ Gdk.Helpers.OnGameSaveLoaded += OnGameSaveLoaded;
+ Gdk.Helpers.OnGameSaveLoadFailed += OnGameSaveLoadFailed;
+ Achievements.Init();
+ var serializationBinder = new VersionDeserializationBinder();
+ _jsonSerializer = new JsonSerializer
+ {
+ SerializationBinder = serializationBinder
+ };
+ _initialized = true;
+ return;
+ }
+
+ OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE);
+
+ var onProfileReadDone = OnProfileReadDone;
+ if (onProfileReadDone == null)
+ {
+ return;
+ }
+
+ onProfileReadDone();
+ }
+
+ public void PreInitialize()
+ {
+ if (_preInitialized)
+ {
+ return;
+ }
+
+ _fileOpsBusyLocks = 0;
+ _pendingGameSave = null;
+ _pendingSettingsSave = null;
+ _pendingGfxSettingsSave = null;
+ _pendingInputActionsSave = null;
+ _preInitialized = true;
+ }
+
+ public void InvokeProfileSignInComplete()
+ {
+ var onProfileSignInComplete = OnProfileSignInComplete;
+ if (onProfileSignInComplete == null)
+ {
+ return;
+ }
+
+ onProfileSignInComplete(ProfileManagerSignInResult.COMPLETE);
+ }
+
+ public void InvokeSaveSetupComplete()
+ {
+ saveSystemAvailable = true;
+ _isLoadingGameBlob = true;
+ _saveData = new QSBX1SaveData();
+ LoadGame(OW_GAME_SAVE_BLOB_NAME);
+ }
+
+ public void InitializeForEditor()
+ {
+ }
+
+ public void SaveGame(GameSave gameSave, SettingsSave settSave, GraphicSettings gfxSettings, string inputJSON)
+ {
+ Debug.Log("MSStoreProfileManager.SaveGame");
+ if (isBusyWithFileOps || LoadManager.IsBusy())
+ {
+ _pendingGameSave = gameSave;
+ _pendingSettingsSave = settSave;
+ _pendingGfxSettingsSave = gfxSettings;
+ _pendingInputActionsSave = inputJSON;
+ return;
+ }
+
+ var gameSaveData = new QSBX1SaveData();
+ var settingsSaveData = new QSBX1SaveData();
+ var saveGameSave = false;
+ if (gameSave != null)
+ {
+ saveGameSave = true;
+
+ if (QSBCore.IsInMultiplayer)
+ {
+ _saveData.gameMultSave = gameSave;
+ gameSaveData.gameMultSave = gameSave;
+ }
+ else
+ {
+ _saveData.gameSave = gameSave;
+ gameSaveData.gameSave = gameSave;
+ }
+ }
+
+ var saveGameSettings = false;
+ if (settSave != null)
+ {
+ saveGameSettings = true;
+ _saveData.settings = settSave;
+ settingsSaveData.settings = settSave;
+ }
+ else
+ {
+ settingsSaveData.settings = _saveData.settings;
+ }
+
+ if (gfxSettings != null)
+ {
+ saveGameSettings = true;
+ _saveData.gfxSettings = gfxSettings;
+ settingsSaveData.gfxSettings = gfxSettings;
+ }
+ else
+ {
+ settingsSaveData.gfxSettings = _saveData.gfxSettings;
+ }
+
+ if (!string.IsNullOrEmpty(inputJSON))
+ {
+ saveGameSettings = true;
+ _saveData.inputActionsJson = inputJSON;
+ settingsSaveData.inputActionsJson = inputJSON;
+ }
+ else if (!string.IsNullOrEmpty(_saveData.inputActionsJson))
+ {
+ settingsSaveData.inputActionsJson = _saveData.inputActionsJson;
+ }
+ else
+ {
+ settingsSaveData.inputActionsJson = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson();
+ }
+
+ if (saveGameSave)
+ {
+ WriteSaveToStorage(gameSaveData, OW_GAME_SAVE_BLOB_NAME);
+ }
+
+ if (saveGameSettings)
+ {
+ WriteSaveToStorage(settingsSaveData, OW_GAME_SETTINGS_BLOB_NAME);
+ }
+ }
+
+ private void LoadGame(string blobName)
+ {
+ _fileOpsBusyLocks++;
+ Gdk.Helpers.LoadSaveData(blobName);
+ }
+
+ private void WriteSaveToStorage(QSBX1SaveData saveData, string blobName)
+ {
+ Debug.Log("Saving to storage: " + blobName);
+ _fileOpsBusyLocks++;
+ var memoryStream = new MemoryStream();
+ using (JsonWriter jsonWriter = new JsonTextWriter(new StreamWriter(memoryStream)))
+ {
+ _jsonSerializer.Serialize(jsonWriter, saveData);
+ }
+
+ var buffer = memoryStream.GetBuffer();
+ Gdk.Helpers.Save(buffer, blobName);
+ }
+
+ public void PerformPendingSaveOperation()
+ {
+ if (!isBusyWithFileOps && !LoadManager.IsBusy())
+ {
+ SaveGame(_pendingGameSave, _pendingSettingsSave, _pendingGfxSettingsSave, _pendingInputActionsSave);
+ _pendingGameSave = null;
+ _pendingSettingsSave = null;
+ _pendingGfxSettingsSave = null;
+ _pendingInputActionsSave = null;
+ }
+ }
+
+ private void OnGameSaveComplete(object sender, string blobName)
+ {
+ _fileOpsBusyLocks--;
+ Debug.Log("[GDK] save to blob " + blobName + " complete");
+ }
+
+ private void OnGameSaveFailed(object sender, string blobName)
+ {
+ _fileOpsBusyLocks--;
+ Debug.Log("[GDK] save to blob " + blobName + " failed");
+ }
+
+ private void OnGameSaveLoaded(object sender, string blobName, GameSaveLoadedArgs saveData)
+ {
+ _fileOpsBusyLocks--;
+ Debug.Log("[GDK] save file load complete! blob name: " + blobName);
+ var memoryStream = new MemoryStream(saveData.Data);
+ memoryStream.Seek(0L, SeekOrigin.Begin);
+ using (var jsonTextReader = new JsonTextReader(new StreamReader(memoryStream)))
+ {
+ var x1SaveData = _jsonSerializer.Deserialize(jsonTextReader);
+ if (_isLoadingGameBlob)
+ {
+ if (x1SaveData != null)
+ {
+ if (x1SaveData.gameSave == null)
+ {
+ Debug.Log("[GDK] tempSaveData.gameSave is null (oh no)");
+ }
+
+ _saveData.gameSave = x1SaveData.gameSave ?? new GameSave();
+ }
+ else
+ {
+ Debug.Log("[GDK] tempSaveData is null (oh no)");
+ _saveData.gameSave = new GameSave();
+ }
+ }
+ else
+ {
+ if (x1SaveData != null)
+ {
+ _saveData.gfxSettings = x1SaveData.gfxSettings ?? new GraphicSettings(true);
+ _saveData.settings = x1SaveData.settings ?? new SettingsSave();
+ _saveData.inputActionsJson = x1SaveData.inputActionsJson ?? ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson();
+ }
+ else
+ {
+ _saveData.gfxSettings = new GraphicSettings(true);
+ _saveData.settings = new SettingsSave();
+ _saveData.inputActionsJson = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson();
+ }
+
+ Debug.Log(string.Format("after settings load, _saveData.gameSave is null: {0}", _saveData.gameSave == null));
+ Debug.Log(string.Format("_saveData loopCount: {0}", _saveData.gameSave.loopCount));
+ }
+ }
+
+ if (_isLoadingGameBlob)
+ {
+ _isLoadingGameBlob = false;
+ LoadGame(OW_GAME_SETTINGS_BLOB_NAME);
+ _isLoadingSettingsBlob = true;
+ return;
+ }
+
+ if (_isLoadingSettingsBlob)
+ {
+ _isLoadingSettingsBlob = false;
+ var onProfileReadDone = OnProfileReadDone;
+ if (onProfileReadDone == null)
+ {
+ return;
+ }
+
+ onProfileReadDone();
+ }
+ }
+
+ private void OnGameSaveLoadFailed(object sender, string blobName)
+ {
+ _fileOpsBusyLocks--;
+ if (_isLoadingGameBlob)
+ {
+ _saveData.gameSave = new GameSave();
+ SaveGame(_saveData.gameSave, null, null, null);
+ _isLoadingGameBlob = false;
+ LoadGame(OW_GAME_SETTINGS_BLOB_NAME);
+ _isLoadingSettingsBlob = true;
+ return;
+ }
+
+ if (_isLoadingSettingsBlob)
+ {
+ _saveData.settings = new SettingsSave();
+ _saveData.gfxSettings = new GraphicSettings(true);
+ _saveData.inputActionsJson = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson();
+ SaveGame(null, _saveData.settings, _saveData.gfxSettings, _saveData.inputActionsJson);
+ _isLoadingSettingsBlob = false;
+ var onProfileReadDone = OnProfileReadDone;
+ if (onProfileReadDone == null)
+ {
+ return;
+ }
+
+ onProfileReadDone();
+ }
+ }
+
+ [Serializable]
+ public class QSBX1SaveData
+ {
+ [XmlElement("gameSave")]
+ public GameSave gameSave;
+
+ [XmlElement("gameMultSave")]
+ [OptionalField(VersionAdded = 5)]
+ public GameSave gameMultSave;
+
+ [XmlElement("settings")]
+ public SettingsSave settings;
+
+ [XmlElement("gfxSettings")]
+ [OptionalField(VersionAdded = 2)]
+ public GraphicSettings gfxSettings;
+
+ [OptionalField(VersionAdded = 3)]
+ [NonSerialized]
+ public InputRebindableData bindingSettings;
+
+ [OptionalField(VersionAdded = 4)]
+ public string inputActionsPacked;
+
+ private InputActionAsset _inputActionsSave;
+
+ [JsonIgnore]
+ public string inputActionsJson
+ {
+ get => inputActionsPacked;
+ set
+ {
+ inputActionsPacked = value;
+ if (!string.IsNullOrEmpty(inputActionsPacked))
+ {
+ _inputActionsSave = InputActionAsset.FromJson(inputActionsPacked);
+ return;
+ }
+
+ _inputActionsSave = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions;
+ }
+ }
+
+ [JsonIgnore]
+ public InputActionAsset inputActionsSave
+ {
+ get
+ {
+ if (_inputActionsSave == null && !string.IsNullOrEmpty(inputActionsPacked))
+ {
+ try
+ {
+ _inputActionsSave = InputActionAsset.FromJson(inputActionsPacked);
+ }
+ catch (Exception)
+ {
+ _inputActionsSave = null;
+ }
+ }
+
+ return _inputActionsSave;
+ }
+ }
+
+ [OnDeserializing]
+ private void SetDefaultValuesOnDeserializing(StreamingContext context)
+ {
+ gfxSettings = null;
+ bindingSettings = null;
+ inputActionsPacked = null;
+ }
+ }
+}
diff --git a/QSB/SaveSync/QSBStandaloneProfileManager.cs b/QSB/SaveSync/QSBStandaloneProfileManager.cs
new file mode 100644
index 00000000..5c91f1b0
--- /dev/null
+++ b/QSB/SaveSync/QSBStandaloneProfileManager.cs
@@ -0,0 +1,1193 @@
+using Newtonsoft.Json;
+using QSB.Utility;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters.Binary;
+using UnityEngine;
+
+namespace QSB.SaveSync;
+
+internal class QSBStandaloneProfileManager : IProfileManager
+{
+ private static QSBStandaloneProfileManager s_instance;
+
+ private const string _saveDirectory = "/SteamSaves";
+ private const string _backupDirectory = "/Backup";
+ private const string _tempDirectory = "/Temp";
+ private const string _gameSaveFilename = "data.owsave";
+ private const string _gameSaveMultFilename = "data_mult.owsave";
+ private const string _gameSettingsFilename = "player.owsett";
+ private const string _gfxSettingsFilename = "graphics.owsett";
+ private const string _legacyInputBindingSettingsFilename = "input.owsett";
+ private const string _inputActionsSettingsFilename = "input_new.owsett";
+ private const int _profileNameCharLimit = 16;
+
+ private string _profilesPath;
+ private string _profileTempPath;
+ private string _profileBackupPath;
+ private int _fileOpsBusyLocks;
+ private GameSave _pendingGameSave;
+ private SettingsSave _pendingSettingsSave;
+ private GraphicSettings _pendingGfxSettingsSave;
+ private string _pendingInputJSONSave = "";
+ private BinaryFormatter _binaryFormatter;
+ private JsonSerializer _jsonSerializer;
+
+ public static QSBStandaloneProfileManager SharedInstance
+ {
+ get
+ {
+ if (s_instance == null)
+ {
+ s_instance = new QSBStandaloneProfileManager();
+ }
+
+ return s_instance;
+ }
+ }
+
+ public GameSave currentProfileGameSave => currentProfile?.gameSave;
+
+ public SettingsSave currentProfileGameSettings => currentProfile?.settingsSave;
+
+ public GraphicSettings currentProfileGraphicsSettings => currentProfile?.graphicsSettings;
+
+ public string currentProfileInputJSON => currentProfile?.inputJSON;
+
+ public QSBProfileData currentProfile { get; private set; }
+
+ public QSBProfileData mostRecentProfile
+ => profiles.OrderByDescending(profile => profile.lastModifiedTime).FirstOrDefault();
+
+ public int profileNameCharacterLimit => _profileNameCharLimit;
+
+ public List profiles { get; private set; }
+
+ public int numberOfProfiles => profiles.Count;
+
+ public bool isInitialized => currentProfileGameSave != null;
+
+ public bool isBusyWithFileOps => _fileOpsBusyLocks > 0;
+
+ public bool hasPendingSaveOperation => _pendingGameSave != null
+ || _pendingSettingsSave != null
+ || _pendingGfxSettingsSave != null
+ || _pendingInputJSONSave != "";
+
+
+ public int profileCharacterLimit => _profileNameCharLimit;
+
+ public delegate void NoProfilesExistEvent();
+ public delegate void BrokenDataExistsEvent();
+ public delegate void BackupDataRestoredEvent();
+ public delegate void UpdatePlayerProfilesEvent();
+
+ public event NoProfilesExistEvent OnNoProfilesExist;
+ public event BrokenDataExistsEvent OnBrokenDataExists;
+ public event BackupDataRestoredEvent OnBackupDataRestored;
+ public event UpdatePlayerProfilesEvent OnUpdatePlayerProfiles;
+ public event ProfileSignInCompleteEvent OnProfileSignInComplete;
+ public event ProfileReadDoneEvent OnProfileReadDone;
+ public event ProfileDataSavedEvent OnProfileDataSaved;
+ public event ProfileSignOutCompleteEvent OnProfileSignOutComplete;
+ public event ProfileSignInStartEvent OnProfileSignInStart;
+ public event ProfileSignOutStartEvent OnProfileSignOutStart;
+ public event ControllerDisconnectedEvent OnControllerDisconnected;
+ public event ControllerReconnectedEvent OnControllerReconnected;
+
+ public void PreInitialize()
+ {
+ _fileOpsBusyLocks = 0;
+ _pendingGameSave = null;
+ _pendingSettingsSave = null;
+ _pendingGfxSettingsSave = null;
+ _pendingInputJSONSave = "";
+ }
+
+ public void Initialize()
+ {
+ _profilesPath = Application.persistentDataPath + _saveDirectory;
+ _profileBackupPath = Application.persistentDataPath + _backupDirectory;
+ _profileTempPath = Application.persistentDataPath + _tempDirectory;
+ profiles = new List();
+ var versionDeserializationBinder = new VersionDeserializationBinder();
+ _jsonSerializer = new JsonSerializer
+ {
+ SerializationBinder = versionDeserializationBinder
+ };
+ _binaryFormatter = new BinaryFormatter
+ {
+ Binder = versionDeserializationBinder
+ };
+ Achievements.Init();
+ InitializeProfileData();
+ }
+
+ public void InitializeForEditor()
+ {
+ _profilesPath = Application.persistentDataPath + _saveDirectory;
+ _profileBackupPath = Application.persistentDataPath + _backupDirectory;
+ _profileTempPath = Application.persistentDataPath + _tempDirectory;
+ profiles = new List();
+ var versionDeserializationBinder = new VersionDeserializationBinder();
+ _jsonSerializer = new JsonSerializer
+ {
+ SerializationBinder = versionDeserializationBinder
+ };
+ _binaryFormatter = new BinaryFormatter
+ {
+ Binder = versionDeserializationBinder
+ };
+ MarkBusyWithFileOps(true);
+ profiles.Clear();
+ LoadProfiles();
+ LoadSaveFilesFromProfiles();
+ var flag = false;
+ for (var i = 0; i < profiles.Count; i++)
+ {
+ if (profiles[i].profileName == "Debug")
+ {
+ currentProfile = profiles[i];
+ flag = true;
+ break;
+ }
+ }
+
+ if (!flag)
+ {
+ TryCreateProfile("Debug");
+ }
+
+ MarkBusyWithFileOps(false);
+ PlayerData.Init(currentProfileGameSave, currentProfileGameSettings, currentProfileGraphicsSettings, currentProfileInputJSON);
+ }
+
+ private void MarkBusyWithFileOps(bool isBusy)
+ {
+ if (isBusy)
+ {
+ _fileOpsBusyLocks++;
+ return;
+ }
+
+ if (_fileOpsBusyLocks <= 0)
+ {
+ Debug.LogWarning("No File I/O lock to remove!");
+ return;
+ }
+
+ _fileOpsBusyLocks--;
+ }
+
+ public void PerformPendingSaveOperation()
+ {
+ if (!isBusyWithFileOps && !LoadManager.IsBusy())
+ {
+ TrySaveProfile(currentProfile, _pendingGameSave, _pendingSettingsSave, _pendingGfxSettingsSave, _pendingInputJSONSave);
+ _pendingGameSave = null;
+ _pendingSettingsSave = null;
+ _pendingGfxSettingsSave = null;
+ _pendingInputJSONSave = "";
+ }
+ }
+
+ public void SaveGame(GameSave gameSave, SettingsSave settSave, GraphicSettings graphicSettings, string inputBindings)
+ {
+ if (isBusyWithFileOps || LoadManager.IsBusy())
+ {
+ _pendingGameSave = gameSave;
+ _pendingSettingsSave = settSave;
+ _pendingGfxSettingsSave = graphicSettings;
+ _pendingInputJSONSave = inputBindings;
+ return;
+ }
+
+ TrySaveProfile(currentProfile, gameSave, settSave, graphicSettings, inputBindings);
+ }
+
+ private void InitializeProfileData()
+ {
+ LoadProfiles();
+ currentProfile = mostRecentProfile;
+ if (currentProfile != null)
+ {
+ LoadSaveFilesFromProfiles();
+ return;
+ }
+
+ var onNoProfilesExist = OnNoProfilesExist;
+ if (onNoProfilesExist == null)
+ {
+ return;
+ }
+
+ onNoProfilesExist();
+ }
+
+ private void LoadSaveFilesFromProfiles()
+ {
+ MarkBusyWithFileOps(isBusy: true);
+ foreach (var profile in profiles)
+ {
+ var path = _profilesPath + "/" + profile.profileName;
+ GameSave saveData = null;
+ GameSave multSaveData = null;
+ SettingsSave settingsData = null;
+ GraphicSettings graphicsData = null;
+ var inputJSON = "";
+ if (Directory.Exists(path))
+ {
+ Stream stream = null;
+ var directoryInfo = new DirectoryInfo(path);
+ profile.brokenSaveData = TryLoadSaveData(ref stream, _gameSaveFilename, directoryInfo, out saveData);
+ profile.brokenMultSaveData = TryLoadSaveData(ref stream, _gameSaveMultFilename, directoryInfo, out multSaveData);
+ profile.brokenSettingsData = TryLoadSaveData(ref stream, _gameSettingsFilename, directoryInfo, out settingsData);
+ profile.brokenGfxSettingsData = TryLoadSaveData(ref stream, _gfxSettingsFilename, directoryInfo, out graphicsData);
+ profile.brokenRebindingData = TryLoadInputBindingsSave(ref stream, directoryInfo, out inputJSON);
+ }
+
+ var profilePath = _profileBackupPath + "/" + profile.profileName;
+ var savePath = profilePath + "/" + _gameSaveFilename;
+ var multSavePath = profilePath + "/" + _gameSaveMultFilename;
+ var settingsPath = profilePath + "/" + _gameSettingsFilename;
+ var graphicsPath = profilePath + "/" + _gfxSettingsFilename;
+ var inputsPath = profilePath + "/" + _inputActionsSettingsFilename;
+
+ if (saveData == null)
+ {
+ profile.brokenSaveData = File.Exists(savePath);
+ saveData = new GameSave();
+ Debug.LogError("Could not find game save for " + profile.profileName);
+ }
+
+ if (multSaveData == null)
+ {
+ profile.brokenMultSaveData = File.Exists(multSavePath);
+ multSaveData = new GameSave();
+ Debug.LogError("Could not find multiplayer game save for " + profile.profileName);
+ }
+
+ if (settingsData == null)
+ {
+ profile.brokenSettingsData = File.Exists(settingsPath);
+ settingsData = new SettingsSave();
+ Debug.LogError("Could not find game settings for " + profile.profileName);
+ }
+
+ if (graphicsData == null)
+ {
+ profile.brokenGfxSettingsData = File.Exists(graphicsPath);
+ graphicsData = new GraphicSettings(init: true);
+ Debug.LogError("Could not find graphics settings for " + profile.profileName);
+ }
+
+ if (inputJSON == "")
+ {
+ profile.brokenRebindingData = File.Exists(inputsPath);
+ inputJSON = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson();
+ Debug.LogError("Could not find input action settings for " + profile.profileName);
+ }
+
+ profile.gameSave = saveData;
+ profile.multiplayerGameSave = multSaveData;
+ profile.settingsSave = settingsData;
+ profile.graphicsSettings = graphicsData;
+ profile.inputJSON = inputJSON;
+ }
+
+ MarkBusyWithFileOps(isBusy: false);
+ if (CurrentProfileHasBrokenData())
+ {
+ OnBrokenDataExists?.Invoke();
+ }
+
+ OnProfileReadDone?.Invoke();
+ }
+
+ private bool TryLoadSaveData(ref Stream stream, string fileName, DirectoryInfo directoryInfo, out T saveData)
+ {
+ saveData = default;
+ var flag = true;
+ var files = directoryInfo.GetFiles(fileName);
+ if (files.Length != 0)
+ {
+ stream = null;
+ if (TryOpenFile(files[0].FullName, ref stream))
+ {
+ var jsonTextReader = new JsonTextReader(new StreamReader(stream));
+ flag = !TryDeserializeJson(jsonTextReader, out saveData);
+ if (flag)
+ {
+ stream.Position = 0L;
+ flag = !TryDeserializeBinary(stream, out saveData);
+ }
+
+ jsonTextReader.Close();
+ }
+ }
+
+ return flag;
+ }
+
+ private bool TryLoadInputBindingsSave(ref Stream stream, DirectoryInfo directoryInfo, out string inputJSON)
+ {
+ inputJSON = null;
+ var result = true;
+ var files = directoryInfo.GetFiles(_inputActionsSettingsFilename);
+ if (files.Length != 0)
+ {
+ stream = null;
+ if (TryOpenFile(files[0].FullName, ref stream))
+ {
+ result = !TryDeserializeJsonAsInputActionsData(stream, out inputJSON);
+ }
+
+ var stream2 = stream;
+ if (stream2 != null)
+ {
+ stream2.Close();
+ }
+ }
+
+ return result;
+ }
+
+ private bool TryOpenFile(string fullPath, ref Stream dataStream)
+ {
+ bool result;
+ try
+ {
+ dataStream = File.Open(fullPath, FileMode.Open);
+ result = true;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError("[" + ex.Message + "] Failed loading opening file " + fullPath);
+ result = false;
+ }
+
+ return result;
+ }
+
+ private bool TryDeserializeBinary(Stream dataStream, out T saveData)
+ {
+ bool result;
+ try
+ {
+ saveData = default;
+ saveData = (T)_binaryFormatter.Deserialize(dataStream);
+ Debug.Log("Successfully read " + typeof(T).Name + " save data as binary");
+ result = true;
+ }
+ catch (Exception ex)
+ {
+ saveData = default;
+ Debug.LogError(string.Concat(new string[]
+ {
+ "[",
+ ex.Message,
+ "] Deserialization error for binary ",
+ typeof(T).Name,
+ " save data"
+ }));
+ result = false;
+ }
+
+ return result;
+ }
+
+ private bool TryDeserializeJson(JsonTextReader jsonReader, out T rebindingData)
+ {
+ bool result;
+ try
+ {
+ rebindingData = _jsonSerializer.Deserialize(jsonReader);
+ result = true;
+ }
+ catch (Exception)
+ {
+ rebindingData = default;
+ Debug.LogWarning("Could not read " + typeof(T).Name + " save data as JSON, it might be in binary so giving that a try.");
+ result = false;
+ }
+
+ return result;
+ }
+
+ private bool TryDeserializeJsonAsInputActionsData(Stream dataStream, out string inputJSON)
+ {
+ bool result;
+ try
+ {
+ using var streamReader = new StreamReader(dataStream);
+ var text = streamReader.ReadToEnd();
+ inputJSON = text;
+ Debug.Log("Successfully read Input Bindings save data as JSON");
+ result = true;
+ }
+ catch (Exception ex)
+ {
+ inputJSON = null;
+ Debug.LogError("[" + ex.Message + "] Deserialization error for Input Actions Save");
+ result = false;
+ }
+
+ return result;
+ }
+
+ public bool CurrentProfileHasBrokenData()
+ {
+ if (currentProfile == null)
+ {
+ Debug.LogError("QSBStandaloneProfileManager.CurrentProfileHasBrokenData We should never get here outside of the Unity Editor");
+ return false;
+ }
+
+ return currentProfile.brokenSaveData || currentProfile.brokenMultSaveData || currentProfile.brokenSettingsData || currentProfile.brokenGfxSettingsData || currentProfile.brokenRebindingData;
+ }
+
+ public bool BackupExistsForBrokenData()
+ {
+ var text = _profileBackupPath + "/" + currentProfile.profileName;
+ var savePath = text + "/" + _gameSaveFilename;
+ var multSavePath = text + "/" + _gameSaveMultFilename;
+ var settingsPath = text + "/" + _gameSettingsFilename;
+ var graphicsPath = text + "/" + _gfxSettingsFilename;
+ var inputsPath = text + "/" + _inputActionsSettingsFilename;
+
+ return (currentProfile.brokenSaveData && File.Exists(savePath))
+ || (currentProfile.brokenMultSaveData && File.Exists(multSavePath))
+ || (currentProfile.brokenSettingsData && File.Exists(settingsPath))
+ || (currentProfile.brokenGfxSettingsData && File.Exists(graphicsPath))
+ || (currentProfile.brokenRebindingData && File.Exists(inputsPath));
+ }
+
+ private void LoadProfiles()
+ {
+ MarkBusyWithFileOps(true);
+ profiles.Clear();
+ if (Directory.Exists(_profilesPath))
+ {
+ QSBProfileData profileData = null;
+ Stream stream = null;
+ var files = new DirectoryInfo(_profilesPath).GetFiles("*.owprofile");
+ foreach (var fileInfo in files)
+ {
+ DebugLog.DebugWrite(fileInfo.Name);
+ try
+ {
+ stream = null;
+ stream = File.Open(fileInfo.FullName, FileMode.Open);
+ var jsonTextReader = new JsonTextReader(new StreamReader(stream));
+ try
+ {
+ profileData = _jsonSerializer.Deserialize(jsonTextReader);
+ }
+ catch
+ {
+ stream.Position = 0L;
+ profileData = (QSBProfileData)_binaryFormatter.Deserialize(stream);
+ }
+ finally
+ {
+ jsonTextReader.Close();
+ }
+
+ if (profileData == null)
+ {
+ DebugLog.DebugWrite("Profile at " + fileInfo.FullName + " null. Skipping.");
+ }
+ else
+ {
+ profiles.Add(profileData);
+ }
+ }
+ catch (Exception ex)
+ {
+ DebugLog.ToConsole("[" + ex.Message + "] Failed loading profile at " + fileInfo.Name, OWML.Common.MessageType.Error);
+ stream?.Close();
+ }
+ }
+ }
+ else
+ {
+ DebugLog.DebugWrite($"{_profilesPath} does not exist");
+ }
+
+ MarkBusyWithFileOps(false);
+ }
+
+ public void RestoreCurrentProfileBackup()
+ {
+ MarkBusyWithFileOps(isBusy: true);
+ var profilePath = _profilesPath + "/" + currentProfile.profileName;
+ var savePath = profilePath + "/" + _gameSaveFilename;
+ var multSavePath = profilePath + "/" + _gameSaveMultFilename;
+ var settingsPath = profilePath + "/" + _gameSettingsFilename;
+ var graphicsPath = profilePath + "/" + _gfxSettingsFilename;
+ var inputsPath = profilePath + "/" + _inputActionsSettingsFilename;
+
+ var profileBackupPath = _profileBackupPath + "/" + currentProfile.profileName;
+ var saveBackupPath = profileBackupPath + "/" + _gameSaveFilename;
+ var multSaveBackupPath = profileBackupPath + "/" + _gameSaveMultFilename;
+ var settingsBackupPath = profileBackupPath + "/" + _gameSettingsFilename;
+ var graphicsBackupPath = profileBackupPath + "/" + _gfxSettingsFilename;
+ var inputsBackupPath = profileBackupPath + "/" + _inputActionsSettingsFilename;
+
+ Stream stream = null;
+ try
+ {
+ if (!Directory.Exists(_profilesPath))
+ {
+ Directory.CreateDirectory(_profilesPath);
+ }
+
+ if (!Directory.Exists(_profileTempPath))
+ {
+ Directory.CreateDirectory(_profileTempPath);
+ }
+
+ if (!Directory.Exists(_profileBackupPath))
+ {
+ Directory.CreateDirectory(_profileBackupPath);
+ }
+
+ if (!Directory.Exists(profilePath))
+ {
+ Directory.CreateDirectory(profilePath);
+ }
+
+ if (!Directory.Exists(profileBackupPath))
+ {
+ Directory.CreateDirectory(profileBackupPath);
+ }
+
+ var di = new DirectoryInfo(profileBackupPath);
+
+ if (currentProfile.brokenSaveData && File.Exists(saveBackupPath))
+ {
+ currentProfile.gameSave = LoadAndCopyBackupSave(_gameSaveFilename, saveBackupPath, savePath);
+ }
+
+ if (currentProfile.brokenMultSaveData && File.Exists(multSaveBackupPath))
+ {
+ currentProfile.multiplayerGameSave = LoadAndCopyBackupSave(_gameSaveMultFilename, multSaveBackupPath, multSavePath);
+ }
+
+ if (currentProfile.brokenSettingsData && File.Exists(settingsBackupPath))
+ {
+ currentProfile.settingsSave = LoadAndCopyBackupSave(_gameSettingsFilename, settingsBackupPath, settingsPath);
+ }
+
+ if (currentProfile.brokenGfxSettingsData && File.Exists(graphicsBackupPath))
+ {
+ currentProfile.graphicsSettings = LoadAndCopyBackupSave(_gfxSettingsFilename, graphicsBackupPath, graphicsPath);
+ }
+
+ if (currentProfile.brokenRebindingData && File.Exists(inputsBackupPath))
+ {
+ TryLoadInputBindingsSave(ref stream, di, out var inputJSON);
+ if (inputJSON != "")
+ {
+ currentProfile.inputJSON = inputJSON;
+ File.Copy(inputsBackupPath, inputsPath, overwrite: true);
+ }
+ else
+ {
+ Debug.LogError("Could not load backup input bindings save.");
+ }
+
+ stream?.Close();
+ stream = null;
+ }
+
+ OnBackupDataRestored?.Invoke();
+
+ T LoadAndCopyBackupSave(string fileName, string backupPath, string fullPath) where T : class
+ {
+ TryLoadSaveData(ref stream, fileName, di, out var saveData);
+ if (saveData != null)
+ {
+ File.Copy(backupPath, fullPath, overwrite: true);
+ }
+ else
+ {
+ Debug.LogError("Could not load backup " + typeof(T).Name + " save.");
+ }
+
+ stream?.Close();
+ stream = null;
+ return saveData;
+ }
+ }
+ catch (Exception ex)
+ {
+ stream?.Close();
+ Debug.LogError("Exception during backup restore: " + ex.Message);
+ MarkBusyWithFileOps(isBusy: false);
+ }
+
+ MarkBusyWithFileOps(isBusy: false);
+ }
+
+ private bool TrySaveProfile(QSBProfileData profileData, GameSave gameSave, SettingsSave settingsSave, GraphicSettings graphicsSettings, string inputJson)
+ {
+ MarkBusyWithFileOps(isBusy: true);
+ var profilePath = _profilesPath + "/" + profileData.profileName;
+ var profileManifestPath = _profilesPath + "/" + profileData.profileName + ".owprofile";
+ var saveDataPath = profilePath + "/" + _gameSaveFilename;
+ var multSaveDataPath = profilePath + "/" + _gameSaveMultFilename;
+ var settingsPath = profilePath + "/" + _gameSettingsFilename;
+ var graphicsPath = profilePath + "/" + _gfxSettingsFilename;
+ var inputsPath = profilePath + "/" + _inputActionsSettingsFilename;
+
+ var tempProfilePath = _profileTempPath + "/GameData";
+ var tempProfileManifestPath = _profileTempPath + "/CurrentProfile.owprofile";
+ var tempSaveDataPath = tempProfilePath + "/" + _gameSaveFilename;
+ var tempMultSaveDataPath = tempProfilePath + "/" + _gameSaveMultFilename;
+ var tempSettingsPath = tempProfilePath + "/" + _gameSettingsFilename;
+ var tempGraphicsPath = tempProfilePath + "/" + _gfxSettingsFilename;
+ var tempInputsPath = tempProfilePath + "/" + _inputActionsSettingsFilename;
+
+ var backupProfilePath = _profileBackupPath + "/" + profileData.profileName;
+ var backupSaveDataPath = backupProfilePath + "/" + _gameSaveFilename;
+ var backupMultSaveDataPath = backupProfilePath + "/" + _gameSaveMultFilename;
+ var backupSettingsPath = backupProfilePath + "/" + _gameSettingsFilename;
+ var backupGraphicsPath = backupProfilePath + "/" + _gfxSettingsFilename;
+ var backupInputsPath = backupProfilePath + "/" + _inputActionsSettingsFilename;
+
+ Stream stream = null;
+ try
+ {
+ // Create folders if they don't exist
+
+ if (!Directory.Exists(_profilesPath))
+ {
+ Directory.CreateDirectory(_profilesPath);
+ }
+
+ if (!Directory.Exists(_profileTempPath))
+ {
+ Directory.CreateDirectory(_profileTempPath);
+ }
+
+ if (!Directory.Exists(_profileBackupPath))
+ {
+ Directory.CreateDirectory(_profileBackupPath);
+ }
+
+ if (!Directory.Exists(profilePath))
+ {
+ Directory.CreateDirectory(profilePath);
+ }
+
+ if (!Directory.Exists(tempProfilePath))
+ {
+ Directory.CreateDirectory(tempProfilePath);
+ }
+
+ if (!Directory.Exists(backupProfilePath))
+ {
+ Directory.CreateDirectory(backupProfilePath);
+ }
+
+ // create temp files
+
+ SaveData(tempProfileManifestPath, profileData);
+ if (gameSave != null)
+ {
+ if (QSBCore.IsInMultiplayer)
+ {
+ profileData.multiplayerGameSave = SaveData(tempMultSaveDataPath, gameSave);
+ }
+ else
+ {
+ profileData.gameSave = SaveData(tempSaveDataPath, gameSave);
+ }
+ }
+
+ if (settingsSave != null)
+ {
+ profileData.settingsSave = SaveData(tempSettingsPath, settingsSave);
+ }
+
+ if (graphicsSettings != null)
+ {
+ profileData.graphicsSettings = SaveData(tempGraphicsPath, graphicsSettings);
+ }
+
+ if (inputJson != null)
+ {
+ File.WriteAllText(tempInputsPath, inputJson);
+ profileData.inputJSON = inputJson;
+ }
+
+ // create backups of old files
+
+ if (File.Exists(saveDataPath))
+ {
+ File.Copy(saveDataPath, backupSaveDataPath, overwrite: true);
+ }
+
+ if (File.Exists(multSaveDataPath))
+ {
+ File.Copy(multSaveDataPath, backupMultSaveDataPath, overwrite: true);
+ }
+
+ if (File.Exists(settingsPath))
+ {
+ File.Copy(settingsPath, backupSettingsPath, overwrite: true);
+ }
+
+ if (File.Exists(graphicsPath))
+ {
+ File.Copy(graphicsPath, backupGraphicsPath, overwrite: true);
+ }
+
+ if (File.Exists(inputsPath))
+ {
+ File.Copy(inputsPath, backupInputsPath, overwrite: true);
+ }
+
+ // delete old files and move temp files
+
+ File.Delete(profileManifestPath);
+ File.Move(tempProfileManifestPath, profileManifestPath);
+
+ if (gameSave != null)
+ {
+ if (QSBCore.IsInMultiplayer)
+ {
+ File.Delete(multSaveDataPath);
+ File.Move(tempMultSaveDataPath, multSaveDataPath);
+ }
+ else
+ {
+ File.Delete(saveDataPath);
+ File.Move(tempSaveDataPath, saveDataPath);
+ }
+ }
+
+ if (settingsSave != null)
+ {
+ File.Delete(settingsPath);
+ File.Move(tempSettingsPath, settingsPath);
+ }
+
+ if (graphicsSettings != null)
+ {
+ File.Delete(graphicsPath);
+ File.Move(tempGraphicsPath, graphicsPath);
+ }
+
+ if (inputJson != null)
+ {
+ File.Delete(inputsPath);
+ File.Move(tempInputsPath, inputsPath);
+ }
+
+ OnProfileDataSaved?.Invoke(true);
+ }
+ catch (Exception ex)
+ {
+ if (stream != null)
+ {
+ stream.Close();
+ }
+
+ OnProfileDataSaved?.Invoke(false);
+
+ Debug.LogError("[" + ex.Message + "] Error saving file for " + profileData.profileName);
+ MarkBusyWithFileOps(isBusy: false);
+ return false;
+ }
+
+ MarkBusyWithFileOps(isBusy: false);
+ return true;
+
+ T SaveData(string filePath, T data)
+ {
+ stream = File.Open(filePath, FileMode.Create);
+ using (JsonWriter jsonWriter = new JsonTextWriter(new StreamWriter(stream)))
+ {
+ _jsonSerializer.Serialize(jsonWriter, data);
+ }
+
+ stream = null;
+ return data;
+ }
+ }
+
+ public bool IsValidCharacterForProfileName(char inputChar)
+ {
+ if (char.IsWhiteSpace(inputChar))
+ {
+ return false;
+ }
+
+ var invalidFileNameChars = Path.GetInvalidFileNameChars();
+ for (var i = 0; i < invalidFileNameChars.Length; i++)
+ {
+ if (invalidFileNameChars[i] == inputChar)
+ {
+ return false;
+ }
+ }
+
+ return inputChar != '.';
+ }
+
+ public bool ValidateProfileName(string profileName)
+ {
+ var result = true;
+ if (profileName == "")
+ {
+ result = false;
+ }
+ else if (profileName.Length > 16)
+ {
+ result = false;
+ }
+ else if (profiles.Count > 0)
+ {
+ for (var i = 0; i < profiles.Count; i++)
+ {
+ if (profiles[i].profileName == profileName)
+ {
+ result = false;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public bool TryCreateProfile(string profileName)
+ {
+ var savedProfile = ValidateProfileName(profileName);
+ if (savedProfile)
+ {
+ var noProfilesExist = profiles.Count == 0;
+ var profileData = new QSBProfileData
+ {
+ profileName = profileName,
+ lastModifiedTime = DateTime.UtcNow
+ };
+ var gameSave = new GameSave();
+ var multGameSave = new GameSave();
+ var settingsSave = new SettingsSave();
+ var graphicSettings = currentProfileGraphicsSettings;
+ if (graphicSettings == null)
+ {
+ graphicSettings = new GraphicSettings(init: true);
+ }
+
+ var text = ((InputManager)OWInput.SharedInputManager).commandManager.DefaultInputActions.ToJson();
+ profiles.Add(profileData);
+ profileData.gameSave = gameSave;
+ profileData.multiplayerGameSave = multGameSave;
+ profileData.settingsSave = settingsSave;
+ profileData.graphicsSettings = graphicSettings;
+ profileData.inputJSON = text;
+ savedProfile = TrySaveProfile(profileData, gameSave, settingsSave, graphicSettings, text);
+ if (savedProfile)
+ {
+ if (currentProfile != null && currentProfile.profileName != string.Empty)
+ {
+ OnProfileSignOutComplete?.Invoke();
+ }
+
+ currentProfile = profileData;
+ if (noProfilesExist)
+ {
+ OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE);
+ OnProfileReadDone?.Invoke();
+ }
+ else
+ {
+ OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE);
+ OnProfileReadDone?.Invoke();
+ OnUpdatePlayerProfiles?.Invoke();
+ }
+ }
+ else
+ {
+ DeleteProfile(profileName);
+ }
+ }
+
+ return savedProfile;
+ }
+
+ public bool SwitchProfile(string profileName)
+ {
+ LoadSaveFilesFromProfiles();
+ var flag = false;
+ for (var i = 0; i < profiles.Count; i++)
+ {
+ if (profileName == profiles[i].profileName)
+ {
+ if (currentProfile != null && currentProfile.profileName != string.Empty && OnProfileSignOutComplete != null)
+ {
+ OnProfileSignOutComplete();
+ }
+
+ currentProfile = profiles[i];
+ flag = true;
+ break;
+ }
+ }
+
+ if (flag)
+ {
+ currentProfile.lastModifiedTime = DateTime.UtcNow;
+ TrySaveProfile(currentProfile, null, null, null, null);
+ OnProfileSignInComplete?.Invoke(ProfileManagerSignInResult.COMPLETE);
+
+ if (CurrentProfileHasBrokenData() && OnBrokenDataExists != null)
+ {
+ OnBrokenDataExists();
+ return false;
+ }
+
+ OnProfileReadDone?.Invoke();
+ }
+
+ return true;
+ }
+
+ public void DeleteProfile(string profileName)
+ {
+ Debug.Log("DeleteProfile");
+ var flag = false;
+ var profileData = new QSBProfileData
+ {
+ profileName = string.Empty
+ };
+ for (var i = 0; i < profiles.Count; i++)
+ {
+ if (profileName == profiles[i].profileName)
+ {
+ profileData = profiles[i];
+ flag = true;
+ break;
+ }
+ }
+
+ if (!flag)
+ {
+ return;
+ }
+
+ MarkBusyWithFileOps(isBusy: true);
+ var profileManifestPath = _profilesPath + "/" + profileData.profileName + ".owprofile";
+ var profilePath = _profilesPath + "/" + profileData.profileName;
+ var gameSavePath = profilePath + "/" + _gameSaveFilename;
+ var multGameSavePath = profilePath + "/" + _gameSaveMultFilename;
+ var settingsPath = profilePath + "/" + _gameSettingsFilename;
+ var graphicsPath = profilePath + "/" + _gfxSettingsFilename;
+ var oldInputsPath = profilePath + "/" + _legacyInputBindingSettingsFilename;
+ var inputsPath = profilePath + "/" + _inputActionsSettingsFilename;
+
+ var backupProfilePath = _profileBackupPath + "/" + profileData.profileName;
+ var backupGameSave = backupProfilePath + "/" + _gameSaveFilename;
+ var backupMultGameSave = backupProfilePath + "/" + _gameSaveMultFilename;
+ var backupSettingsPath = backupProfilePath + "/" + _gameSettingsFilename;
+ var backupGraphicsPath = backupProfilePath + "/" + _gfxSettingsFilename;
+ var backupOldInputsPath = backupProfilePath + "/" + _legacyInputBindingSettingsFilename;
+ var backupInputsPath = backupProfilePath + "/" + _inputActionsSettingsFilename;
+ Stream stream = null;
+ try
+ {
+ if (File.Exists(profileManifestPath))
+ {
+ File.Delete(profileManifestPath);
+ Debug.Log("Delete " + profileManifestPath);
+ }
+
+ if (File.Exists(gameSavePath))
+ {
+ File.Delete(gameSavePath);
+ Debug.Log("Delete " + gameSavePath);
+ }
+
+ if (File.Exists(multGameSavePath))
+ {
+ File.Delete(multGameSavePath);
+ Debug.Log("Delete " + multGameSavePath);
+ }
+
+ if (File.Exists(settingsPath))
+ {
+ File.Delete(settingsPath);
+ Debug.Log("Delete " + settingsPath);
+ }
+
+ if (File.Exists(graphicsPath))
+ {
+ File.Delete(graphicsPath);
+ Debug.Log("Delete " + graphicsPath);
+ }
+
+ if (File.Exists(oldInputsPath))
+ {
+ File.Delete(oldInputsPath);
+ Debug.Log("Delete " + oldInputsPath);
+ }
+
+ if (File.Exists(inputsPath))
+ {
+ File.Delete(inputsPath);
+ Debug.Log("Delete " + inputsPath);
+ }
+
+ if (File.Exists(backupGameSave))
+ {
+ File.Delete(backupGameSave);
+ Debug.Log("Delete " + backupGameSave);
+ }
+
+ if (File.Exists(backupMultGameSave))
+ {
+ File.Delete(backupMultGameSave);
+ Debug.Log("Delete " + backupMultGameSave);
+ }
+
+ if (File.Exists(backupSettingsPath))
+ {
+ File.Delete(backupSettingsPath);
+ Debug.Log("Delete " + backupSettingsPath);
+ }
+
+ if (File.Exists(backupGraphicsPath))
+ {
+ File.Delete(backupGraphicsPath);
+ Debug.Log("Delete " + backupGraphicsPath);
+ }
+
+ if (File.Exists(backupOldInputsPath))
+ {
+ File.Delete(backupOldInputsPath);
+ Debug.Log("Delete " + backupOldInputsPath);
+ }
+
+ if (File.Exists(backupInputsPath))
+ {
+ File.Delete(backupInputsPath);
+ Debug.Log("Delete " + backupInputsPath);
+ }
+
+ profiles.Remove(profileData);
+ var files = Directory.GetFiles(profilePath);
+ var directories = Directory.GetDirectories(profilePath);
+ if (files.Length == 0 && directories.Length == 0)
+ {
+ Directory.Delete(profilePath);
+ }
+ else
+ {
+ Debug.LogWarning(" Directory not empty. Cannot delete. ");
+ }
+
+ if (Directory.Exists(backupProfilePath))
+ {
+ files = Directory.GetFiles(backupProfilePath);
+ directories = Directory.GetDirectories(backupProfilePath);
+ if (files.Length == 0 && directories.Length == 0)
+ {
+ Directory.Delete(backupProfilePath);
+ }
+ else
+ {
+ Debug.LogWarning("Backup Directory not empty. Cannot delete.");
+ }
+ }
+
+ OnUpdatePlayerProfiles?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ stream?.Close();
+ Debug.LogError("[" + ex.Message + "] Failed to delete all profile data");
+ MarkBusyWithFileOps(isBusy: false);
+ }
+
+ MarkBusyWithFileOps(isBusy: false);
+ }
+
+ [Serializable]
+ public class QSBProfileData
+ {
+ public string profileName;
+ public DateTime lastModifiedTime;
+ public bool brokenSaveData;
+ public bool brokenMultSaveData;
+ public bool brokenSettingsData;
+ public bool brokenGfxSettingsData;
+ public bool brokenRebindingData;
+ private GameSave _gameSave;
+ private GameSave _multiplayerGameSave;
+ private SettingsSave _settingsSave;
+ private GraphicSettings _graphicsSettings;
+ private string _inputJSON;
+
+ [JsonIgnore]
+ public GameSave gameSave
+ {
+ get => _gameSave;
+ set => _gameSave = value;
+ }
+
+
+ [JsonIgnore]
+ public GameSave multiplayerGameSave
+ {
+ get => _multiplayerGameSave;
+ set => _multiplayerGameSave = value;
+ }
+
+ [JsonIgnore]
+ public SettingsSave settingsSave
+ {
+ get => _settingsSave;
+ set => _settingsSave = value;
+ }
+
+ [JsonIgnore]
+ public GraphicSettings graphicsSettings
+ {
+ get => _graphicsSettings;
+ set => _graphicsSettings = value;
+ }
+
+ [JsonIgnore]
+ public string inputJSON
+ {
+ get => _inputJSON;
+ set => _inputJSON = value;
+ }
+
+ [OnDeserializing]
+ private void SetDefaultValuesOnDeserializing(StreamingContext context)
+ {
+ brokenSaveData = false;
+ brokenMultSaveData = false;
+ brokenSettingsData = false;
+ brokenGfxSettingsData = false;
+ brokenRebindingData = false;
+ }
+
+ [OnDeserialized]
+ private void SetDefaultValuesOnDeserialized(StreamingContext context)
+ {
+ brokenSaveData = false;
+ brokenMultSaveData = false;
+ brokenSettingsData = false;
+ brokenGfxSettingsData = false;
+ brokenRebindingData = false;
+ }
+ }
+}
diff --git a/QSB/SectorSync/WorldObjects/QSBSector.cs b/QSB/SectorSync/WorldObjects/QSBSector.cs
index 28d91286..6dcce996 100644
--- a/QSB/SectorSync/WorldObjects/QSBSector.cs
+++ b/QSB/SectorSync/WorldObjects/QSBSector.cs
@@ -23,8 +23,6 @@ public class QSBSector : WorldObject
}
}
- public override void SendInitialState(uint to) { }
-
private static EyeShuttleController _cachedShuttleController;
public bool ShouldSyncTo(DynamicOccupant occupantType)
diff --git a/QSB/ShipSync/Messages/FlyShipMessage.cs b/QSB/ShipSync/Messages/FlyShipMessage.cs
index 60e8baa6..144efd70 100644
--- a/QSB/ShipSync/Messages/FlyShipMessage.cs
+++ b/QSB/ShipSync/Messages/FlyShipMessage.cs
@@ -8,6 +8,9 @@ using UnityEngine;
namespace QSB.ShipSync.Messages;
+///
+/// TODO: initial state for the current flyer
+///
internal class FlyShipMessage : QSBMessage
{
static FlyShipMessage()
diff --git a/QSB/ShipSync/ShipCustomAttach.cs b/QSB/ShipSync/ShipCustomAttach.cs
index 6ce9adc2..e8106d09 100644
--- a/QSB/ShipSync/ShipCustomAttach.cs
+++ b/QSB/ShipSync/ShipCustomAttach.cs
@@ -1,12 +1,22 @@
-using UnityEngine;
+using QSB.Localization;
+using UnityEngine;
namespace QSB.ShipSync;
public class ShipCustomAttach : MonoBehaviour
{
- private static readonly ScreenPrompt _attachPrompt = new(InputLibrary.interactSecondary, InputLibrary.interact,
- "Attach to ship" + " ", ScreenPrompt.MultiCommandType.HOLD_ONE_AND_PRESS_2ND);
- private static readonly ScreenPrompt _detachPrompt = new(InputLibrary.cancel, "Detach from ship" + " ");
+ private readonly ScreenPrompt _attachPrompt = new(
+ InputLibrary.interactSecondary,
+ InputLibrary.interact,
+ QSBLocalization.Current.AttachToShip + " ",
+ ScreenPrompt.MultiCommandType.HOLD_ONE_AND_PRESS_2ND
+ );
+
+ private readonly ScreenPrompt _detachPrompt = new(
+ InputLibrary.cancel,
+ QSBLocalization.Current.DetachFromShip + " "
+ );
+
private PlayerAttachPoint _playerAttachPoint;
private void Awake()
diff --git a/QSB/ShipSync/ShipManager.cs b/QSB/ShipSync/ShipManager.cs
index aa88ca0f..c85a2751 100644
--- a/QSB/ShipSync/ShipManager.cs
+++ b/QSB/ShipSync/ShipManager.cs
@@ -42,15 +42,28 @@ internal class ShipManager : WorldObjectManager
_currentFlyer = value;
}
}
+ public bool IsShipWrecked => _shipDestroyed || ShipCockpitUI._shipDamageCtrlr.IsDestroyed();
private readonly List _playersInShip = new();
private uint _currentFlyer = uint.MaxValue;
+ private bool _shipDestroyed;
public void Start()
{
Instance = this;
QSBPlayerManager.OnRemovePlayer += OnRemovePlayer;
+ GlobalMessenger.AddListener("ShipDestroyed", OnShipDestroyed);
+ }
+
+ public void OnDestroy()
+ {
+ GlobalMessenger.RemoveListener("ShipDestroyed", OnShipDestroyed);
+ }
+
+ private void OnShipDestroyed()
+ {
+ _shipDestroyed = true;
}
private void OnRemovePlayer(PlayerInfo player)
@@ -63,6 +76,8 @@ internal class ShipManager : WorldObjectManager
public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct)
{
+ _shipDestroyed = false;
+
var shipBody = Locator.GetShipBody();
if (shipBody == null)
{
@@ -89,17 +104,6 @@ internal class ShipManager : WorldObjectManager
if (QSBCore.IsHost)
{
- if (ShipTransformSync.LocalInstance != null)
- {
- if (ShipTransformSync.LocalInstance.gameObject == null)
- {
- DebugLog.ToConsole($"Warning - ShipTransformSync's LocalInstance is not null, but it's gameobject is null!", MessageType.Warning);
- return;
- }
-
- NetworkServer.Destroy(ShipTransformSync.LocalInstance.gameObject);
- }
-
if (QSBPlayerManager.LocalPlayer.TransformSync == null)
{
DebugLog.ToConsole($"Error - Tried to spawn ship, but LocalPlayer's TransformSync is null!", MessageType.Error);
@@ -115,7 +119,7 @@ internal class ShipManager : WorldObjectManager
_shipCustomAttach.transform.SetParent(shipBody.transform, false);
_shipCustomAttach.AddComponent();
- QSBWorldSync.Init(new List()
+ QSBWorldSync.Init(new[]
{
CockpitController._headlight,
CockpitController._landingLight,
@@ -134,7 +138,24 @@ internal class ShipManager : WorldObjectManager
QSBWorldSync.Init();
}
- public override void UnbuildWorldObjects() => Destroy(_shipCustomAttach);
+ public override void UnbuildWorldObjects()
+ {
+ if (QSBCore.IsHost)
+ {
+ if (ShipTransformSync.LocalInstance != null)
+ {
+ if (ShipTransformSync.LocalInstance.gameObject == null)
+ {
+ DebugLog.ToConsole($"Warning - ShipTransformSync's LocalInstance is not null, but it's gameobject is null!", MessageType.Warning);
+ return;
+ }
+
+ NetworkServer.Destroy(ShipTransformSync.LocalInstance.gameObject);
+ }
+ }
+
+ Destroy(_shipCustomAttach);
+ }
public void AddPlayerToShip(PlayerInfo player)
{
@@ -218,4 +239,4 @@ internal class ShipManager : WorldObjectManager
CockpitController._landingCam.enabled = false;
CockpitController._shipAudioController.PlayLandingCamOff();
}
-}
\ No newline at end of file
+}
diff --git a/QSB/ShipSync/TransformSync/ShipTransformSync.cs b/QSB/ShipSync/TransformSync/ShipTransformSync.cs
index 8abe29dd..29d6597b 100644
--- a/QSB/ShipSync/TransformSync/ShipTransformSync.cs
+++ b/QSB/ShipSync/TransformSync/ShipTransformSync.cs
@@ -57,13 +57,20 @@ public class ShipTransformSync : SectoredRigidbodySync
}
var targetPos = ReferenceTransform.FromRelPos(transform.position);
+ var targetRot = ReferenceTransform.FromRelRot(transform.rotation);
- if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime)
+ if (PlayerState.IsInsideShip())
{
- _lastSetPositionTime = Time.unscaledTime;
-
- var targetRot = ReferenceTransform.FromRelRot(transform.rotation);
+ if (Time.unscaledTime >= _lastSetPositionTime + ForcePositionAfterTime)
+ {
+ _lastSetPositionTime = Time.unscaledTime;
+ AttachedRigidbody.SetPosition(targetPos);
+ AttachedRigidbody.SetRotation(targetRot);
+ }
+ }
+ else
+ {
AttachedRigidbody.SetPosition(targetPos);
AttachedRigidbody.SetRotation(targetRot);
}
diff --git a/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs b/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs
index cd49af02..e75b0c01 100644
--- a/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs
+++ b/QSB/ShipSync/WorldObjects/QSBShipDetachableLeg.cs
@@ -16,6 +16,6 @@ internal class QSBShipDetachableLeg : LinkedWorldObject
{
if (AttachedObject._damaged)
{
- this.SendMessage(new HullDamagedMessage());
+ this.SendMessage(new HullDamagedMessage { To = to });
}
else
{
- this.SendMessage(new HullRepairedMessage());
+ this.SendMessage(new HullRepairedMessage { To = to });
}
- this.SendMessage(new HullChangeIntegrityMessage(AttachedObject._integrity));
+ this.SendMessage(new HullChangeIntegrityMessage(AttachedObject._integrity) { To = to });
}
public void SetDamaged()
@@ -58,4 +58,4 @@ internal class QSBShipHull : WorldObject
var damageEffect = AttachedObject._damageEffect;
damageEffect.SetEffectBlend(1f - newIntegrity);
}
-}
\ No newline at end of file
+}
diff --git a/QSB/Syncs/Occasional/OccasionalTransformSync.cs b/QSB/Syncs/Occasional/OccasionalTransformSync.cs
index 9d265983..c4ba37f9 100644
--- a/QSB/Syncs/Occasional/OccasionalTransformSync.cs
+++ b/QSB/Syncs/Occasional/OccasionalTransformSync.cs
@@ -105,8 +105,8 @@ public class OccasionalTransformSync : UnsectoredRigidbodySync
_toMove.Add(new MoveData
{
Child = child,
- RelPos = AttachedRigidbody.transform.ToRelPos(pos),
- RelRot = AttachedRigidbody.transform.ToRelRot(child.GetRotation()),
+ RelPos = AttachedTransform.ToRelPos(pos),
+ RelRot = AttachedTransform.ToRelRot(child.GetRotation()),
RelVel = AttachedRigidbody.ToRelVel(child.GetVelocity(), pos),
RelAngVel = AttachedRigidbody.ToRelAngVel(child.GetAngularVelocity())
});
@@ -116,9 +116,9 @@ public class OccasionalTransformSync : UnsectoredRigidbodySync
{
foreach (var data in _toMove)
{
- var pos = AttachedRigidbody.transform.FromRelPos(data.RelPos);
+ var pos = AttachedTransform.FromRelPos(data.RelPos);
data.Child.SetPosition(pos);
- data.Child.SetRotation(AttachedRigidbody.transform.FromRelRot(data.RelRot));
+ data.Child.SetRotation(AttachedTransform.FromRelRot(data.RelRot));
data.Child.SetVelocity(AttachedRigidbody.FromRelVel(data.RelVel, pos));
data.Child.SetAngularVelocity(AttachedRigidbody.FromRelAngVel(data.RelAngVel));
}
diff --git a/QSB/TimeSync/Messages/ServerTimeMessage.cs b/QSB/TimeSync/Messages/ServerTimeMessage.cs
index 3be1812c..043074a6 100644
--- a/QSB/TimeSync/Messages/ServerTimeMessage.cs
+++ b/QSB/TimeSync/Messages/ServerTimeMessage.cs
@@ -7,11 +7,13 @@ public class ServerTimeMessage : QSBMessage
{
private float ServerTime;
private int LoopCount;
+ private float SecondsRemaining;
- public ServerTimeMessage(float time, int count)
+ public ServerTimeMessage(float time, int count, float secondsRemaining)
{
ServerTime = time;
LoopCount = count;
+ SecondsRemaining = secondsRemaining;
}
public override void Serialize(NetworkWriter writer)
@@ -19,6 +21,7 @@ public class ServerTimeMessage : QSBMessage
base.Serialize(writer);
writer.Write(ServerTime);
writer.Write(LoopCount);
+ writer.Write(SecondsRemaining);
}
public override void Deserialize(NetworkReader reader)
@@ -26,8 +29,9 @@ public class ServerTimeMessage : QSBMessage
base.Deserialize(reader);
ServerTime = reader.Read();
LoopCount = reader.Read();
+ SecondsRemaining = reader.Read();
}
public override void OnReceiveRemote()
- => WakeUpSync.LocalInstance.OnClientReceiveMessage(ServerTime, LoopCount);
-}
\ No newline at end of file
+ => WakeUpSync.LocalInstance.OnClientReceiveMessage(ServerTime, LoopCount, SecondsRemaining);
+}
diff --git a/QSB/TimeSync/Messages/SetSecondsRemainingMessage.cs b/QSB/TimeSync/Messages/SetSecondsRemainingMessage.cs
new file mode 100644
index 00000000..c8fbe274
--- /dev/null
+++ b/QSB/TimeSync/Messages/SetSecondsRemainingMessage.cs
@@ -0,0 +1,13 @@
+using QSB.Messaging;
+using QSB.Patches;
+
+namespace QSB.TimeSync.Messages;
+
+///
+/// sent from non-host to host
+///
+public class SetSecondsRemainingMessage : QSBMessage
+{
+ public SetSecondsRemainingMessage(float secondsRemaining) : base(secondsRemaining) => To = 0;
+ public override void OnReceiveRemote() => QSBPatch.RemoteCall(() => TimeLoop.SetSecondsRemaining(Data));
+}
diff --git a/QSB/TimeSync/Patches/TimePatches.cs b/QSB/TimeSync/Patches/TimePatches.cs
index 2763ac99..2c21229a 100644
--- a/QSB/TimeSync/Patches/TimePatches.cs
+++ b/QSB/TimeSync/Patches/TimePatches.cs
@@ -1,6 +1,8 @@
using HarmonyLib;
using QSB.Inputs;
+using QSB.Messaging;
using QSB.Patches;
+using QSB.TimeSync.Messages;
using QSB.Utility;
namespace QSB.TimeSync.Patches;
@@ -10,13 +12,13 @@ internal class TimePatches : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
+ ///
+ /// prevents wakeup prompt since we automatically wake you up.
+ /// (doesn't happen for host because we don't patch until TimeLoop._initialized i.e. after Start)
+ ///
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerCameraEffectController), nameof(PlayerCameraEffectController.OnStartOfTimeLoop))]
- public static bool PlayerCameraEffectController_OnStartOfTimeLoop()
- {
- DebugLog.DebugWrite($"OnStartOfTimeLoop");
- return false;
- }
+ public static bool PlayerCameraEffectController_OnStartOfTimeLoop() => false;
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerCameraEffectController), nameof(PlayerCameraEffectController.WakeUp))]
@@ -27,7 +29,6 @@ internal class TimePatches : QSBPatch
Delay.RunWhen(() => !__instance._isOpeningEyes, () => QSBInputManager.Instance.SetInputsEnabled(true));
}
-
[HarmonyPrefix]
[HarmonyPatch(typeof(OWTime), nameof(OWTime.Pause))]
public static bool StopPausing(OWTime.PauseType pauseType)
@@ -40,4 +41,20 @@ internal class TimePatches : QSBPatch
[HarmonyPatch(typeof(SubmitActionSkipToNextLoop), nameof(SubmitActionSkipToNextLoop.AdvanceToNewTimeLoop))]
public static bool StopMeditation()
=> false;
-}
\ No newline at end of file
+}
+
+internal class ClientTimePatches : QSBPatch
+{
+ public override QSBPatchTypes Type => QSBPatchTypes.OnNonServerClientConnect;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TimeLoop), nameof(TimeLoop.SetSecondsRemaining))]
+ private static void SetSecondsRemaining(float secondsRemaining)
+ {
+ if (Remote)
+ {
+ return;
+ }
+ new SetSecondsRemainingMessage(secondsRemaining).Send();
+ }
+}
diff --git a/QSB/TimeSync/WakeUpSync.cs b/QSB/TimeSync/WakeUpSync.cs
index 29abf702..9b8ef589 100644
--- a/QSB/TimeSync/WakeUpSync.cs
+++ b/QSB/TimeSync/WakeUpSync.cs
@@ -5,6 +5,7 @@ using QSB.ClientServerStateSync.Messages;
using QSB.DeathSync;
using QSB.Inputs;
using QSB.Messaging;
+using QSB.Patches;
using QSB.Player;
using QSB.Player.Messages;
using QSB.TimeSync.Messages;
@@ -129,6 +130,7 @@ public class WakeUpSync : NetworkBehaviour
}
else
{
+ // dont bother sleeping, just wake up
if (!_hasWokenUp)
{
Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, WakeUp);
@@ -138,12 +140,13 @@ public class WakeUpSync : NetworkBehaviour
}
private void SendServerTime()
- => new ServerTimeMessage(_serverTime, PlayerData.LoadLoopCount()).Send();
+ => new ServerTimeMessage(_serverTime, PlayerData.LoadLoopCount(), TimeLoop.GetSecondsRemaining()).Send();
- public void OnClientReceiveMessage(float time, int count)
+ public void OnClientReceiveMessage(float time, int count, float secondsRemaining)
{
_serverTime = time;
_serverLoopCount = count;
+ QSBPatch.RemoteCall(() => TimeLoop.SetSecondsRemaining(secondsRemaining));
}
private void WakeUpOrSleep()
@@ -162,7 +165,7 @@ public class WakeUpSync : NetworkBehaviour
var myTime = Time.timeSinceLevelLoad;
var diff = myTime - _serverTime;
- if (ServerStateManager.Instance.GetServerState() is not ServerState.InSolarSystem and not ServerState.InEye)
+ if (ServerStateManager.Instance.GetServerState() is not (ServerState.InSolarSystem or ServerState.InEye))
{
return;
}
@@ -170,13 +173,19 @@ public class WakeUpSync : NetworkBehaviour
if (diff > PauseOrFastForwardThreshold)
{
StartPausing(PauseReason.TooFarAhead);
- return;
}
-
- if (diff < -PauseOrFastForwardThreshold)
+ else if (diff < -PauseOrFastForwardThreshold)
{
StartFastForwarding(FastForwardReason.TooFarBehind);
}
+ else
+ {
+ // should only happen from Init so we gotta wait
+ if (!_hasWokenUp)
+ {
+ Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, WakeUp);
+ }
+ }
}
private void StartFastForwarding(FastForwardReason reason)
@@ -294,7 +303,8 @@ public class WakeUpSync : NetworkBehaviour
if (CurrentState == State.Pausing && (PauseReason)CurrentReason == PauseReason.WaitingForAllPlayersToBeReady)
{
- if (clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem)
+ if ((clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem) ||
+ (clientState == ClientState.AliveInEye && serverState == ServerState.InEye))
{
ResetTimeScale();
}
@@ -390,7 +400,8 @@ public class WakeUpSync : NetworkBehaviour
if (CurrentState == State.Pausing && (PauseReason)CurrentReason == PauseReason.WaitingForAllPlayersToBeReady)
{
- if (clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem)
+ if ((clientState == ClientState.AliveInSolarSystem && serverState == ServerState.InSolarSystem) ||
+ (clientState == ClientState.AliveInEye && serverState == ServerState.InEye))
{
ResetTimeScale();
}
@@ -422,7 +433,7 @@ public class WakeUpSync : NetworkBehaviour
{
var diff = GetTimeDifference();
- if (diff is > PauseOrFastForwardThreshold or < (-PauseOrFastForwardThreshold))
+ if (diff is > PauseOrFastForwardThreshold or < -PauseOrFastForwardThreshold)
{
WakeUpOrSleep();
return;
diff --git a/QSB/Translations/en.json b/QSB/Translations/en.json
index 68314f46..e0605cfc 100644
--- a/QSB/Translations/en.json
+++ b/QSB/Translations/en.json
@@ -4,13 +4,16 @@
"MainMenuConnect": "CONNECT TO MULTIPLAYER",
"PauseMenuDisconnect": "DISCONNECT",
"PauseMenuStopHosting": "STOP HOSTING",
- "PublicIPAddress": "Public IP Address\n\n(YOUR SAVE DATA WILL BE OVERWRITTEN)",
- "ProductUserID": "Product User ID\n\n(YOUR SAVE DATA WILL BE OVERWRITTEN)",
+ "PublicIPAddress": "Public IP Address\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)",
+ "ProductUserID": "Product User ID\n\n(YOUR MULTIPLAYER SAVE DATA WILL BE OVERWRITTEN)",
"Connect": "CONNECT",
"Cancel": "CANCEL",
- "HostExistingOrNew": "Do you want to host an existing expedition, or host a new expedition?",
+ "HostExistingOrNewOrCopy": "Do you want to host an existing multiplayer expedition, host a new expedition, or copy the existing singleplayer expedition to multiplayer?",
+ "HostNewOrCopy": "Do you want to host a new expedition, or copy the existing singleplayer expedition to multiplayer?",
+ "HostExistingOrNew": "Do you want to host an existing multiplayer expedition, or host a new expedition?",
"ExistingSave": "EXISTING SAVE",
"NewSave": "NEW SAVE",
+ "CopySave": "COPY SAVE",
"DisconnectAreYouSure": "Are you sure you want to disconnect?\nThis will send you back to the main menu.",
"Yes": "YES",
"No": "NO",
@@ -36,6 +39,11 @@
"TimeSyncWaitForAllToReady": "Waiting for start of loop...",
"TimeSyncWaitForAllToDie": "Waiting for end of loop...",
"GalaxyMapEveryoneNotPresent": "It's not yet time. Everyone should be here to witness this.",
+ "YouAreDead": "You are dead.",
+ "WaitingForRespawn": "Waiting for someone to respawn you...",
+ "WaitingForAllToDie": "Waiting for {0} player(s) to die...",
+ "AttachToShip": "Attach to ship",
+ "DetachFromShip": "Detach from ship",
"DeathMessages": {
"Default": [
"{0} died",
diff --git a/QSB/Translations/fr.json b/QSB/Translations/fr.json
index 68ff74e6..f1fda671 100644
--- a/QSB/Translations/fr.json
+++ b/QSB/Translations/fr.json
@@ -4,13 +4,16 @@
"MainMenuConnect": "SE CONNECTER AU MULTIJOUEUR",
"PauseMenuDisconnect": "DÉCONNECTER",
"PauseMenuStopHosting": "ARRÊTEZ L'HÉBERGEMENT",
- "PublicIPAddress": "Adresse IP publique\n\n(CELA EFFACERA VOTRE PROGRESSION)",
- "ProductUserID": "ID utilisateur\n\n(CELA EFFACERA VOTRE PROGRESSION)",
+ "PublicIPAddress": "Adresse IP publique\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)",
+ "ProductUserID": "ID utilisateur\n\n(CELA EFFACERA VOTRE PROGRESSION MULTIJOUEUR)",
"Connect": "SE CONNECTER",
"Cancel": "ANNULER",
- "HostExistingOrNew": "Veux-tu héberger une expédition existante, ou héberger une nouvelle expédition?",
+ "HostExistingOrNewOrCopy": "Veux-tu héberger une expédition multijouer existante, héberger une nouvelle expédition, ou héberger une copie de ton expédition solo existante?",
+ "HostNewOrCopy": "Veux-tu héberger une nouvelle expédition, ou héberger une copie de ton expédition solo existante?",
+ "HostExistingOrNew": "Veux-tu héberger une expédition multijouer existante, ou héberger une nouvelle expédition?",
"ExistingSave": "SAUVEGARDE EXISTANTE",
"NewSave": "NOUVELLE SAUVEGARDE",
+ "CopySave": "COPIER SAUVEGARDE",
"DisconnectAreYouSure": "Veux-tu vraiment te déconnecter?\n Cela te ramènera au menu principal.",
"Yes": "OUI",
"No": "NON",
diff --git a/QSB/Translations/pt-br.json b/QSB/Translations/pt-br.json
new file mode 100644
index 00000000..7aa4aea3
--- /dev/null
+++ b/QSB/Translations/pt-br.json
@@ -0,0 +1,121 @@
+{
+ "Language": "PORTUGUESE_BR",
+ "MainMenuHost": "ABRIR PARA MULTIJOGADORES",
+ "MainMenuConnect": "CONECTAR PARA JOGO DE MULTIJOGADORES",
+ "PauseMenuDisconnect": "DESCONECTAR",
+ "PauseMenuStopHosting": "PARAR DE HOSPEDAR",
+ "PublicIPAddress": "Endereço de IP Público\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)",
+ "ProductUserID": "ID de Produto de Usuário\n\n(O DADOS DO SEU SAVE DE MULTIJOGADORES SERÃO SOBRESCRITOS)",
+ "Connect": "CONECTAR",
+ "Cancel": "CANCELAR",
+ "HostExistingOrNewOrCopy": "Você quer hospedar uma expedição de multijogadores pré-existente, hospedar uma nova expedição, ou copiar a já existente expedição do seu save para multiplos jogadores?",
+ "HostNewOrCopy": "Você quer hospedar uma nova expedição, ou copiar a já existente expedição do seu save para multiplos jogadores?",
+ "HostExistingOrNew": "Você quer hospedar uma expedição de multijogadores pré-existente, ou hospedar uma nova expedição?",
+ "ExistingSave": "SAVE PRÉ-EXISTENTE",
+ "NewSave": "NOVO SAVE",
+ "CopySave": "COPIAR SAVE",
+ "DisconnectAreYouSure": "Você tem certeza que quer desconectar?\nEssa ação irá de enviar de volta ao menu principal.",
+ "Yes": "SIM",
+ "No": "NÃO",
+ "StopHostingAreYouSure": "Você tem certeza que quer parar de hospedar\nEssa ação irá de desconectar os jogadores conectados e eviará todos de volta ao menu principal.",
+ "CopyProductUserIDToClipboard": "Hospedando servidor.\nOutros jogadores poderão se conectar usando o seu ID de produto de usuário, o qual é :\n{0}\nVocê quer copia-lo para a área de transferência?",
+ "Connecting": "CONECTANDO...",
+ "OK": "OK",
+ "ServerRefusedConnection": "Servidor recusou a conexão.\n{0}",
+ "ClientDisconnectWithError": "Cliente desconectou com um erro!\n{0}",
+ "QSBVersionMismatch": "As versão do QSB não correspondem. (Cliente:{0}, Servidor:{1})",
+ "OWVersionMismatch": "As versão de Outer Wilds não correspondem. (Cliente:{0}, Servidor:{1})",
+ "DLCMismatch": "O estado da instalação da DLC não correspondem. (Cliente:{0}, Servidor:{1})",
+ "GameProgressLimit": "O jogo progrediu além do limite.",
+ "AddonMismatch": "Incompatibilidade de Addons. (Cliente:{0} addons, Servidor:{1} addons)",
+ "IncompatibleMod": "Usando um mod incompativel ou não permitido. Primeiro mod encontrado foi {0}",
+ "PlayerJoinedTheGame": "{0} entrou!",
+ "PlayerWasKicked": "{0} foi expulso.",
+ "KickedFromServer": "Expulso do servidor. Motivo : {0}",
+ "RespawnPlayer": "Renascer Jogador",
+ "TimeSyncTooFarBehind": "{0}\nAvançando o tempo para corresponder com o tempo do servidor...",
+ "TimeSyncWaitingForStartOfServer": "Esperando para o servidor começar...",
+ "TimeSyncTooFarAhead": "{0}\nParando para corresponder com o tempo do servidor...",
+ "TimeSyncWaitForAllToReady": "Esperando para o começo do loop...",
+ "TimeSyncWaitForAllToDie": "Esperando para o fim do loop...",
+ "GalaxyMapEveryoneNotPresent": "Ainda não é a hora. Todos deveriam estar aqui para presenciar isso.",
+ "DeathMessages": {
+ "Default": [
+ "{0} morreu",
+ "{0} foi morto"
+ ],
+ "Impact": [
+ "{0} esqueceu de usar os retro-foguetes",
+ "{0} experienciou a inércia",
+ "{0} bateu muito forte no chão",
+ "{0} virou uma panqueca",
+ "{0} morreu no impacto",
+ "{0} impactou o chão muito forte",
+ "{0} morreu graças a brusca desaceleração"
+ ],
+ "Asphyxiation": [
+ "{0} esqueceu de respirar",
+ "{0} asfixiou",
+ "{0} moreu de asfixiação",
+ "{0} esqueceu de como respirar",
+ "{0} esqueceu de checar o seu oxigênio",
+ "{0} ficou sem ar",
+ "{0} ficou sem oxigênio",
+ "{0} não precisava de ar de qualquer maneira"
+ ],
+ "Energy": [
+ "{0} foi cozinhado"
+ ],
+ "Supernova": [
+ "{0} ficou sem tempo",
+ "{0} queimou",
+ "{0} foi cozinhado",
+ "{0} foi vaporizado",
+ "{0} perdeu a noção do tempo"
+ ],
+ "Digestion": [
+ "{0} foi comido",
+ "{0} encontrou um peixe",
+ "{0} encontrou uma horrenda criatura",
+ "{0} se meteu com o peixe errado",
+ "{0} foi digerido",
+ "{0} morreu pela digestão"
+ ],
+ "Crushed": [
+ "{0} foi esmagado",
+ "{0} foi amassado",
+ "{0} foi enterrado",
+ "{0} não saiu a tempo",
+ "{0} foi nadar na areia",
+ "{0} subestimou a areia",
+ "{0} ficou preso embaixo da areia"
+ ],
+ "Lava": [
+ "{0} morreu na lava",
+ "{0} foi derretido",
+ "{0} tentou nadar na lava",
+ "{0} caiu na lava",
+ "{0} morreu pela lava",
+ "{0} foi nadar na lava",
+ "{0} acabou queimado pela lava"
+ ],
+ "BlackHole": [
+ "{0} foi atrás de suas mamórias"
+ ],
+ "DreamExplosion": [
+ "{0} explodiu",
+ "{0} foi um dos primeiros a testar",
+ "{0} foi frito",
+ "{0} morreu graças a uma explosão",
+ "{0} usou o artefato errado"
+ ],
+ "CrushedByElevator": [
+ "{0} foi esmagado",
+ "{0} foi amassado",
+ "{0} foi esmagado por um elevador",
+ "{0} ficou embaixo de um elevador",
+ "{0} virou uma panqueca",
+ "{0} foi amassado por um elevador"
+ ]
+ }
+}
diff --git a/QSB/Translations/ru.json b/QSB/Translations/ru.json
index c1c457e5..c08d646b 100644
--- a/QSB/Translations/ru.json
+++ b/QSB/Translations/ru.json
@@ -4,13 +4,16 @@
"MainMenuConnect": "ПОДКЛЮЧИТЬСЯ К МУЛЬТИПЛЕЕРУ",
"PauseMenuDisconnect": "ОТКЛЮЧИТЬСЯ",
"PauseMenuStopHosting": "ПРЕКРАТИТЬ ХОСТИНГ",
- "PublicIPAddress": "Публичный IP-адрес\n\n(ВАШИ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)",
- "ProductUserID": "ID игрока\n\n(ВАШИ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)",
+ "PublicIPAddress": "Публичный IP-адрес\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)",
+ "ProductUserID": "ID игрока\n\n(ВАШИ МНОГОПОЛЬЗОВАТЕЛЬСКИЕ СОХРАНЁННЫЕ ДАННЫЕ БУДУТ ПЕРЕЗАПИСАНЫ)",
"Connect": "Подключиться",
"Cancel": "Отмена",
+ "HostExistingOrNewOrCopy": "Вы хотите продолжить существующую мультиплеерную экспедицию, начать новую мультиплеерную экспедицию, или скопировать прогресс из одиночной экспедиции?",
+ "HostNewOrCopy": "Вы хотите начать новую мультиплеерную экспедицию, или скопировать прогресс из одиночной экспедиции?",
"HostExistingOrNew": "Вы желаете хостить сервер на существующем сохранении, или создать новое?",
"ExistingSave": "СУЩЕСТВУЮЩЕЕ СОХРАНЕНИЕ",
"NewSave": "НОВОЕ СОХРАНЕНИЕ",
+ "CopySave": "СКОПИРОВАТЬ СОХРАНЕНИЕ",
"DisconnectAreYouSure": "Вы уверены, что хотите отсоедениться?\nЭто отправит вас обратно в главное меню.",
"Yes": "ДА",
"No": "НЕТ",
diff --git a/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs b/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs
index 5c371c79..bbf30193 100644
--- a/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs
+++ b/QSB/TriggerSync/Messages/TriggerInitialStateMessage.cs
@@ -17,12 +17,12 @@ public class TriggerInitialStateMessage : QSBWorldObjectMessage : WorldObject, IQSBTrigger
QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave;
}
+ private void OnPlayerLeave(PlayerInfo player) => Exit(player);
+
public override void SendInitialState(uint to) =>
((IQSBTrigger)this).SendMessage(new TriggerInitialStateMessage(Occupants) { To = to });
@@ -71,14 +73,6 @@ public abstract class QSBTrigger : WorldObject, IQSBTrigger
}
}
- private void OnPlayerLeave(PlayerInfo player)
- {
- if (Occupants.Contains(player))
- {
- Exit(player);
- }
- }
-
public void Enter(PlayerInfo player)
{
if (!Occupants.SafeAdd(player))
@@ -108,4 +102,4 @@ public abstract class QSBTrigger : WorldObject, IQSBTrigger
/// called when a player exits this trigger or leaves the game
///
protected virtual void OnExit(PlayerInfo player) { }
-}
\ No newline at end of file
+}
diff --git a/QSB/Utility/DebugActions.cs b/QSB/Utility/DebugActions.cs
index bbb9e4c7..66d1476a 100644
--- a/QSB/Utility/DebugActions.cs
+++ b/QSB/Utility/DebugActions.cs
@@ -1,10 +1,12 @@
-using QSB.ItemSync.WorldObjects.Items;
+using OWML.Common;
+using QSB.ItemSync.WorldObjects.Items;
using QSB.Messaging;
using QSB.Player;
using QSB.RespawnSync;
using QSB.ShipSync;
using QSB.Utility.Messages;
using QSB.WorldSync;
+using System;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
@@ -13,6 +15,8 @@ namespace QSB.Utility;
public class DebugActions : MonoBehaviour, IAddComponentOnStart
{
+ public static Type WorldObjectSelection;
+
private static void GoToVessel()
{
var spawnPoint = GameObject.Find("Spawn_Vessel").GetComponent();
@@ -38,6 +42,49 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart
private void Awake() => enabled = QSBCore.DebugSettings.DebugMode;
private int _otherPlayerToTeleportTo;
+ private int _backTimer;
+ private int _forwardTimer;
+
+ private const int UpdatesUntilScroll = 30;
+ private const int UpdatesBetweenScroll = 5;
+
+ private static void GoForwardOneObject()
+ {
+ var allWorldObjects = typeof(IWorldObject).GetDerivedTypes().ToArray();
+ if (WorldObjectSelection == null)
+ {
+ WorldObjectSelection = allWorldObjects.First();
+ return;
+ }
+
+ var index = Array.IndexOf(allWorldObjects, WorldObjectSelection) + 1;
+
+ if (index == allWorldObjects.Length)
+ {
+ index = 0;
+ }
+
+ WorldObjectSelection = allWorldObjects[index];
+ }
+
+ private static void GoBackOneObject()
+ {
+ var allWorldObjects = typeof(IWorldObject).GetDerivedTypes().ToArray();
+ if (WorldObjectSelection == null)
+ {
+ WorldObjectSelection = allWorldObjects.Last();
+ return;
+ }
+
+ var index = Array.IndexOf(allWorldObjects, WorldObjectSelection) - 1;
+
+ if (index < 0)
+ {
+ index = allWorldObjects.Length - 1;
+ }
+
+ WorldObjectSelection = allWorldObjects[index];
+ }
public void Update()
{
@@ -46,6 +93,57 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart
return;
}
+ if (Keyboard.current[Key.Comma].isPressed && Keyboard.current[Key.Period].isPressed)
+ {
+ WorldObjectSelection = null;
+ }
+ else if (Keyboard.current[Key.Comma].wasPressedThisFrame)
+ {
+ GoBackOneObject();
+ }
+ else if (Keyboard.current[Key.Period].wasPressedThisFrame)
+ {
+ GoForwardOneObject();
+ }
+ else
+ {
+ if (Keyboard.current[Key.Comma].isPressed)
+ {
+ _backTimer++;
+
+ if (_backTimer >= UpdatesUntilScroll)
+ {
+ if (_backTimer == UpdatesUntilScroll + UpdatesBetweenScroll)
+ {
+ _backTimer = UpdatesUntilScroll;
+ GoBackOneObject();
+ }
+ }
+ }
+ else
+ {
+ _backTimer = 0;
+ }
+
+ if (Keyboard.current[Key.Period].isPressed)
+ {
+ _forwardTimer++;
+
+ if (_forwardTimer >= UpdatesUntilScroll)
+ {
+ if (_forwardTimer == UpdatesUntilScroll + UpdatesBetweenScroll)
+ {
+ _forwardTimer = UpdatesUntilScroll;
+ GoForwardOneObject();
+ }
+ }
+ }
+ else
+ {
+ _forwardTimer = 0;
+ }
+ }
+
if (Keyboard.current[Key.Numpad1].wasPressedThisFrame)
{
var otherPlayers = QSBPlayerManager.PlayerList.Where(x => !x.IsLocalPlayer).ToList();
@@ -119,11 +217,17 @@ public class DebugActions : MonoBehaviour, IAddComponentOnStart
if (Keyboard.current[Key.Numpad7].wasPressedThisFrame)
{
GoToVessel();
+ InsertWarpCore();
}
if (Keyboard.current[Key.Numpad8].wasPressedThisFrame)
{
- InsertWarpCore();
+ var player = new PlayerInfo(QSBPlayerManager.LocalPlayer.TransformSync);
+ QSBPlayerManager.PlayerList.SafeAdd(player);
+ QSBPlayerManager.OnAddPlayer?.Invoke(player);
+ DebugLog.DebugWrite($"Create Player : {player}", MessageType.Info);
+
+ JoinLeaveSingularity.Create(player, true);
}
if (Keyboard.current[Key.Numpad9].wasPressedThisFrame)
diff --git a/QSB/Utility/DebugGUI.cs b/QSB/Utility/DebugGUI.cs
index 9740a88c..5d237c00 100644
--- a/QSB/Utility/DebugGUI.cs
+++ b/QSB/Utility/DebugGUI.cs
@@ -133,6 +133,8 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart
WriteLine(1, $"TimeLoop IsTimeFlowing : {TimeLoop.IsTimeFlowing()}");
WriteLine(1, $"TimeLoop IsTimeLoopEnabled : {TimeLoop.IsTimeLoopEnabled()}");
}
+
+ WriteLine(1, $"Selected WorldObject : {(DebugActions.WorldObjectSelection == null ? "All" : DebugActions.WorldObjectSelection.Name)}");
}
#endregion
@@ -142,23 +144,26 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart
WriteLine(2, "Player data :");
foreach (var player in QSBPlayerManager.PlayerList)
{
- WriteLine(2, player.ToString());
+ WriteLine(2, player.ToString(), Color.cyan);
WriteLine(2, $"State : {player.State}");
WriteLine(2, $"Eye State : {player.EyeState}");
WriteLine(2, $"Dead : {player.IsDead}");
- WriteLine(2, $"Visible : {player.Visible}");
WriteLine(2, $"Ready : {player.IsReady}");
WriteLine(2, $"Suited Up : {player.SuitedUp}");
+ WriteLine(2, $"In Suited Up State : {player.AnimationSync.InSuitedUpState}");
WriteLine(2, $"InDreamWorld : {player.InDreamWorld}");
-
if (player.IsReady && QSBWorldSync.AllObjectsReady)
{
WriteLine(2, $"Illuminated : {player.LightSensor.IsIlluminated()}");
var singleLightSensor = (SingleLightSensor)player.LightSensor;
- foreach (var item in singleLightSensor._lightSources)
+ // will be null for remote player light sensors
+ if (singleLightSensor._lightSources != null)
{
- WriteLine(2, $"- {item.GetLightSourceType()}");
+ foreach (var item in singleLightSensor._lightSources)
+ {
+ WriteLine(2, $"- {item.GetLightSourceType()}");
+ }
}
var networkTransform = player.TransformSync;
@@ -258,7 +263,7 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart
}
else
{
- WriteLine(4, $"- LANTERN NULL", Color.red);
+ WriteLine(4, "- LANTERN NULL", Color.red);
}
var playerCamera = player.player.Camera;
@@ -271,7 +276,7 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart
}
else
{
- WriteLine(4, $"- CAMERA NULL", Color.red);
+ WriteLine(4, "- CAMERA NULL", Color.red);
}
}
}
@@ -323,7 +328,11 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart
{
if (QSBCore.DebugSettings.DrawLabels)
{
- foreach (var obj in QSBWorldSync.GetWorldObjects())
+ var list = DebugActions.WorldObjectSelection == null
+ ? QSBWorldSync.GetWorldObjects()
+ : QSBWorldSync.GetWorldObjects(DebugActions.WorldObjectSelection);
+
+ foreach (var obj in list)
{
if (obj.ShouldDisplayDebug())
{
@@ -349,7 +358,11 @@ internal class DebugGUI : MonoBehaviour, IAddComponentOnStart
{
if (QSBCore.DebugSettings.DrawLines)
{
- foreach (var obj in QSBWorldSync.GetWorldObjects())
+ var list = DebugActions.WorldObjectSelection == null
+ ? QSBWorldSync.GetWorldObjects()
+ : QSBWorldSync.GetWorldObjects(DebugActions.WorldObjectSelection);
+
+ foreach (var obj in list)
{
if (obj.ShouldDisplayDebug())
{
diff --git a/QSB/Utility/DebugLog.cs b/QSB/Utility/DebugLog.cs
index 2c5351aa..5f01ec8e 100644
--- a/QSB/Utility/DebugLog.cs
+++ b/QSB/Utility/DebugLog.cs
@@ -1,9 +1,9 @@
using OWML.Common;
using OWML.Logging;
-using QSB.WorldSync;
using System.Diagnostics;
using System.Linq;
-using UnityEngine;
+using System.Reflection;
+using System.Runtime.CompilerServices;
namespace QSB.Utility;
@@ -19,7 +19,7 @@ public static class DebugLog
message = $"[{ProcessInstanceId}] " + message;
}
- QSBCore.Helper.Console.WriteLine(message, type, GetCallingType(new StackTrace()));
+ QSBCore.Helper.Console.WriteLine(message, type, GetCallingType());
}
public static void ToHud(string message)
@@ -44,7 +44,7 @@ public static class DebugLog
if (QSBCore.Helper == null)
{
// yes i know this is only meant for OWML, but it's useful as a backup
- ModConsole.OwmlConsole.WriteLine(message, type, GetCallingType(new StackTrace()));
+ ModConsole.OwmlConsole.WriteLine(message, type, GetCallingType());
return;
}
@@ -54,8 +54,10 @@ public static class DebugLog
}
}
- private static string GetCallingType(StackTrace frame) =>
- frame.GetFrames()!
- .Select(x => x.GetMethod().DeclaringType!.Name)
- .First(x => x != nameof(DebugLog));
+ private static string GetCallingType() =>
+ new StackTrace(2) // skip this function and calling function
+ .GetFrames()!
+ .Select(x => x.GetMethod().DeclaringType!)
+ .First(x => x != typeof(DebugLog) && !x.IsDefined(typeof(CompilerGeneratedAttribute)))
+ .Name;
}
diff --git a/QSB/Utility/DeterministicManager.cs b/QSB/Utility/DeterministicManager.cs
index f190d585..c1982a1d 100644
--- a/QSB/Utility/DeterministicManager.cs
+++ b/QSB/Utility/DeterministicManager.cs
@@ -8,6 +8,9 @@ using UnityEngine;
namespace QSB.Utility;
+///
+/// TODO make this only do cache clearing on pre scene load when HOSTING instead of just all the time
+///
public static class DeterministicManager
{
private static readonly Harmony _harmony = new(typeof(DeterministicManager).FullName);
diff --git a/QSB/Utility/LinkedWorldObject/Extensions.cs b/QSB/Utility/LinkedWorldObject/ILinkedWorldObject_Extensions.cs
similarity index 64%
rename from QSB/Utility/LinkedWorldObject/Extensions.cs
rename to QSB/Utility/LinkedWorldObject/ILinkedWorldObject_Extensions.cs
index 97b0b69d..c3ab2893 100644
--- a/QSB/Utility/LinkedWorldObject/Extensions.cs
+++ b/QSB/Utility/LinkedWorldObject/ILinkedWorldObject_Extensions.cs
@@ -5,27 +5,27 @@ using UnityEngine;
namespace QSB.Utility.LinkedWorldObject;
-public static class Extensions
+public static class ILinkedWorldObject_Extensions
{
///
/// link a world object and a network behaviour
///
- public static void LinkTo(this ILinkedWorldObject worldObject, ILinkedNetworkBehaviour networkBehaviour)
+ public static void LinkTo(this ILinkedWorldObject @this, ILinkedNetworkBehaviour networkBehaviour)
{
- worldObject.SetNetworkBehaviour((NetworkBehaviour)networkBehaviour);
- networkBehaviour.SetWorldObject(worldObject);
+ @this.SetNetworkBehaviour((NetworkBehaviour)networkBehaviour);
+ networkBehaviour.SetWorldObject(@this);
}
///
/// link a world object and network object, then spawn it.
/// (host only)
///
- public static void SpawnLinked(this ILinkedWorldObject worldObject, GameObject prefab, bool spawnWithServerAuthority)
+ public static void SpawnLinked(this ILinkedWorldObject @this, GameObject prefab, bool spawnWithServerAuthority)
{
var go = Object.Instantiate(prefab);
var networkBehaviour = go.GetComponent();
- worldObject.LinkTo(networkBehaviour);
+ @this.LinkTo(networkBehaviour);
if (spawnWithServerAuthority)
{
@@ -41,6 +41,6 @@ public static class Extensions
/// wait for a world object to be linked.
/// (non host only)
///
- public static async UniTask WaitForLink(this ILinkedWorldObject worldObject, CancellationToken ct) =>
- await UniTask.WaitUntil(() => worldObject.NetworkBehaviour, cancellationToken: ct);
+ public static async UniTask WaitForLink(this ILinkedWorldObject @this, CancellationToken ct) =>
+ await UniTask.WaitUntil(() => @this.NetworkBehaviour, cancellationToken: ct);
}
diff --git a/QSB/Utility/Messages/DebugChangeSceneMessage.cs b/QSB/Utility/Messages/DebugChangeSceneMessage.cs
index bd0d2aca..4a70571c 100644
--- a/QSB/Utility/Messages/DebugChangeSceneMessage.cs
+++ b/QSB/Utility/Messages/DebugChangeSceneMessage.cs
@@ -12,8 +12,7 @@ public class DebugChangeSceneMessage : QSBMessage
{
if (Data)
{
- PlayerData._currentGameSave.warpedToTheEye = false;
- PlayerData.SaveCurrentGame();
+ PlayerData.SaveEyeCompletion();
LoadManager.LoadSceneAsync(OWScene.SolarSystem, true, LoadManager.FadeType.ToBlack);
}
else
diff --git a/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs b/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs
index 121965e1..1dbba547 100644
--- a/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs
+++ b/QSB/Utility/Messages/DebugTriggerSupernovaMessage.cs
@@ -1,9 +1,17 @@
using QSB.Messaging;
+using QSB.Patches;
namespace QSB.Utility.Messages;
public class DebugTriggerSupernovaMessage : QSBMessage
{
public override void OnReceiveLocal() => OnReceiveRemote();
- public override void OnReceiveRemote() => TimeLoop.SetSecondsRemaining(0);
-}
\ No newline at end of file
+
+ public override void OnReceiveRemote()
+ {
+ PlayerData.SaveLoopCount(2);
+ TimeLoop.SetTimeLoopEnabled(true);
+ TimeLoop._isTimeFlowing = true;
+ QSBPatch.RemoteCall(() => TimeLoop.SetSecondsRemaining(0));
+ }
+}
diff --git a/QSB/WorldSync/QSBOWRigidbody.cs b/QSB/WorldSync/QSBOWRigidbody.cs
index 9d35cb48..323d342b 100644
--- a/QSB/WorldSync/QSBOWRigidbody.cs
+++ b/QSB/WorldSync/QSBOWRigidbody.cs
@@ -5,7 +5,5 @@
///
internal class QSBOWRigidbody : WorldObject
{
- public override void SendInitialState(uint to) { }
-
public override bool ShouldDisplayDebug() => false;
}
diff --git a/QSB/WorldSync/QSBWorldSync.cs b/QSB/WorldSync/QSBWorldSync.cs
index 5d93be2d..5d788f70 100644
--- a/QSB/WorldSync/QSBWorldSync.cs
+++ b/QSB/WorldSync/QSBWorldSync.cs
@@ -187,7 +187,7 @@ public static class QSBWorldSync
if (QSBCore.IsInMultiplayer && loadScene.IsUniverseScene())
{
// So objects have time to be deleted, made, whatever
- // I.E. wait until Start has been called
+ // i.e. wait until Start has been called
Delay.RunNextFrame(() => BuildWorldObjects(loadScene).Forget());
}
};
@@ -233,6 +233,9 @@ public static class QSBWorldSync
where TWorldObject : IWorldObject
=> WorldObjects.OfType();
+ public static IEnumerable GetWorldObjects(Type type)
+ => WorldObjects.Where(type.IsInstanceOfType);
+
public static TWorldObject GetWorldObject(this int objectId)
where TWorldObject : IWorldObject
{
diff --git a/QSB/WorldSync/WorldObject.cs b/QSB/WorldSync/WorldObject.cs
index a9a324a3..b01aa699 100644
--- a/QSB/WorldSync/WorldObject.cs
+++ b/QSB/WorldSync/WorldObject.cs
@@ -19,5 +19,5 @@ public abstract class WorldObject : IWorldObject
public virtual string ReturnLabel() => ToString();
public virtual void DisplayLines() { }
- public abstract void SendInitialState(uint to);
+ public virtual void SendInitialState(uint to) { }
}
\ No newline at end of file
diff --git a/QSB/manifest.json b/QSB/manifest.json
index 6de3460a..256b8626 100644
--- a/QSB/manifest.json
+++ b/QSB/manifest.json
@@ -7,7 +7,7 @@
"body": "- Disable *all* other mods. (Can heavily affect performance)\n- Make sure you are not running any other network-intensive applications."
},
"uniqueName": "Raicuparta.QuantumSpaceBuddies",
- "version": "0.20.2",
+ "version": "0.21.0",
"owmlVersion": "2.5.2",
"dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ],
"pathsToPreserve": [ "debugsettings.json", "storage.json" ]
diff --git a/README.md b/README.md
index 4611bf02..05d88efe 100644
--- a/README.md
+++ b/README.md
@@ -181,10 +181,10 @@ The template for this file is this :
### Contributers
-- [ShoosGun](https://github.com/ShoosGun)
- [Chris Yeninas](https://github.com/PhantomGamers) - Help with project files and GitHub workflows.
- [Tlya](https://github.com/Tllya) - Russian translation.
- [Xen](https://github.com/xen-42) - French translation.
+- [ShoosGun](https://github.com/ShoosGun) - Portuguese translation.
### Special Thanks
- Thanks to Logan Ver Hoef for help with the game code, and for helping make the damn game in the first place.
diff --git a/TRANSLATING.md b/TRANSLATING.md
index d8a4b7fb..bd1f8c92 100644
--- a/TRANSLATING.md
+++ b/TRANSLATING.md
@@ -8,13 +8,13 @@ QSB can only be translated to the languages Outer Wilds supports - so if you don
- English
- French
- Russian
+- Portuguese (Brazil)
### Un-translated languages :
- Spanish (Latin American)
- German
- Italian
- Polish
-- Portuguese (Brazil)
- Japanese
- Chinese (Simplified)
- Korean