Merge pull request #416 from misternebula/stuff

Audio Sync and bug fixes
This commit is contained in:
_nebula 2021-12-15 21:13:30 +00:00 committed by GitHub
commit 23e91b2082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 308 additions and 145 deletions

View File

@ -127,5 +127,21 @@ namespace QSB.Animation.Patches
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");
var playerAnimationSync = QSBPlayerManager.LocalPlayer.AnimationSync;
QSBEventManager.FireEvent(EventNames.QSBAnimTrigger, playerAnimationSync.AttachedNetId, "Jump");
return false;
}
}
}

View File

@ -0,0 +1,17 @@
using UnityEngine;
namespace QSB.Audio
{
internal class PlayerAudioManager
{
public static QSBPlayerAudioController InitRemote(Transform playerBody)
{
var REMOTE_Audio_Player = new GameObject("REMOTE_Audio_Player");
REMOTE_Audio_Player.transform.parent = playerBody;
REMOTE_Audio_Player.transform.localPosition = Vector3.zero;
REMOTE_Audio_Player.transform.localScale = Vector3.one;
return REMOTE_Audio_Player.AddComponent<QSBPlayerAudioController>();
}
}
}

View File

@ -0,0 +1,9 @@
namespace QSB.Audio
{
internal class QSBJetpackThrusterAudio : QSBThrusterAudio
{
public OWAudioSource _underwaterSource;
public OWAudioSource _oxygenSource;
public OWAudioSource _boostSource;
}
}

View File

@ -0,0 +1,72 @@
using UnityEngine;
namespace QSB.Audio
{
public class QSBPlayerAudioController : MonoBehaviour
{
public OWAudioSource _oneShotExternalSource;
public OWAudioSource _repairToolSource;
private void Start()
{
_oneShotExternalSource = CreateBaseAudio(transform, "OneShotAudio_PlayerExternal", false, 0, 1, AudioType.None, OWAudioMixer.TrackName.Player_External, false);
_repairToolSource = CreateBaseAudio(transform, "RepairToolAudio", true, 128, 0.5f, AudioType.None, OWAudioMixer.TrackName.Player_External, false);
var thrusterAudio = new GameObject("REMOTE_ThrusterAudio").transform;
thrusterAudio.parent = transform;
var jetpatchThrusterAudio = thrusterAudio.gameObject.AddComponent<QSBJetpackThrusterAudio>();
jetpatchThrusterAudio._rotationalSource = CreateBaseAudio(thrusterAudio, "RotationalSource", false, 0, 1, AudioType.None, OWAudioMixer.TrackName.Player, false);
jetpatchThrusterAudio._translationalSource = CreateBaseAudio(thrusterAudio, "TranslationalSource", true, 0, 0.1f, AudioType.PlayerSuitJetpackThrustTranslational_LP, OWAudioMixer.TrackName.Player_External, false);
jetpatchThrusterAudio._underwaterSource = CreateBaseAudio(thrusterAudio, "UnderwaterSource", true, 0, 0.1f, AudioType.PlayerSuitJetpackThrustUnderwater_LP, OWAudioMixer.TrackName.Player_External, false);
jetpatchThrusterAudio._oxygenSource = CreateBaseAudio(thrusterAudio, "OxygenPropellantSource", true, 0, 0.2f, AudioType.PlayerSuitJetpackOxygenPropellant_LP, OWAudioMixer.TrackName.Player_External, false);
jetpatchThrusterAudio._boostSource = CreateBaseAudio(thrusterAudio, "BoosterSource", true, 0, 0.35f, AudioType.PlayerSuitJetpackBoost, OWAudioMixer.TrackName.Player_External, false);
}
public void PlayEquipTool()
=> _oneShotExternalSource.PlayOneShot(AudioType.ToolTranslatorEquip, 1f);
public void PlayUnequipTool()
=> _oneShotExternalSource.PlayOneShot(AudioType.ToolTranslatorUnequip, 1f);
public void PlayTurnOnFlashlight()
=> _oneShotExternalSource.PlayOneShot(AudioType.ToolFlashlightOn, 1f);
public void PlayTurnOffFlashlight()
=> _oneShotExternalSource.PlayOneShot(AudioType.ToolFlashlightOff, 1f);
private OWAudioSource CreateBaseAudio(
Transform parent,
string name,
bool loop,
int priority,
float volume,
AudioType audioLibraryClip,
OWAudioMixer.TrackName track,
bool randomize)
{
var go = new GameObject(name);
go.transform.parent = parent;
go.transform.localPosition = Vector3.zero;
go.transform.localScale = Vector3.one;
var audioSource = go.AddComponent<AudioSource>();
audioSource.mute = false;
audioSource.bypassEffects = false;
audioSource.bypassListenerEffects = false;
audioSource.bypassReverbZones = false;
audioSource.playOnAwake = false;
audioSource.loop = loop;
audioSource.priority = priority;
audioSource.volume = volume;
audioSource.spatialBlend = 1f;
var owAudioSource = go.AddComponent<OWAudioSource>();
owAudioSource._audioLibraryClip = audioLibraryClip;
owAudioSource._clipSelectionOnPlay = OWAudioSource.ClipSelectionOnPlay.RANDOM;
owAudioSource._track = track;
owAudioSource._randomizePlayheadOnAwake = randomize;
return owAudioSource;
}
}
}

View File

@ -0,0 +1,10 @@
using UnityEngine;
namespace QSB.Audio
{
internal class QSBThrusterAudio : MonoBehaviour
{
public OWAudioSource _translationalSource;
public OWAudioSource _rotationalSource;
}
}

View File

@ -1,6 +1,7 @@
using OWML.Common;
using QSB.Animation.Player;
using QSB.Animation.Player.Thrusters;
using QSB.Audio;
using QSB.CampfireSync.WorldObjects;
using QSB.ClientServerStateSync;
using QSB.Events;
@ -130,6 +131,7 @@ namespace QSB.Player
public bool SignalscopeEquipped { get; set; }
public bool TranslatorEquipped { get; set; }
public bool ProbeActive { get; set; }
public QSBPlayerAudioController AudioController { get; set; }
// Local only
public PlayerProbeLauncher LocalProbeLauncher

View File

@ -1,5 +1,6 @@
using OWML.Utils;
using QSB.Animation.Player;
using QSB.Audio;
using QSB.Events;
using QSB.Instruments;
using QSB.RoastingSync;
@ -173,12 +174,12 @@ namespace QSB.Player.TransformSync
REMOTE_Player_Body.AddComponent<PlayerMapMarker>().PlayerName = Player.Name;
Player.AudioController = PlayerAudioManager.InitRemote(REMOTE_Player_Body.transform);
/*
* SET UP PLAYER CAMERA
*/
PlayerToolsManager.InitRemote(REMOTE_PlayerCamera.transform);
var camera = REMOTE_PlayerCamera.AddComponent<Camera>();
camera.enabled = false;
var owcamera = REMOTE_PlayerCamera.AddComponent<OWCamera>();
@ -189,6 +190,8 @@ namespace QSB.Player.TransformSync
Player.CameraBody = REMOTE_PlayerCamera;
_visibleCameraRoot = REMOTE_PlayerCamera.transform;
PlayerToolsManager.InitRemote(Player);
/*
* SET UP ROASTING STICK
*/

View File

@ -38,6 +38,7 @@ using UnityEngine;
/*
Copyright (C) 2020 - 2021
Henry Pointer (_nebula / misternebula),
John Corby (JohnCorby),
Aleksander Waage (AmazingAlek),
Ricardo Lopes (Raicuparta)

View File

@ -302,17 +302,19 @@ namespace QSB.Syncs
}
/* Red Cube = Where visible object should be
* Green/Yellow Cube = Where visible object is
* Green cube = Where visible object is
* Magenta cube = Reference transform
* Red Line = Connection between Red Cube and Green/Yellow Cube
* Cyan Line = Connection between Green/Yellow cube and reference transform
* Red Line = Connection between Red Cube and Green Cube
* Cyan Line = Connection between Green cube and reference transform
*/
Popcron.Gizmos.Cube(ReferenceTransform.DecodePos(transform.position), ReferenceTransform.DecodeRot(transform.rotation), Vector3.one / 4, Color.red);
Popcron.Gizmos.Cube(ReferenceTransform.DecodePos(transform.position), ReferenceTransform.DecodeRot(transform.rotation), Vector3.one / 8, Color.red);
Popcron.Gizmos.Line(ReferenceTransform.DecodePos(transform.position), AttachedObject.transform.position, Color.red);
var color = HasMoved() ? Color.green : Color.yellow;
Popcron.Gizmos.Cube(AttachedObject.transform.position, AttachedObject.transform.rotation, Vector3.one / 4, color);
Popcron.Gizmos.Cube(ReferenceTransform.position, ReferenceTransform.rotation, Vector3.one / 4, Color.magenta);
Popcron.Gizmos.Cube(AttachedObject.transform.position, AttachedObject.transform.rotation, Vector3.one / 6, Color.green);
Popcron.Gizmos.Cube(ReferenceTransform.position, ReferenceTransform.rotation, Vector3.one / 8, Color.magenta);
Popcron.Gizmos.Line(AttachedObject.transform.position, ReferenceTransform.position, Color.cyan);
}
}

View File

@ -1,4 +1,5 @@
using UnityEngine;
using QSB.Player;
using UnityEngine;
namespace QSB.Tools.FlashlightTool
{
@ -6,7 +7,7 @@ namespace QSB.Tools.FlashlightTool
{
private static readonly Vector3 FlashlightOffset = new(0.7196316f, -0.2697681f, 0.3769455f);
internal static void CreateFlashlight(Transform cameraBody)
internal static void CreateFlashlight(PlayerInfo player)
{
var flashlightRoot = Object.Instantiate(GameObject.Find("FlashlightRoot"));
flashlightRoot.name = "REMOTE_FlashlightRoot";
@ -14,10 +15,12 @@ namespace QSB.Tools.FlashlightTool
var oldComponent = flashlightRoot.GetComponent<Flashlight>();
var component = flashlightRoot.AddComponent<QSBFlashlight>();
component.Player = player;
component.Init(oldComponent);
oldComponent.enabled = false;
flashlightRoot.transform.parent = cameraBody;
flashlightRoot.transform.parent = player.CameraBody.transform;
flashlightRoot.transform.localPosition = FlashlightOffset;
flashlightRoot.SetActive(true);
}

View File

@ -1,4 +1,5 @@
using UnityEngine;
using QSB.Player;
using UnityEngine;
namespace QSB.Tools.FlashlightTool
{
@ -14,6 +15,7 @@ namespace QSB.Tools.FlashlightTool
private LightSourceVolume _lightSourceVolume;
public bool FlashlightOn;
public PlayerInfo Player;
public void Start()
{
@ -73,6 +75,7 @@ namespace QSB.Tools.FlashlightTool
}
FlashlightOn = true;
Player.AudioController.PlayTurnOnFlashlight();
var rotation = _root.rotation;
_basePivot.rotation = rotation;
_baseRotation = rotation;
@ -93,6 +96,7 @@ namespace QSB.Tools.FlashlightTool
}
FlashlightOn = false;
Player.AudioController.PlayTurnOffFlashlight();
_lightSourceVolume.SetVolumeActivation(FlashlightOn);
}

View File

@ -1,4 +1,5 @@
using QSB.Tools.FlashlightTool;
using QSB.Player;
using QSB.Tools.FlashlightTool;
using QSB.Tools.ProbeLauncherTool;
using QSB.Tools.SignalscopeTool;
using QSB.Tools.TranslatorTool;
@ -19,11 +20,11 @@ namespace QSB.Tools
public static Material Props_HEA_Lightbulb_OFF_mat;
public static Material Structure_HEA_PlayerShip_Screens_mat;
public static void InitRemote(Transform playerCamera)
public static void InitRemote(PlayerInfo player)
{
try
{
CreateStowTransforms(playerCamera);
CreateStowTransforms(player.CameraBody.transform);
Props_HEA_PlayerTool_mat = GameObject.Find("Props_HEA_ProbeLauncher_ProbeCamera/ProbeLauncherChassis").GetComponent<MeshRenderer>().materials[0];
Props_HEA_Lightbulb_OFF_mat = GameObject.Find("Props_HEA_Probe_Prelaunch").GetComponent<MeshRenderer>().materials[1];
@ -48,10 +49,10 @@ namespace QSB.Tools
DebugLog.ToConsole($"Error when trying to find materials : {ex}", OWML.Common.MessageType.Error);
}
FlashlightCreator.CreateFlashlight(playerCamera);
SignalscopeCreator.CreateSignalscope(playerCamera);
ProbeLauncherCreator.CreateProbeLauncher(playerCamera);
TranslatorCreator.CreateTranslator(playerCamera);
FlashlightCreator.CreateFlashlight(player);
SignalscopeCreator.CreateSignalscope(player);
ProbeLauncherCreator.CreateProbeLauncher(player);
TranslatorCreator.CreateTranslator(player);
}
public static void InitLocal()

View File

@ -3,6 +3,7 @@ using QSB.Events;
using QSB.Patches;
using QSB.Player;
using QSB.Tools.ProbeLauncherTool.WorldObjects;
using QSB.Utility;
using QSB.WorldSync;
using UnityEngine;
@ -67,23 +68,31 @@ namespace QSB.Tools.ProbeLauncherTool.Patches
return false;
}
// BUG : This plays the sound to everyone
// TODO : ehhhh idk about this. maybe copy each sound source so we have a 2d version (for local) and a 3d version (for remote)?
// this would probably be a whole qsb version on it's own
[HarmonyPrefix]
[HarmonyPatch(typeof(ProbeLauncherEffects), nameof(ProbeLauncherEffects.PlayRetrievalClip))]
public static bool ProbeLauncherEffects_PlayRetrievalClip(OWAudioSource ____owAudioSource)
public static bool ProbeLauncherEffects_PlayRetrievalClip(ProbeLauncherEffects __instance)
{
____owAudioSource.GetAudioSource().spatialBlend = 1f;
if (__instance._owAudioSource == null)
{
DebugLog.ToConsole($"Error - _owAudioSource of {__instance._owAudioSource}", OWML.Common.MessageType.Error);
return true;
}
__instance._owAudioSource.GetAudioSource().spatialBlend = 1f;
return true;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ProbeLauncherEffects), nameof(ProbeLauncherEffects.PlayLaunchClip))]
public static bool ProbeLauncherEffects_PlayLaunchClip(OWAudioSource ____owAudioSource)
public static bool ProbeLauncherEffects_PlayLaunchClip(ProbeLauncherEffects __instance)
{
____owAudioSource.GetAudioSource().spatialBlend = 1f;
if (__instance._owAudioSource == null)
{
DebugLog.ToConsole($"Error - _owAudioSource of {__instance._owAudioSource}", OWML.Common.MessageType.Error);
return true;
}
__instance._owAudioSource.GetAudioSource().spatialBlend = 1f;
return true;
}
}

View File

@ -1,4 +1,5 @@
using UnityEngine;
using QSB.Player;
using UnityEngine;
using UnityEngine.Rendering;
namespace QSB.Tools.ProbeLauncherTool
@ -7,7 +8,7 @@ namespace QSB.Tools.ProbeLauncherTool
{
private static readonly Vector3 ProbeLauncherOffset = new(0.5745087f, -0.26f, 0.4453125f);
internal static void CreateProbeLauncher(Transform cameraBody)
internal static void CreateProbeLauncher(PlayerInfo player)
{
var ProbeLauncher = GameObject.Find("PlayerCamera/ProbeLauncher");
@ -29,7 +30,7 @@ namespace QSB.Tools.ProbeLauncherTool
var effects = REMOTE_ProbeLauncher.AddComponent<ProbeLauncherEffects>();
effects._launchParticles = REMOTE_LaunchParticleEffect.GetComponent<ParticleSystem>();
effects._underwaterLaunchParticles = REMOTE_LaunchParticleEffect_Underwater.GetComponent<ParticleSystem>();
effects._owAudioSource = ProbeLauncher.GetComponent<ProbeLauncherEffects>()._owAudioSource;
effects._owAudioSource = player.AudioController._repairToolSource;
var recallEffect = REMOTE_Props_HEA_ProbeLauncher.Find("RecallEffect");
@ -71,11 +72,12 @@ namespace QSB.Tools.ProbeLauncherTool
tool.ArrivalDegrees = 5f;
tool.Type = ToolType.ProbeLauncher;
tool.ToolGameObject = REMOTE_Props_HEA_ProbeLauncher.gameObject;
tool.Player = player;
tool.PreLaunchProbeProxy = preLaunchProbe.gameObject;
tool.ProbeRetrievalEffect = recallEffect.GetComponent<SingularityWarpEffect>();
tool.Effects = effects;
REMOTE_ProbeLauncher.transform.parent = cameraBody;
REMOTE_ProbeLauncher.transform.parent = player.CameraBody.transform;
REMOTE_ProbeLauncher.transform.localPosition = ProbeLauncherOffset;
//QSBCore.UnityEvents.FireInNUpdates(() => REMOTE_ProbeLauncher.SetActive(true), 5);

View File

@ -6,6 +6,6 @@ namespace QSB.Tools.ProbeLauncherTool
internal class ProbeLauncherManager : WorldObjectManager
{
protected override void RebuildWorldObjects(OWScene scene)
=> QSBWorldSync.Init<QSBProbeLauncher, ProbeLauncher>();
=> QSBWorldSync.Init<QSBProbeLauncher, ProbeLauncher>(typeof(PlayerProbeLauncher));
}
}

View File

@ -1,4 +1,5 @@
using UnityEngine;
using QSB.Utility;
using UnityEngine;
namespace QSB.Tools.ProbeLauncherTool
{
@ -10,6 +11,11 @@ namespace QSB.Tools.ProbeLauncherTool
public void RetrieveProbe(bool playEffects)
{
if (Effects._owAudioSource == null)
{
Effects._owAudioSource = Player.AudioController._repairToolSource;
}
PreLaunchProbeProxy.SetActive(true);
if (playEffects)
{
@ -22,9 +28,13 @@ namespace QSB.Tools.ProbeLauncherTool
{
PreLaunchProbeProxy.SetActive(false);
if (Effects._owAudioSource == null)
{
Effects._owAudioSource = Player.AudioController._repairToolSource;
}
// TODO : make this do underwater stuff correctly
Effects.PlayLaunchClip(false);
// BUG : this plays particles on everyone's launcher...
Effects.PlayLaunchParticles(false);
}
}

View File

@ -1,9 +1,11 @@
using UnityEngine;
using QSB.Player;
using UnityEngine;
namespace QSB.Tools
{
public class QSBTool : PlayerTool
{
public PlayerInfo Player { get; set; }
public ToolType Type { get; set; }
public GameObject ToolGameObject { get; set; }
@ -44,5 +46,17 @@ namespace QSB.Tools
UnequipTool();
}
public override void EquipTool()
{
base.EquipTool();
Player.AudioController.PlayEquipTool();
}
public override void UnequipTool()
{
base.UnequipTool();
Player.AudioController.PlayUnequipTool();
}
}
}

View File

@ -1,4 +1,5 @@
using UnityEngine;
using QSB.Player;
using UnityEngine;
using UnityEngine.Rendering;
namespace QSB.Tools.SignalscopeTool
@ -7,7 +8,7 @@ namespace QSB.Tools.SignalscopeTool
{
private static readonly Vector3 SignalscopeScale = new(1.5f, 1.5f, 1.5f);
internal static void CreateSignalscope(Transform cameraBody)
internal static void CreateSignalscope(PlayerInfo player)
{
var signalscopeRoot = Object.Instantiate(GameObject.Find("Signalscope"));
signalscopeRoot.name = "REMOTE_Signalscope";
@ -26,12 +27,13 @@ namespace QSB.Tools.SignalscopeTool
tool.ArrivalDegrees = 5f;
tool.Type = ToolType.Signalscope;
tool.ToolGameObject = Props_HEA_Signalscope.gameObject;
tool.Player = player;
oldSignalscope.enabled = false;
Props_HEA_Signalscope.GetComponent<MeshRenderer>().material = PlayerToolsManager.Props_HEA_PlayerTool_mat;
Props_HEA_Signalscope.GetComponent<MeshRenderer>().shadowCastingMode = ShadowCastingMode.On;
signalscopeRoot.transform.parent = cameraBody;
signalscopeRoot.transform.parent = player.CameraBody.transform;
signalscopeRoot.transform.localPosition = Vector3.zero;
signalscopeRoot.transform.localScale = SignalscopeScale;
signalscopeRoot.SetActive(true);

View File

@ -1,4 +1,5 @@
using QSB.Utility;
using QSB.Player;
using QSB.Utility;
using UnityEngine;
using UnityEngine.Rendering;
@ -8,7 +9,7 @@ namespace QSB.Tools.TranslatorTool
{
private static readonly Vector3 TranslatorScale = new(0.75f, 0.75f, 0.75f);
internal static void CreateTranslator(Transform cameraBody)
internal static void CreateTranslator(PlayerInfo player)
{
var NomaiTranslatorProp = GameObject.Find("NomaiTranslatorProp");
@ -51,7 +52,8 @@ namespace QSB.Tools.TranslatorTool
tool.ArrivalDegrees = 5f;
tool.Type = ToolType.Translator;
tool.ToolGameObject = REMOTE_TranslatorGroup.gameObject;
tool.RaycastTransform = cameraBody;
tool.Player = player;
tool.RaycastTransform = player.CameraBody.transform;
Object.Destroy(oldTranslator);
PlayerToolsManager.GetRenderer(REMOTE_NomaiTranslatorProp, "Props_HEA_Translator_Screen").material = PlayerToolsManager.Structure_HEA_PlayerShip_Screens_mat;
@ -65,7 +67,7 @@ namespace QSB.Tools.TranslatorTool
PlayerToolsManager.GetRenderer(REMOTE_NomaiTranslatorProp, "Props_HEA_Translator_Button_R").material = PlayerToolsManager.Props_HEA_Lightbulb_mat;
PlayerToolsManager.GetRenderer(REMOTE_NomaiTranslatorProp, "Props_HEA_Translator_Button_R").shadowCastingMode = ShadowCastingMode.On;
REMOTE_NomaiTranslatorProp.transform.parent = cameraBody;
REMOTE_NomaiTranslatorProp.transform.parent = player.CameraBody.transform;
REMOTE_NomaiTranslatorProp.transform.localPosition = Vector3.zero;
REMOTE_NomaiTranslatorProp.transform.localScale = TranslatorScale;
QSBCore.UnityEvents.FireInNUpdates(() => REMOTE_NomaiTranslatorProp.SetActive(true), 5);

View File

@ -109,17 +109,33 @@ namespace QSB.WorldSync
where TUnityObject : MonoBehaviour
{
var list = GetUnityObjects<TUnityObject>().ToList();
//DebugLog.DebugWrite($"{typeof(TWorldObject).Name} init : {list.Count} instances.", MessageType.Info);
for (var id = 0; id < list.Count; id++)
Init<TWorldObject, TUnityObject>(list);
}
public static void Init<TWorldObject, TUnityObject>(params Type[] typesToExclude)
where TWorldObject : WorldObject<TUnityObject>, new()
where TUnityObject : MonoBehaviour
{
var list = GetUnityObjects<TUnityObject>().Where(x => !typesToExclude.Contains(x.GetType()));
Init<TWorldObject, TUnityObject>(list);
}
private static void Init<TWorldObject, TUnityObject>(IEnumerable<TUnityObject> listToInitFrom)
where TWorldObject : WorldObject<TUnityObject>, new()
where TUnityObject : MonoBehaviour
{
//DebugLog.DebugWrite($"{typeof(TWorldObject).Name} init : {listToInitFrom.Count()} instances.", MessageType.Info);
foreach (var item in listToInitFrom)
{
var obj = new TWorldObject
{
AttachedObject = list[id],
AttachedObject = item,
ObjectId = WorldObjects.Count
};
obj.Init();
WorldObjects.Add(obj);
WorldObjectsToUnityObjects.Add(list[id], obj);
WorldObjectsToUnityObjects.Add(item, obj);
}
}

View File

@ -1,6 +1,6 @@
{
"filename": "QSB.dll",
"author": "Nebula, Alek, & Rai",
"author": "Nebula, John, Alek, & Rai",
"name": "Quantum Space Buddies",
"warning": {
"title": "Follow these steps before playing multiplayer :",

162
README.md
View File

@ -4,10 +4,13 @@
![GitHub release (latest by date)](https://img.shields.io/github/v/release/misternebula/quantum-space-buddies?style=flat-square)
![GitHub Release Date](https://img.shields.io/github/release-date/misternebula/quantum-space-buddies?label=last%20release&style=flat-square)
![GitHub all releases](https://img.shields.io/github/downloads/misternebula/quantum-space-buddies/total?style=flat-square)
![GitHub release (latest by date)](https://img.shields.io/github/downloads/misternebula/quantum-space-buddies/latest/total?style=flat-square)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/misternebula/quantum-space-buddies/dev?label=last%20commit%20to%20dev&style=flat-square)
Quantum Space Buddies (QSB) is a multiplayer mod for Outer Wilds. The mod uses the OWML mod loader and customized UNET code (internally referred to as QNet or QuantumUNET) for networking.
# Spoilers within!
## License
QNet code adapted in part from Unity Technologies' UNET.
@ -27,98 +30,6 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- TOC -->
- [FAQs](#frequently-asked-questions)
- [Requirements](#requirements)
- [Compatibility with other mods](#compatibility-with-other-mods)
- [What is synced?](#what-is-currently-synced)
- [Why can't I connect?](#why-cant-i-connect-to-a-server)
- [Installation](#installation)
- [Easy installation (recommended)](#easy-installation-recommended)
- [Manual installation](#manual-installation)
- [Playing as a client](#playing-as-a-client)
- [Playing as a host](#playing-as-a-host)
- [Development Setup](#development-setup)
- [Authors and Special Thanks](#authors-and-special-thanks)
- [Help / Discuss development / Whatever](#help--discuss-development--whatever)
<!-- /TOC -->
## Frequently Asked Questions
### Requirements
- Latest version of OWML.
- Latest version of Mod Manager. (If using)
- Latest version of Outer Wilds. (Epic version preferred, as Steam version is untestable. **We cannot guarantee QSB, or OWML, will work on cracked/pirated versions of Outer Wilds. Do not come asking us for help when using pirated versions.**)
- Fast and stable internet connection, upload and download.
- Above minimum Outer Wilds system requirements.
- Knowledge on port forwarding and router/network configuration. We can be tech support for the mod, not your router or computer.
### Compatibility with other mods
TL;DR - Don't use any mods with QSB that aren't marked as QSB compatible.
QSB relies on exact orders of objects found using Resources.FindObjectsOfTypeAll to sync objects, so any mod that changes the hierarchy at all risks breaking QSB. Also, QSB relies on certain game events being called when things happen in-game. Any mod that makes these things happen without calling the correct events will break QSB. Some mods will work fine and have been tested, like CrouchMod. Others may only work partly, like EnableDebugMode and TAICheat.
**NomaiVR compatibility is currently not planned and likely will never happen, due to extensive changes needed to both mods for it to work.**
### What is currently synced?
| System / Mechanic | Synced? |
| :---: | :---: |
| Anglerfish | Yes |
| Brittle Hollow fragments | Yes |
| Campfires | Yes |
| Conversations with NPCs | Yes |
| Discovering signals/frequencies | Yes |
| Eye of the Universe ancient glade | No |
| Eye of the Universe instrument hunt | No |
| Eye of the Universe jam session | No |
| Eye of the Universe quantum lightning | No |
| Geysers | Yes |
| Items | Yes |
| Jellyfish | No |
| Marshmallow roasting | Yes |
| Meteors | Yes |
| Museum statue | Yes |
| NPC animations | Yes |
| Nomai orbs | Yes |
| Nomai shuttle | Kind of |
| Orbital Probe Cannon (direction) | No |
| Player animation | Yes |
| Player position | Yes |
| Player tools | Yes |
| Projection pools | Yes |
| Quantum objects | Yes |
| Repairing ship parts | Yes |
| Repairing "satellite" parts | No |
| Ship | Yes |
| Ship log | Yes |
| Solanum | Yes |
| Timber Hearth satellite | Yes |
| Tornadoes | No |
QSB also changes some mechanics of the base game, to better fit a multiplayer experience. These include :
- Adding dialogue boxes above NPC and player heads, so other players can "listen in" on conversations.
- Quantum objects check observations from all players and all player probes.
- When dying from any cause other than the supernova, the ATP black hole, or the end of the game, the player respawns instantly at Timber Hearth.
- While at least one player is in them, players can walk into and out of projection pools at will, and everything will work as expected.
- The ship's electrical systems remain on if any player is in the ship.
- The ship's hatch functions differently, allowing multiple players to enter/exit without much annoyance.
### Why can't I connect to a server?
#### For the host :
- Open port 7777 TCP and UDP on your router. If access the internet through multiple layers of routers, the port will need to be opened on every router.
- Open port 7777 TCP and UDP in and out on your firewall. Some AVs might block you editing firewall settings, so check with your specific software.
- Make sure you are giving your public IPv4 address to your clients.
#### For the client :
- Open port 7777 TCP and UDP in and out on your firewall. Some AVs might block you editing firewall settings, so check with your specific software.
- Sometimes, it has helped to change your network profile to "private".
- Make sure you are putting the right address into the address box.
If nothing here works, many people have got QSB working through programs such as Hamachi. Also make sure you are not running through a VPN while trying to connect.
Note - _nebula has no idea how Hamachi works and has never used it, so don't ask them for help setting it up! As said before, we are tech support for the mod. If you cannot connect to someone, or someone cannot connect to you, **that is not QSB's fault.**
## Installation
### Easy installation (recommended)
@ -137,7 +48,7 @@ Note - _nebula has no idea how Hamachi works and has never used it, so don't ask
## Playing as a client
- Run the game.
- On the title/pause menu, select "MULTIPLAYER (CONNECT)".
- On the title menu, select "CONNECT TO MULTIPLAYER".
- Enter the public IP address of the host.
- Hit connect, and pray.
@ -145,9 +56,66 @@ Note - _nebula has no idea how Hamachi works and has never used it, so don't ask
- Open port `7777` on your router.
- Run the game.
- On the title/pause menu, select "MULTIPLAYER (HOST)".
- On the pause menu, select "OPEN TO MULTIPLAYER".
- Give your external IPv4 address to your clients ([like what you see here](http://whatismyip.host/)).
## Frequently Asked Questions
#### Requirements
- Latest version of OWML.
- Latest version of Mod Manager. (If using)
- Latest version of Outer Wilds. (Epic version preferred, as Steam version is untestable. **We cannot guarantee QSB, or OWML, will work on cracked/pirated versions of Outer Wilds. Do not come asking us for help when using pirated versions.**)
- Fast and stable internet connection, upload and download.
- Above minimum Outer Wilds system requirements.
- Knowledge on port forwarding and router/network configuration. We can be tech support for the mod, not your router or computer.
#### Compatibility with other mods
TL;DR - Don't use any mods with QSB that aren't marked as QSB compatible.
QSB relies on exact orders of objects found using Resources.FindObjectsOfTypeAll to sync objects, so any mod that changes the hierarchy at all risks breaking QSB. Also, QSB relies on certain game events being called when things happen in-game. Any mod that makes these things happen without calling the correct events will break QSB. Some mods will work fine and have been tested, like CrouchMod. Others may only work partly, like EnableDebugMode and TAICheat.
#### Will you make this compatible with NomaiVR?
Short answer : No.
Long answer : Pay me enough money, and maybe I'll consider it.
#### Why can't a Steam game connect to an Epic game, and vice versa? Do you hate Steam/Epic?
QSB is incompatible between game vendors because of how it works at a base level. Not because I dislike Steam or Epic.
Technical explanation : QSB relies on the orders of lists returned by certain Unity methods to be the same on all clients. For Unity objects, these are (probably) ordered by AssetID or InstanceID. These IDs are different across different game builds. The Epic and Steam versions are different builds. Therefore, the lists are ordered differently and everything breaks.
#### Why can't I connect to a server?
##### For the host :
- Open port 7777 TCP and UDP on your router. If access the internet through multiple layers of routers, the port will need to be opened on every router.
- Open port 7777 TCP and UDP in and out on your firewall. Some AVs might block you editing firewall settings, so check with your specific software.
- Make sure you are giving your public IPv4 address to your clients.
##### For the client :
- Open port 7777 TCP and UDP in and out on your firewall. Some AVs might block you editing firewall settings, so check with your specific software.
- Sometimes, it has helped to change your network profile to "private".
- Make sure you are putting the right address into the address box.
If nothing here works, many people have got QSB working through programs such as Hamachi. Also make sure you are not running through a VPN while trying to connect.
Note - _nebula has no idea how Hamachi works and has never used it, so don't ask them for help setting it up! As said before, we are tech support for the mod. If you cannot connect to someone, or someone cannot connect to you, **that is not QSB's fault.**
#### Why do I keep getting thrown around the ship?
Boring boring physics stuff. The velocity of the ship is synced, as well as the angular velocity. However, this velocity is not also applied to the player. (Or it is sometimes. I don't 100% know.) This means the ship will accelerate, leaving the player "behind". Which makes you fly into the walls alot.
So really there's nothing we can do about this. I disabled damage by impact inside the ship, so if you die inside the ship while it is flying then that is a bug.
#### What's the difference between QSB and Outer Wilds Online?
QSB is a fully synced game. The other players are actually there in the world, and can affect things. The loop starts/ends at the same time for everyone, and you share ship logs / signal discoveries.
Outer Wilds Online is not multiplayer. The other players cannot affect your game, and do not contribute to anything in your save. The loop is entirely per-player.
#### Why would someone make this mod? Seems like a lot of effort for no reward.
Good question.
Let me know if you find an answer.
## Development Setup
- [Download the Outer Wilds Mod Manager](https://github.com/raicuparta/ow-mod-manager) and install it anywhere you like;
@ -196,14 +164,14 @@ It is also recommended to lower all graphics settings to minimum, be in windowed
### Authors
- [\_nebula](https://github.com/misternebula) - Developer of v0.3 onwards
- [AmazingAlek](https://github.com/amazingalek) - On-and-off developer and sometimes code tidy-er
- [Raicuparta](https://github.com/Raicuparta) - Developer of v0.1 - v0.2
- [JohnCorby](https://github.com/JohnCorby) - Co-developer of 0.13.0 onwards.
- [AmazingAlek](https://github.com/amazingalek) - Developer of v0.1.0 - v0.7.1.
- [Raicuparta](https://github.com/Raicuparta) - Developer of v0.1.0 - v0.2.0.
### Contributers
- [ShoosGun](https://github.com/ShoosGun)
- [Chris Yeninas](https://github.com/PhantomGamers)
- [Will Corby](https://github.com/JohnCorby)
### Special Thanks
- Thanks to Logan Ver Hoef for help with the game code.