Merge pull request #577 from misternebula/dev

0.23.0
This commit is contained in:
_nebula 2022-12-03 11:26:19 +00:00 committed by GitHub
commit c19536eaec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 872 additions and 590 deletions

View File

@ -16,7 +16,6 @@
</PropertyGroup>
<PropertyGroup Label="Default Locations" Condition="!Exists('$(DevEnvLoc)')">
<GameDir>C:\Program Files\Epic Games\OuterWilds</GameDir>
<OwmlDir>$(AppData)\OuterWildsModManager\OWML</OwmlDir>
<UnityAssetsDir>$(SolutionDir)\qsb-unityproject\Assets</UnityAssetsDir>
</PropertyGroup>

View File

@ -39,6 +39,7 @@ public class AnimationSync : PlayerSyncObject
/// <summary>
/// This wipes the NetworkAnimator's fields, so it assumes the parameters have changed.
/// Basically just forces it to set all its dirty flags.
/// BUG: this doesnt work for other players because its only called by the host.
/// </summary>
private void SendInitialState(uint to) => NetworkAnimator.Invoke("Awake");

View File

@ -1,7 +1,9 @@
using UnityEngine;
using QSB.Utility;
using UnityEngine;
namespace QSB.ConversationSync;
[UsedInUnityProject]
public class CameraFacingBillboard : MonoBehaviour
{
private OWCamera _activeCam;

View File

@ -3,7 +3,6 @@ using QSB.DeathSync.Messages;
using QSB.Messaging;
using QSB.Patches;
using QSB.Player;
using QSB.Utility;
using System.Linq;
using UnityEngine;
@ -14,96 +13,47 @@ public class DeathPatches : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
/// <summary>
/// don't take damage from impact in ship
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.OnImpact))]
public static bool PlayerResources_OnImpact(PlayerResources __instance, ImpactData impact) =>
// don't take damage from impact in ship
!PlayerState.IsInsideShip();
public static bool PlayerResources_OnImpact(PlayerResources __instance, ImpactData impact)
{
if (QSBCore.ShipDamage)
{
return true;
}
return !PlayerState.IsInsideShip();
}
/// <summary>
/// don't insta-die from impact in ship
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(typeof(HighSpeedImpactSensor), nameof(HighSpeedImpactSensor.FixedUpdate))]
public static bool HighSpeedImpactSensor_FixedUpdate(HighSpeedImpactSensor __instance)
[HarmonyPatch(typeof(HighSpeedImpactSensor), nameof(HighSpeedImpactSensor.HandlePlayerInsideShip))]
public static bool HighSpeedImpactSensor_HandlePlayerInsideShip(HighSpeedImpactSensor __instance)
{
if (__instance._isPlayer && (PlayerState.IsAttached() || PlayerState.IsInsideShuttle() || PlayerState.UsingNomaiRemoteCamera()))
if (QSBCore.ShipDamage)
{
return false;
return true;
}
if (__instance._dieNextUpdate && !__instance._dead)
var shipCenter = Locator.GetShipTransform().position + Locator.GetShipTransform().up * 2f;
var distanceFromShip = Vector3.Distance(__instance._body.GetPosition(), shipCenter);
if (distanceFromShip > 8f)
{
__instance._dead = true;
__instance._dieNextUpdate = false;
if (__instance.gameObject.CompareTag("Player"))
{
Locator.GetDeathManager().SetImpactDeathSpeed(__instance._impactSpeed);
Locator.GetDeathManager().KillPlayer(DeathType.Impact);
}
else if (__instance.gameObject.CompareTag("Ship"))
{
__instance.GetComponent<ShipDamageController>().Explode();
}
__instance._body.SetPosition(shipCenter);
}
if (__instance._isPlayer && PlayerState.IsInsideShip())
if (!__instance._dead)
{
var shipCenter = Locator.GetShipTransform().position + Locator.GetShipTransform().up * 2f;
var distanceFromShip = Vector3.Distance(__instance._body.GetPosition(), shipCenter);
if (distanceFromShip > 8f)
var a = __instance._body.GetVelocity() - Locator.GetShipBody().GetPointVelocity(__instance._body.GetPosition());
if (a.sqrMagnitude > __instance._sqrCheckSpeedThreshold)
{
__instance._body.SetPosition(shipCenter);
}
if (!__instance._dead)
{
var a = __instance._body.GetVelocity() - Locator.GetShipBody().GetPointVelocity(__instance._body.GetPosition());
if (a.sqrMagnitude > __instance._sqrCheckSpeedThreshold)
{
__instance._impactSpeed = a.magnitude;
__instance._body.AddVelocityChange(-a);
}
}
return false;
}
var passiveReferenceFrame = __instance._sectorDetector.GetPassiveReferenceFrame();
if (!__instance._dead && passiveReferenceFrame != null)
{
var relativeVelocity = __instance._body.GetVelocity() - passiveReferenceFrame.GetOWRigidBody().GetPointVelocity(__instance._body.GetPosition());
if (relativeVelocity.sqrMagnitude > __instance._sqrCheckSpeedThreshold)
{
var hitCount = Physics.RaycastNonAlloc(__instance.transform.TransformPoint(__instance._localOffset), relativeVelocity, __instance._raycastHits, relativeVelocity.magnitude * Time.deltaTime + __instance._radius, OWLayerMask.physicalMask, QueryTriggerInteraction.Ignore);
for (var i = 0; i < hitCount; i++)
{
if (__instance._raycastHits[i].rigidbody.mass > 10f && !__instance._raycastHits[i].rigidbody.Equals(__instance._body.GetRigidbody()))
{
var owRigidbody = __instance._raycastHits[i].rigidbody.GetComponent<OWRigidbody>();
if (owRigidbody == null)
{
DebugLog.ToConsole("Rigidbody does not have attached OWRigidbody!!!", OWML.Common.MessageType.Error);
Debug.Break();
}
else
{
relativeVelocity = __instance._body.GetVelocity() - owRigidbody.GetPointVelocity(__instance._body.GetPosition());
var a2 = Vector3.Project(relativeVelocity, __instance._raycastHits[i].normal);
if (a2.sqrMagnitude > __instance._sqrCheckSpeedThreshold)
{
__instance._body.AddVelocityChange(-a2);
__instance._impactSpeed = a2.magnitude;
if (!PlayerState.IsInsideTheEye())
{
__instance._dieNextUpdate = true;
}
break;
}
}
}
}
__instance._impactSpeed = a.magnitude;
__instance._body.AddVelocityChange(-a);
}
}

View File

@ -1,8 +1,9 @@
using Cysharp.Threading.Tasks;
using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
using QSB.Utility;
using QSB.WorldSync;
using System.Linq;
using System.Threading;
using UnityEngine;
namespace QSB.EchoesOfTheEye.AlarmTotemSync;
@ -11,18 +12,12 @@ public class AlarmTotemManager : WorldObjectManager
public override WorldObjectScene WorldObjectScene => WorldObjectScene.SolarSystem;
public override bool DlcOnly => true;
private QSBAlarmSequenceController _qsbAlarmSequenceController;
public static AlarmBell[] AlarmBells;
public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct)
{
// QSBWorldSync.Init<QSBAlarmTotem, AlarmTotem>();
QSBWorldSync.Init<QSBAlarmTotem, AlarmTotem>();
QSBWorldSync.Init<QSBAlarmBell, AlarmBell>();
_qsbAlarmSequenceController = new GameObject(nameof(QSBAlarmSequenceController))
.AddComponent<QSBAlarmSequenceController>();
DontDestroyOnLoad(_qsbAlarmSequenceController.gameObject);
AlarmBells = QSBWorldSync.GetUnityObjects<AlarmBell>().Where(x => x._lightController).SortDeterministic().ToArray();
}
public override void UnbuildWorldObjects() =>
Destroy(_qsbAlarmSequenceController.gameObject);
}

View File

@ -0,0 +1,37 @@
using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
using QSB.Messaging;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.Messages;
public class SetVisibleMessage : QSBWorldObjectMessage<QSBAlarmTotem, bool>
{
public SetVisibleMessage(bool visible) : base(visible) { }
public override void OnReceiveRemote()
{
if (WorldObject.AttachedObject._isPlayerVisible == Data)
{
return;
}
WorldObject.AttachedObject._isPlayerVisible = Data;
if (Data)
{
Locator.GetAlarmSequenceController().IncreaseAlarmCounter();
WorldObject.AttachedObject._secondsConcealed = 0f;
WorldObject.AttachedObject._simTotemMaterials[0] = WorldObject.AttachedObject._simAlarmMaterial;
WorldObject.AttachedObject._simTotemRenderer.sharedMaterials = WorldObject.AttachedObject._simTotemMaterials;
WorldObject.AttachedObject._simVisionConeRenderer.SetColor(WorldObject.AttachedObject._simAlarmColor);
GlobalMessenger.FireEvent("AlarmTotemTriggered");
}
else
{
Locator.GetAlarmSequenceController().DecreaseAlarmCounter();
WorldObject.AttachedObject._secondsConcealed = 0f;
WorldObject.AttachedObject._simTotemMaterials[0] = WorldObject.AttachedObject._origSimEyeMaterial;
WorldObject.AttachedObject._simTotemRenderer.sharedMaterials = WorldObject.AttachedObject._simTotemMaterials;
WorldObject.AttachedObject._simVisionConeRenderer.SetColor(WorldObject.AttachedObject._simVisionConeRenderer.GetOriginalColor());
WorldObject.AttachedObject._pulseLightController.FadeTo(0f, 0.5f);
}
}
}

View File

@ -1,12 +0,0 @@
using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
using QSB.Messaging;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.Messages;
public class TotemEnabledMessage : QSBWorldObjectMessage<QSBAlarmTotem, bool>
{
public TotemEnabledMessage(bool enabled) : base(enabled) { }
public override void OnReceiveRemote() =>
WorldObject.SetEnabled(Data);
}

View File

@ -1,16 +0,0 @@
using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
using QSB.Messaging;
using System.Collections.Generic;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.Messages;
public class TotemVisibleForMessage : QSBWorldObjectMessage<QSBAlarmTotem, List<uint>>
{
public TotemVisibleForMessage(List<uint> visibleFor) : base(visibleFor) { }
public override void OnReceiveRemote()
{
WorldObject.VisibleFor.Clear();
WorldObject.VisibleFor.AddRange(Data);
}
}

View File

@ -1,11 +0,0 @@
using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
using QSB.Messaging;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.Messages;
public class TotemVisibleMessage : QSBWorldObjectMessage<QSBAlarmTotem, bool>
{
public TotemVisibleMessage(bool visible) : base(visible) { }
public override void OnReceiveLocal() => OnReceiveRemote();
public override void OnReceiveRemote() => WorldObject.SetVisible(From, Data);
}

View File

@ -0,0 +1,93 @@
using HarmonyLib;
using QSB.Patches;
using UnityEngine;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.Patches;
[HarmonyPatch(typeof(AlarmSequenceController))]
public class AlarmSequenceControllerPatches : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
[HarmonyPrefix]
[HarmonyPatch(nameof(AlarmSequenceController.IncreaseAlarmCounter))]
private static bool IncreaseAlarmCounter(AlarmSequenceController __instance)
{
__instance._alarmCounter++;
if (__instance._alarmCounter == 1)
{
__instance.PlayChimes();
}
__instance.enabled = true;
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(AlarmSequenceController.DecreaseAlarmCounter))]
private static bool DecreaseAlarmCounter(AlarmSequenceController __instance)
{
__instance._alarmCounter--;
if (__instance._alarmCounter < 0)
{
__instance._alarmCounter = 0;
Debug.LogError("Something went wrong, alarm counter should never drop below zero!");
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(AlarmSequenceController.StopChimes))]
private static bool StopChimes(AlarmSequenceController __instance)
{
__instance._playing = false;
__instance._stopRequested = false;
__instance._animationStarted = false;
foreach (var alarmBell in AlarmTotemManager.AlarmBells)
{
alarmBell.StopAnimation();
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(AlarmSequenceController.Update))]
private static bool Update(AlarmSequenceController __instance)
{
__instance.UpdateWakeFraction();
if (__instance._playing)
{
__instance.UpdateChimes();
}
__instance.UpdatePulse();
if (!__instance._playing && __instance._alarmCounter == 0 && __instance._pulse <= 0.01f)
{
__instance._pulse = 0f;
__instance._targetPulse = 0f;
__instance.enabled = false;
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(AlarmSequenceController.PlaySingleChime))]
private static bool PlaySingleChime(AlarmSequenceController __instance)
{
foreach (var alarmBell in AlarmTotemManager.AlarmBells)
{
alarmBell.PlaySingleChime(__instance._chimeIndex);
}
if (!__instance._animationStarted && !__instance._dreamWorldController.IsInDream())
{
foreach (var alarmBell in AlarmTotemManager.AlarmBells)
{
alarmBell.PlayAnimation();
}
__instance._animationStarted = true;
}
if (__instance._dreamWorldController.IsInDream() && !__instance._dreamWorldController.IsExitingDream())
{
Locator.GetDreamWorldAudioController().PlaySingleAlarmChime(__instance._chimeIndex, __instance._volumeCurve.Evaluate(__instance._wakeFraction));
}
return false;
}
}

View File

@ -1,8 +1,11 @@
using HarmonyLib;
using QSB.AuthoritySync;
using QSB.EchoesOfTheEye.AlarmTotemSync.Messages;
using QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
using QSB.Messaging;
using QSB.Patches;
using QSB.Player;
using QSB.Utility;
using QSB.WorldSync;
using UnityEngine;
@ -12,27 +15,58 @@ 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)
private static bool OnSectorOccupantAdded(AlarmTotem __instance, SectorDetector sectorDetector)
{
if (sectorDetector.GetOccupantType() == DynamicOccupant.Player && QSBWorldSync.AllObjectsReady)
if (!QSBWorldSync.AllObjectsReady)
{
__instance.GetWorldObject<QSBAlarmTotem>()
.SendMessage(new TotemEnabledMessage(true));
return true;
}
if (sectorDetector.GetOccupantType() == DynamicOccupant.Player)
{
__instance.enabled = true;
var qsbAlarmTotem = __instance.GetWorldObject<QSBAlarmTotem>();
qsbAlarmTotem.RequestOwnership();
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(AlarmTotem), nameof(AlarmTotem.OnSectorOccupantRemoved))]
private static void OnSectorOccupantRemoved(AlarmTotem __instance, SectorDetector sectorDetector)
private static bool OnSectorOccupantRemoved(AlarmTotem __instance, SectorDetector sectorDetector)
{
if (sectorDetector.GetOccupantType() == DynamicOccupant.Player && QSBWorldSync.AllObjectsReady)
if (!QSBWorldSync.AllObjectsReady)
{
__instance.GetWorldObject<QSBAlarmTotem>()
.SendMessage(new TotemEnabledMessage(false));
return true;
}
if (sectorDetector.GetOccupantType() == DynamicOccupant.Player)
{
__instance.enabled = false;
var qsbAlarmTotem = __instance.GetWorldObject<QSBAlarmTotem>();
qsbAlarmTotem.ReleaseOwnership();
Delay.RunFramesLater(10, () =>
{
// no one else took ownership, so we can safely turn stuff off
// ie turn off when no one else is there
if (qsbAlarmTotem.Owner == 0)
{
__instance._pulseLightController.SetIntensity(0f);
__instance._simTotemMaterials[0] = __instance._origSimEyeMaterial;
__instance._simTotemRenderer.sharedMaterials = __instance._simTotemMaterials;
__instance._simVisionConeRenderer.SetColor(__instance._simVisionConeRenderer.GetOriginalColor());
if (__instance._isPlayerVisible)
{
__instance._isPlayerVisible = false;
__instance._secondsConcealed = 0f;
Locator.GetAlarmSequenceController().DecreaseAlarmCounter();
}
}
});
}
return false;
}
[HarmonyPrefix]
@ -43,45 +77,87 @@ public class AlarmTotemPatches : QSBPatch
{
return true;
}
var qsbAlarmTotem = __instance.GetWorldObject<QSBAlarmTotem>();
var isLocallyVisible = qsbAlarmTotem.IsLocallyVisible;
qsbAlarmTotem.IsLocallyVisible = __instance.CheckPlayerVisible();
if (qsbAlarmTotem.IsLocallyVisible && !isLocallyVisible)
if (qsbAlarmTotem.Owner != QSBPlayerManager.LocalPlayerId)
{
qsbAlarmTotem.SendMessage(new TotemVisibleMessage(true));
}
else if (isLocallyVisible && !qsbAlarmTotem.IsLocallyVisible)
{
qsbAlarmTotem.SendMessage(new TotemVisibleMessage(false));
return false;
}
var isPlayerVisible = __instance._isPlayerVisible;
__instance._isPlayerVisible = qsbAlarmTotem.VisibleFor.Count > 0;
if (__instance._isPlayerVisible && !isPlayerVisible)
__instance._isPlayerVisible = __instance.CheckPlayerVisible();
if (!isPlayerVisible && __instance._isPlayerVisible)
{
Locator.GetAlarmSequenceController().IncreaseAlarmCounter();
__instance._secondsConcealed = 0f;
__instance._simTotemMaterials[0] = __instance._simAlarmMaterial;
__instance._simTotemRenderer.sharedMaterials = __instance._simTotemMaterials;
__instance._simVisionConeRenderer.SetColor(__instance._simAlarmColor);
if (__instance._isTutorialTotem)
{
GlobalMessenger.FireEvent("TutorialAlarmTotemTriggered");
}
GlobalMessenger.FireEvent("AlarmTotemTriggered");
qsbAlarmTotem.SendMessage(new SetVisibleMessage(true));
}
else if (isPlayerVisible && !__instance._isPlayerVisible)
{
Locator.GetAlarmSequenceController().DecreaseAlarmCounter();
__instance._secondsConcealed = 0f;
__instance._simTotemMaterials[0] = __instance._origSimEyeMaterial;
__instance._simTotemRenderer.sharedMaterials = __instance._simTotemMaterials;
__instance._simVisionConeRenderer.SetColor(__instance._simVisionConeRenderer.GetOriginalColor());
__instance._pulseLightController.FadeTo(0f, 0.5f);
qsbAlarmTotem.SendMessage(new SetVisibleMessage(false));
}
return false;
}
*/
[HarmonyPrefix]
[HarmonyPatch(typeof(AlarmTotem), nameof(AlarmTotem.CheckPlayerVisible))]
private static bool CheckPlayerVisible(AlarmTotem __instance, out bool __result)
{
if (!__instance._isFaceOpen)
{
__result = false;
return false;
}
foreach (var player in QSBPlayerManager.PlayerList)
{
var position = player.Camera.transform.position;
if (__instance.CheckPointInVisionCone(position) && !__instance.CheckLineOccluded(__instance._sightOrigin.position, position))
{
if (player.LightSensor.IsIlluminated())
{
__result = true;
return false;
}
if (player.AssignedSimulationLantern == null)
{
continue;
}
var lanternController = player.AssignedSimulationLantern.AttachedObject.GetLanternController();
if (lanternController.IsHeldByPlayer())
{
if (lanternController.IsConcealed())
{
if (!__instance._hasConcealedFromAlarm)
{
__instance._secondsConcealed += Time.deltaTime;
if (__instance._secondsConcealed > 1f)
{
__instance._hasConcealedFromAlarm = true;
GlobalMessenger.FireEvent("ConcealFromAlarmTotem");
}
}
__result = false;
return false;
}
__result = true;
return false;
}
}
}
__result = false;
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(AlarmBell), nameof(AlarmBell.OnEntry))]

View File

@ -1,245 +0,0 @@
using UnityEngine;
namespace QSB.EchoesOfTheEye.AlarmTotemSync;
/// <summary>
/// copied and modified from base game
/// </summary>
public class QSBAlarmSequenceController : MonoBehaviour
{
private const int CHIME_COUNT = 3;
[SerializeField]
private float _wakeDuration = 5f;
[SerializeField]
private float _recoveryDuration = 2f;
[Header("Audio")]
[SerializeField]
private float _audioLingerTime = 3f;
[SerializeField]
private AnimationCurve _volumeCurve;
[Header("Pulse")]
[SerializeField]
private float _pulseAttackLength = 0.2f;
[SerializeField]
private float _pulseHoldLength = 0.1f;
[SerializeField]
private float _pulseCooldownLength = 1f;
private DreamWorldController _dreamWorldController;
private AlarmBell _activeBell;
private int _alarmCounter;
private float _wakeFraction;
private float _targetPulse;
private float _pulse;
private bool _playing;
private bool _animationStarted;
private bool _stopRequested;
private float _stopTime;
private int _chimeIndex;
private float _lastChimeTime;
private float _chimeInterval;
private void Awake()
{
// Locator.RegisterAlarmSequenceController(this);
GlobalMessenger.AddListener("ExitDreamWorld", OnExitDreamWorld);
}
private void Start()
{
_dreamWorldController = Locator.GetDreamWorldController();
enabled = false;
}
private void OnDestroy()
{
GlobalMessenger.RemoveListener("ExitDreamWorld", OnExitDreamWorld);
}
public void RegisterDreamEyeMaskController(DreamEyeMaskController dreamEyeMaskController) { }
public float GetPulseIntensity() => _pulse;
public bool IsAlarmWakingPlayer() => _alarmCounter > 0 && !PlayerState.IsResurrected();
public void IncreaseAlarmCounter()
{
if (!_dreamWorldController.IsInDream() || _dreamWorldController.IsExitingDream())
{
return;
}
_alarmCounter++;
if (_alarmCounter == 1)
{
PlayChimes();
}
enabled = true;
}
public void DecreaseAlarmCounter()
{
if (!_dreamWorldController.IsInDream() || _dreamWorldController.IsExitingDream())
{
return;
}
_alarmCounter--;
if (_alarmCounter < 0)
{
_alarmCounter = 0;
Debug.LogError("Something went wrong, alarm counter should never drop below zero!");
}
}
private void PlayChimes()
{
if (Locator.GetDreamWorldController().GetDreamCampfire() != null)
{
_activeBell = Locator.GetDreamWorldController().GetDreamCampfire().GetAlarmBell();
}
_playing = true;
_chimeInterval = 1f;
_lastChimeTime = 0f;
_stopRequested = false;
}
private void StopChimes()
{
_playing = false;
_stopRequested = false;
_animationStarted = false;
if (_activeBell != null)
{
_activeBell.StopAnimation();
_activeBell = null;
}
}
private void Update()
{
if (_dreamWorldController.IsInDream() && !_dreamWorldController.IsExitingDream())
{
UpdateWakeFraction();
}
if (_playing)
{
UpdateChimes();
}
UpdatePulse();
if (!_playing && _alarmCounter == 0 && _pulse <= 0.01f)
{
_pulse = 0f;
_targetPulse = 0f;
enabled = false;
}
}
private void UpdateWakeFraction()
{
if (_alarmCounter > 0)
{
_wakeFraction = Mathf.MoveTowards(_wakeFraction, 1f, Time.deltaTime / _wakeDuration);
if (_wakeFraction >= 1f && !PlayerState.IsResurrected())
{
_dreamWorldController.ExitDreamWorld(DreamWakeType.Alarm);
}
}
else if (_wakeFraction > 0f)
{
_wakeFraction = Mathf.MoveTowards(_wakeFraction, 0f, Time.deltaTime / _recoveryDuration);
if (_wakeFraction <= 0f)
{
StopChimes();
}
}
}
private void UpdateChimes()
{
if (Time.time > _lastChimeTime + _chimeInterval)
{
if (!PlayerState.IsResurrected())
{
PlaySingleChime();
}
_targetPulse = 1f;
_lastChimeTime = Time.time;
_chimeInterval = Mathf.Max(_chimeInterval - 0.08f, 0.4f);
_chimeIndex++;
_chimeIndex = _chimeIndex >= CHIME_COUNT ? 0 : _chimeIndex;
}
if (_stopRequested && Time.time > _stopTime)
{
StopChimes();
}
}
private void UpdatePulse()
{
if (Time.time > _lastChimeTime + _pulseAttackLength + _pulseHoldLength)
{
var num = _pulseCooldownLength * _chimeInterval;
_targetPulse = Mathf.MoveTowards(_targetPulse, 0f, 1f / num * Time.deltaTime);
_pulse = _targetPulse;
return;
}
_pulse = Mathf.MoveTowards(_pulse, _targetPulse, 1f / _pulseAttackLength * Time.deltaTime);
}
private void PlaySingleChime()
{
if (_activeBell != null)
{
_activeBell.PlaySingleChime(_chimeIndex);
if (!_animationStarted && !_dreamWorldController.IsInDream())
{
_activeBell.PlayAnimation();
_animationStarted = true;
}
}
if (_dreamWorldController.IsInDream() && !_dreamWorldController.IsExitingDream())
{
Locator.GetDreamWorldAudioController().PlaySingleAlarmChime(_chimeIndex, _volumeCurve.Evaluate(_wakeFraction));
}
}
private void OnExitDreamWorld()
{
if (_playing)
{
_stopRequested = true;
_stopTime = Time.time + _audioLingerTime;
}
_alarmCounter = 0;
_wakeFraction = 0f;
}
}

View File

@ -1,83 +1,17 @@
using Cysharp.Threading.Tasks;
using QSB.AuthoritySync;
using QSB.EchoesOfTheEye.AlarmTotemSync.Messages;
using QSB.Messaging;
using QSB.Player;
using QSB.WorldSync;
using System.Collections.Generic;
using System.Threading;
namespace QSB.EchoesOfTheEye.AlarmTotemSync.WorldObjects;
/// <summary>
/// TODO: make this not NRE (by not doing enable sync) and then readd it back in
/// </summary>
public class QSBAlarmTotem : WorldObject<AlarmTotem>
public class QSBAlarmTotem : AuthWorldObject<AlarmTotem>
{
public readonly List<uint> VisibleFor = new();
public bool IsLocallyVisible;
public override bool CanOwn => AttachedObject.enabled;
public override void SendInitialState(uint to)
{
this.SendMessage(new TotemEnabledMessage(AttachedObject.enabled) { To = to });
this.SendMessage(new TotemVisibleForMessage(VisibleFor) { To = to });
}
base.SendInitialState(to);
public override async UniTask Init(CancellationToken ct) =>
QSBPlayerManager.OnRemovePlayer += OnPlayerLeave;
public override void OnRemoval() =>
QSBPlayerManager.OnRemovePlayer -= OnPlayerLeave;
private void OnPlayerLeave(PlayerInfo player) =>
VisibleFor.QuickRemove(player.PlayerId);
public void SetVisible(uint playerId, bool visible)
{
if (visible)
{
VisibleFor.SafeAdd(playerId);
}
else
{
VisibleFor.QuickRemove(playerId);
}
}
public void SetEnabled(bool enabled)
{
if (AttachedObject.enabled == enabled)
{
return;
}
if (!enabled &&
AttachedObject._sector &&
AttachedObject._sector.ContainsOccupant(DynamicOccupant.Player))
{
// local player is in sector, do not disable
return;
}
AttachedObject.enabled = enabled;
if (!enabled)
{
AttachedObject._simTotemMaterials[0] = AttachedObject._origSimEyeMaterial;
AttachedObject._simTotemRenderer.sharedMaterials = AttachedObject._simTotemMaterials;
AttachedObject._simVisionConeRenderer.SetColor(AttachedObject._simVisionConeRenderer.GetOriginalColor());
AttachedObject._pulseLightController.SetIntensity(0f);
/*
if (AttachedObject._isPlayerVisible)
{
AttachedObject._isPlayerVisible = false;
Locator.GetAlarmSequenceController().DecreaseAlarmCounter();
}
*/
if (IsLocallyVisible)
{
IsLocallyVisible = false;
this.SendMessage(new TotemVisibleMessage(false));
}
}
this.SendMessage(new SetVisibleMessage(AttachedObject._isPlayerVisible) { To = to });
}
}

View File

@ -10,6 +10,9 @@ public class DreamLanternManager : 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<QSBDreamLanternController, DreamLanternController>();
QSBWorldSync.Init<QSBDreamLanternItem, DreamLanternItem>();
}
}

View File

@ -1,9 +1,10 @@
using Cysharp.Threading.Tasks;
using QSB.ItemSync.WorldObjects.Items;
using System.Linq;
using System.Threading;
using UnityEngine;
namespace QSB.ItemSync.WorldObjects.Items;
namespace QSB.EchoesOfTheEye.DreamLantern.WorldObjects;
public class QSBDreamLanternItem : QSBItem<DreamLanternItem>
{

View File

@ -0,0 +1,114 @@
using HarmonyLib;
using QSB.Animation.Player;
using QSB.Messaging;
using QSB.Patches;
using QSB.Player;
using QSB.Utility;
using System.Collections.Generic;
using UnityEngine;
namespace QSB.EchoesOfTheEye.DreamWorld.Messages;
public class DreamWorldFakePlayer : MonoBehaviour
{
private static readonly List<DreamWorldFakePlayer> _instances = new();
public static void Create(PlayerInfo player)
{
var go = new GameObject($"player {player} DreamWorldFakePlayer");
go.SetActive(false);
go.AddComponent<DreamWorldFakePlayer>()._player = player;
go.SetActive(true);
}
public static void Destroy(PlayerInfo player)
{
foreach (var dreamWorldFakePlayer in _instances)
{
if (dreamWorldFakePlayer._player == player)
{
Destroy(dreamWorldFakePlayer.gameObject);
}
}
}
private PlayerInfo _player;
private void Awake()
{
_instances.SafeAdd(this);
QSBPlayerManager.OnRemovePlayer += OnRemovePlayer;
transform.parent = _player.TransformSync.ReferenceTransform;
transform.localPosition = _player.TransformSync.transform.position;
transform.localRotation = _player.TransformSync.transform.rotation;
#region fake player
var fakePlayer = _player.Body.transform.Find("REMOTE_Traveller_HEA_Player_v2").gameObject.InstantiateInactive();
fakePlayer.transform.SetParent(transform, false);
Destroy(fakePlayer.GetComponent<Animator>());
Destroy(fakePlayer.GetComponent<PlayerHeadRotationSync>());
var REMOTE_ItemCarryTool = fakePlayer.transform.Find(
// TODO : kill me for my sins
"Traveller_Rig_v01:Traveller_Trajectory_Jnt/" +
"Traveller_Rig_v01:Traveller_ROOT_Jnt/" +
"Traveller_Rig_v01:Traveller_Spine_01_Jnt/" +
"Traveller_Rig_v01:Traveller_Spine_02_Jnt/" +
"Traveller_Rig_v01:Traveller_Spine_Top_Jnt/" +
"Traveller_Rig_v01:Traveller_RT_Arm_Clavicle_Jnt/" +
"Traveller_Rig_v01:Traveller_RT_Arm_Shoulder_Jnt/" +
"Traveller_Rig_v01:Traveller_RT_Arm_Elbow_Jnt/" +
"Traveller_Rig_v01:Traveller_RT_Arm_Wrist_Jnt/" +
"REMOTE_ItemCarryTool"
).gameObject;
Destroy(REMOTE_ItemCarryTool);
fakePlayer.SetActive(true);
#endregion
}
private void OnDestroy()
{
_instances.QuickRemove(this);
QSBPlayerManager.OnRemovePlayer -= OnRemovePlayer;
}
private void OnRemovePlayer(PlayerInfo player)
{
if (player != _player)
{
return;
}
Destroy(gameObject);
}
}
public class DreamWorldFakePlayerPatch : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
/// <summary>
/// do this early to create the fake player BEFORE teleporting
/// </summary>
[HarmonyPatch(typeof(DreamWorldController), nameof(DreamWorldController.EnterDreamWorld))]
[HarmonyPrefix]
private static void EnterDreamWorld()
{
if (Locator.GetToolModeSwapper().GetItemCarryTool().GetHeldItemType() == ItemType.DreamLantern)
{
new DreamWorldFakePlayerMessage().Send();
}
}
}
public class DreamWorldFakePlayerMessage : QSBMessage
{
public override void OnReceiveRemote()
{
DreamWorldFakePlayer.Create(QSBPlayerManager.GetPlayer(From));
}
}

View File

@ -1,5 +1,5 @@
using QSB.EchoesOfTheEye.Ghosts.WorldObjects;
using QSB.ItemSync.WorldObjects.Items;
using QSB.EchoesOfTheEye.DreamLantern.WorldObjects;
using QSB.EchoesOfTheEye.Ghosts.WorldObjects;
using QSB.Messaging;
using QSB.Player;
using QSB.Player.TransformSync;

View File

@ -50,5 +50,8 @@ internal class ExitDreamWorldMessage : QSBMessage
ghost.GetEffects().OnSectorOccupantsUpdated();
}
}
Locator.GetAlarmSequenceController().OnExitDreamWorld();
DreamWorldFakePlayer.Destroy(player);
}
}

View File

@ -5,5 +5,5 @@ namespace QSB.EchoesOfTheEye.RaftSync.Messages;
public class RaftDockOnPressInteractMessage : QSBWorldObjectMessage<QSBRaftDock>
{
public override void OnReceiveRemote() => WorldObject.OnPressInteract();
public override void OnReceiveRemote() => WorldObject.AttachedObject.OnPressInteract();
}

View File

@ -6,6 +6,4 @@ namespace QSB.EchoesOfTheEye.RaftSync.WorldObjects;
public class QSBRaftDock : WorldObject<RaftDock>, IQSBDropTarget
{
IItemDropTarget IQSBDropTarget.AttachedObject => AttachedObject;
public void OnPressInteract() => AttachedObject.OnPressInteract();
}

View File

@ -8,7 +8,7 @@ public class QSBSarcophagus : WorldObject<SarcophagusController>
{
public override void SendInitialState(uint to)
{
if (AttachedObject._isOpen)
if (AttachedObject._isOpen || AttachedObject._isSlightlyOpen)
{
this.SendMessage(new OpenMessage());
}

View File

@ -0,0 +1,17 @@
using QSB.EchoesOfTheEye.VisionTorch.WorldObjects;
using QSB.Messaging;
namespace QSB.EchoesOfTheEye.VisionTorch.Messages;
public class VisionTorchProjectMessage : QSBWorldObjectMessage<QSBVisionTorchItem, bool>
{
public VisionTorchProjectMessage(bool projecting) : base(projecting) { }
public override void OnReceiveRemote()
{
WorldObject.AttachedObject._isProjecting = Data;
WorldObject.AttachedObject._mindProjectorTrigger.SetProjectorActive(Data);
// prevent the torch from actually doing things remotely
WorldObject.AttachedObject.mindProjectorTrigger._triggerVolume.SetTriggerActivation(false);
}
}

View File

@ -0,0 +1,45 @@
using HarmonyLib;
using QSB.EchoesOfTheEye.VisionTorch.Messages;
using QSB.EchoesOfTheEye.VisionTorch.WorldObjects;
using QSB.Messaging;
using QSB.Patches;
using QSB.WorldSync;
namespace QSB.EchoesOfTheEye.VisionTorch.Patches;
public class VisionTorchPatches : QSBPatch
{
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
[HarmonyPrefix]
[HarmonyPatch(typeof(VisionTorchItem), nameof(VisionTorchItem.Update))]
private static bool Update(VisionTorchItem __instance)
{
if (!QSBWorldSync.AllObjectsReady)
{
return true;
}
if (PlayerState.IsViewingProjector() && __instance._mindSlideProjector.mindSlideCollection.slideCollectionContainer.slideIndex == 1)
{
OWInput.ChangeInputMode(InputMode.None);
__instance._mindSlideProjector.OnProjectionComplete += __instance.OnProjectionComplete;
__instance.enabled = false;
return false;
}
__instance._wasProjecting = __instance._isProjecting;
__instance._isProjecting = OWInput.IsPressed(InputLibrary.toolActionPrimary, InputMode.Character);
if (__instance._isProjecting && !__instance._wasProjecting)
{
__instance._mindProjectorTrigger.SetProjectorActive(true);
__instance.GetWorldObject<QSBVisionTorchItem>().SendMessage(new VisionTorchProjectMessage(true));
}
else if (!__instance._isProjecting && __instance._wasProjecting)
{
__instance._mindProjectorTrigger.SetProjectorActive(false);
__instance.GetWorldObject<QSBVisionTorchItem>().SendMessage(new VisionTorchProjectMessage(false));
}
return false;
}
}

View File

@ -0,0 +1,15 @@
using Cysharp.Threading.Tasks;
using QSB.EchoesOfTheEye.VisionTorch.WorldObjects;
using QSB.WorldSync;
using System.Threading;
namespace QSB.EchoesOfTheEye.VisionTorch;
public class VisionTorchManager : WorldObjectManager
{
public override WorldObjectScene WorldObjectScene => WorldObjectScene.SolarSystem;
public override bool DlcOnly => true;
public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken ct) =>
QSBWorldSync.Init<QSBVisionTorchItem, VisionTorchItem>();
}

View File

@ -0,0 +1,13 @@
using QSB.EchoesOfTheEye.VisionTorch.Messages;
using QSB.ItemSync.WorldObjects.Items;
using QSB.Messaging;
namespace QSB.EchoesOfTheEye.VisionTorch.WorldObjects;
public class QSBVisionTorchItem : QSBItem<VisionTorchItem>
{
public override void SendInitialState(uint to)
{
this.SendMessage(new VisionTorchProjectMessage(AttachedObject._isProjecting) { To = to });
}
}

View File

@ -4,9 +4,6 @@ using UnityEngine;
namespace QSB;
/// <summary>
/// TODO: TEST THIS. see if things horribly break. this could be huge.
/// </summary>
[HarmonyPatch(typeof(OWExtensions))]
public class GetAttachedOWRigidbodyPatch : QSBPatch
{

View File

@ -9,7 +9,23 @@ internal class InputPatches : QSBPatch
public override QSBPatchTypes Type => QSBPatchTypes.OnClientConnect;
[HarmonyPrefix]
[HarmonyPatch(typeof(OWInput), nameof(OWInput.Update))]
public static bool OWInput_Update()
=> QSBInputManager.Instance.InputsEnabled;
[HarmonyPatch(typeof(AbstractCommands), nameof(AbstractCommands.Update))]
public static bool AbstractCommands_Update(AbstractCommands __instance)
{
if (QSBInputManager.Instance.InputsEnabled)
{
return true;
}
__instance.Consumed = false;
__instance.WasActiveLastFrame = __instance.IsActiveThisFrame;
__instance.IsActiveThisFrame = false;
if (__instance.WasActiveLastFrame)
{
__instance.InputStartedTime = float.MaxValue;
}
return false;
}
}

View File

@ -5,7 +5,6 @@ using QSB.ItemSync.WorldObjects.Items;
using QSB.ItemSync.WorldObjects.Sockets;
using QSB.Utility;
using QSB.WorldSync;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using UnityEngine;
@ -21,14 +20,13 @@ internal class ItemManager : WorldObjectManager
DebugLog.DebugWrite("Building OWItems...", MessageType.Info);
// Items
QSBWorldSync.Init<QSBDreamLanternItem, DreamLanternItem>();
QSBWorldSync.Init<QSBNomaiConversationStone, NomaiConversationStone>();
QSBWorldSync.Init<QSBScrollItem, ScrollItem>();
QSBWorldSync.Init<QSBSharedStone, SharedStone>();
QSBWorldSync.Init<QSBSimpleLanternItem, SimpleLanternItem>();
QSBWorldSync.Init<QSBSlideReelItem, SlideReelItem>();
QSBWorldSync.Init<QSBVisionTorchItem, VisionTorchItem>();
QSBWorldSync.Init<QSBWarpCoreItem, WarpCoreItem>();
// dream lantern and vision torch are set up in their own managers
// Sockets
QSBWorldSync.Init<QSBItemSocket, OWItemSocket>();

View File

@ -149,6 +149,12 @@ internal class ItemRemotePatches : QSBPatch
return true;
}
if (__instance._isProjecting)
{
__instance._mindProjectorTrigger.SetProjectorActive(false);
__instance._isProjecting = false;
}
// if (Locator.GetDreamWorldController().IsInDream())
// {
base_DropItem(__instance, position, normal, parent, sector, customDropTarget);
@ -213,6 +219,12 @@ internal class ItemRemotePatches : QSBPatch
return true;
}
if (__instance._isProjecting)
{
__instance._mindProjectorTrigger.SetProjectorActive(false);
__instance._isProjecting = false;
}
base_SocketItem(__instance, socketTransform, sector);
if (__instance._visionBeam != null)
{

View File

@ -1,6 +0,0 @@
namespace QSB.ItemSync.WorldObjects.Items;
/// <summary>
/// TODO: SYNC THIS SHIT LMAOOOOOO
/// </summary>
internal class QSBVisionTorchItem : QSBItem<VisionTorchItem> { }

View File

@ -2,7 +2,6 @@
using QSB.Utility;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
@ -82,28 +81,4 @@ public static class QSBLocalization
Current = newTranslation;
LanguageChanged?.Invoke();
}
public static CultureInfo CultureInfo
=> Current.Language switch
{
/*
* Language tags from BCP-47 standard, implemented by windows
* https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
* I have no fucking idea if this will work on linux. ¯\_()_/¯
*/
TextTranslation.Language.ENGLISH => new CultureInfo("en"),
TextTranslation.Language.SPANISH_LA => new CultureInfo("es-419"),
TextTranslation.Language.GERMAN => new CultureInfo("de"),
TextTranslation.Language.FRENCH => new CultureInfo("fr"),
TextTranslation.Language.ITALIAN => new CultureInfo("it"),
TextTranslation.Language.POLISH => new CultureInfo("pl"),
TextTranslation.Language.PORTUGUESE_BR => new CultureInfo("pt-BR"),
TextTranslation.Language.JAPANESE => new CultureInfo("ja"),
TextTranslation.Language.RUSSIAN => new CultureInfo("ru"),
TextTranslation.Language.CHINESE_SIMPLE => new CultureInfo("zh-Hans"),
TextTranslation.Language.KOREAN => new CultureInfo("ko"),
TextTranslation.Language.TURKISH => new CultureInfo("tr"),
_ => new CultureInfo("en") // what
};
}

View File

@ -42,7 +42,7 @@ internal class MenuManager : MonoBehaviour, IAddComponentOnStart
private const int _titleButtonIndex = 2;
private float _connectPopupOpenTime;
private const string UpdateChangelog = "QSB Version 0.22.0\r\nFixed lots of bugs, and added lots of SFX and VFX stuff.";
private const string UpdateChangelog = "QSB Version 0.23.0\r\nA lot of small improvements and bug fixes.";
private Action<bool> PopupClose;

View File

@ -74,6 +74,9 @@ internal class UseFlightConsoleMessage : QSBMessage<bool>
// Client messes up its position when they start flying it
// We can just recall it immediately so its in the right place.
var console = QSBWorldSync.GetUnityObject<RemoteFlightConsole>();
console.RespawnModelShip(false);
if (console._modelShipBody) // for when model ship is destroyed
{
console.RespawnModelShip(false);
}
}
}

View File

@ -1,9 +1,6 @@
using Mirror;
using QSB.Player;
using QSB.Utility;
using QSB.Player;
using QSB.Player.TransformSync;
using QSB.Utility.VariableSync;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace QSB.ModelShip;
@ -26,7 +23,7 @@ public class ModelShipThrusterVariableSyncer : MonoBehaviour
public void Update()
{
if (QSBPlayerManager.LocalPlayer.FlyingModelShip)
if (PlayerTransformSync.LocalInstance && QSBPlayerManager.LocalPlayer.FlyingModelShip)
{
GetFromShip();
return;

View File

@ -1,4 +1,5 @@
using QSB.Utility;
using QSB.ServerSettings;
using QSB.Utility;
using UnityEngine;
namespace QSB.Player;
@ -34,7 +35,16 @@ public class PlayerHUDMarker : HUDDistanceMarker
return false;
}
return _player.IsReady && !_player.IsDead && (!_player.InDreamWorld || QSBPlayerManager.LocalPlayer.InDreamWorld) && _player.Visible;
if (!ServerSettingsManager.ShowPlayerNames)
{
return false;
}
return _player.IsReady &&
!_player.IsDead &&
_player.Visible &&
_player.InDreamWorld == QSBPlayerManager.LocalPlayer.InDreamWorld &&
_player.IsInMoon == QSBPlayerManager.LocalPlayer.IsInMoon;
}
private void Update()

View File

@ -22,6 +22,7 @@ public partial class PlayerInfo
public uint PlayerId { get; }
public string Name { get; set; }
public PlayerHUDMarker HudMarker { get; set; }
public PlayerMapMarker MapMarker { get; set; }
public PlayerTransformSync TransformSync { get; }
public ClientState State { get; set; }
public EyeState EyeState { get; set; }

View File

@ -1,4 +1,6 @@
using QSB.ItemSync.WorldObjects.Items;
using QSB.EchoesOfTheEye.DreamLantern;
using QSB.EchoesOfTheEye.DreamLantern.WorldObjects;
using QSB.ItemSync.WorldObjects.Items;
namespace QSB.Player;

View File

@ -1,4 +1,5 @@
using QSB.Utility;
using QSB.ServerSettings;
using QSB.Utility;
using UnityEngine;
namespace QSB.Player;
@ -31,6 +32,7 @@ public class PlayerMapMarker : MonoBehaviour
public void Init(PlayerInfo player)
{
_player = player;
_player.MapMarker = this;
_hasBeenSetUpForInit = true;
}
@ -57,10 +59,20 @@ public class PlayerMapMarker : MonoBehaviour
return false;
}
if (!ServerSettingsManager.ShowPlayerNames)
{
return false;
}
var playerScreenPos = Locator.GetActiveCamera().WorldToScreenPoint(transform.position);
var isInfrontOfCamera = playerScreenPos.z > 0f;
return _player.IsReady && !_player.IsDead && (!_player.InDreamWorld || QSBPlayerManager.LocalPlayer.InDreamWorld) && _player.Visible && isInfrontOfCamera;
return isInfrontOfCamera &&
_player.IsReady &&
!_player.IsDead &&
_player.Visible &&
_player.InDreamWorld == QSBPlayerManager.LocalPlayer.InDreamWorld &&
_player.IsInMoon == QSBPlayerManager.LocalPlayer.IsInMoon;
}
public void LateUpdate()
@ -77,4 +89,9 @@ public class PlayerMapMarker : MonoBehaviour
_canvasMarker.SetVisibility(shouldBeVisible);
}
}
public void Remove()
{
// TODO
}
}

View File

@ -54,6 +54,7 @@ public class PlayerTransformSync : SectoredTransformSync
QSBPatch.Remote = false;
base.OnStopClient();
Player.HudMarker?.Remove();
Player.MapMarker?.Remove();
QSBPlayerManager.PlayerList.Remove(Player);
DebugLog.DebugWrite($"Remove Player : {Player}", MessageType.Info);
}

View File

@ -27,7 +27,20 @@ public static class ShaderReplacer
continue;
}
material.shader = replacementShader;
// preserve override tag and render queue (for Standard shader)
// keywords and properties are already preserved
if (material.renderQueue != material.shader.renderQueue)
{
var renderType = material.GetTag("RenderType", false);
var renderQueue = material.renderQueue;
material.shader = replacementShader;
material.SetOverrideTag("RenderType", renderType);
material.renderQueue = renderQueue;
}
else
{
material.shader = replacementShader;
}
}
}
}

View File

@ -7,17 +7,6 @@
<NoWarn>CS1998;CS0649</NoWarn>
</PropertyGroup>
<Target Name="clean before building" BeforeTargets="PreBuildEvent">
<ItemGroup>
<_Files Remove="@(_Files)" />
<_Files Include="$(OutputPath)\*.dll" />
<_Files Include="$(OutputPath)\*.exe" />
<_Files Include="$(OutputPath)\*.pdb" />
<_Files Include="$(OutputPath)\AssetBundles\*" />
</ItemGroup>
<Delete Files="@(_Files)" />
</Target>
<Target Name="clean after building" AfterTargets="PostBuildEvent">
<ItemGroup>
<_Files Remove="@(_Files)" />
@ -31,33 +20,13 @@
</Target>
<PropertyGroup>
<GameDllsDir Condition="Exists('$(GameDir)')">$(GameDir)\OuterWilds_Data\Managed</GameDllsDir>
<UnityDllsDir Condition="Exists('$(UnityAssetsDir)')">$(UnityAssetsDir)\Dlls</UnityDllsDir>
</PropertyGroup>
<Target Name="copy dlls to unity" AfterTargets="PostBuildEvent" Condition="Exists('$(UnityDllsDir)') and Exists('$(GameDllsDir)')">
<ItemGroup>
<_Files Remove="@(_Files)" />
<_Files Include="$(UnityDllsDir)\*.dll" />
<_Files Include="$(UnityDllsDir)\*.exe" />
<_Files Include="$(UnityDllsDir)\*.pdb" />
</ItemGroup>
<Delete Files="@(_Files)" />
<Target Name="copy dlls to unity" AfterTargets="PostBuildEvent" Condition="Exists('$(UnityDllsDir)')">
<ItemGroup>
<_Files Remove="@(_Files)" />
<_Files Include="$(OutputPath)\*.dll" />
<_Files Include="$(OutputPath)\*.exe" />
<_Files Include="$(OutputPath)\*.pdb" />
<_Files Include="$(GameDllsDir)\EOS-SDK.dll" />
<_Files Include="$(GameDllsDir)\Autofac.dll" />
<_Files Include="$(GameDllsDir)\Newtonsoft.Json.dll" />
<_Files Include="$(GameDllsDir)\0Harmony.dll" />
<_Files Include="$(GameDllsDir)\MonoMod*.dll" />
<_Files Include="$(GameDllsDir)\Mono.Cecil.dll" />
<_Files Include="$(GameDllsDir)\OWML*.dll" />
<_Files Include="$(GameDllsDir)\NAudio-Unity.dll" />
<_Files Include="$(GameDllsDir)\com.rlabrecque.steamworks.net.dll" />
</ItemGroup>
<Copy SourceFiles="@(_Files)" DestinationFolder="$(UnityDllsDir)" />
</Target>
@ -90,7 +59,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="OuterWildsGameLibs" Version="1.1.13.393" IncludeAssets="compile" />
<PackageReference Include="OWML" Version="2.7.0" IncludeAssets="compile" />
<PackageReference Include="OWML" Version="2.8.0" IncludeAssets="compile" />
<Reference Include="..\Mirror\*.dll" />
<Reference Include="..\UniTask\*.dll" />
<ProjectReference Include="..\EpicOnlineTransport\EpicOnlineTransport.csproj" />

View File

@ -4,9 +4,11 @@ using OWML.Common;
using OWML.ModHelper;
using QSB.Localization;
using QSB.Menus;
using QSB.Messaging;
using QSB.Patches;
using QSB.QuantumSync;
using QSB.SaveSync;
using QSB.ServerSettings;
using QSB.Utility;
using QSB.WorldSync;
using System;
@ -56,6 +58,8 @@ public class QSBCore : ModBehaviour
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 bool ShowPlayerNames { get; private set; }
public static bool ShipDamage { 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
@ -242,6 +246,14 @@ public class QSBCore : ModBehaviour
{
DefaultServerIP = config.GetSettingsValue<string>("defaultServerIP");
IncompatibleModsAllowed = config.GetSettingsValue<bool>("incompatibleModsAllowed");
ShowPlayerNames = config.GetSettingsValue<bool>("showPlayerNames");
ShipDamage = config.GetSettingsValue<bool>("shipDamage");
if (IsHost)
{
ServerSettingsManager.ServerShowPlayerNames = ShowPlayerNames;
new ServerSettingsMessage().Send();
}
}
private void Update()

View File

@ -315,7 +315,11 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
Destroy(GetComponent<RespawnOnDeath>());
Destroy(GetComponent<ServerStateManager>());
Destroy(GetComponent<ClientStateManager>());
QSBPlayerManager.PlayerList.ForEach(player => player.HudMarker?.Remove());
QSBPlayerManager.PlayerList.ForEach(player =>
{
player.HudMarker?.Remove();
player.MapMarker?.Remove();
});
QSBWorldSync.RemoveWorldObjects();
@ -397,7 +401,11 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart
DebugLog.DebugWrite("OnStopServer", MessageType.Info);
Destroy(GetComponent<RespawnOnDeath>());
DebugLog.ToConsole("Server stopped!", MessageType.Info);
QSBPlayerManager.PlayerList.ForEach(player => player.HudMarker?.Remove());
QSBPlayerManager.PlayerList.ForEach(player =>
{
player.HudMarker?.Remove();
player.MapMarker?.Remove();
});
base.OnStopServer();
}

View File

@ -12,7 +12,7 @@ public class RespawnHUDMarker : HUDDistanceMarker
{
_markerRadius = 0.2f;
_markerTarget = transform;
_markerLabel = QSBLocalization.Current.RespawnPlayer.ToUpper(QSBLocalization.CultureInfo);
_markerLabel = QSBLocalization.Current.RespawnPlayer.ToUpper();
_isReady = true;
base.InitCanvasMarker();

View File

@ -1,6 +1,7 @@
using QSB.ConversationSync.Messages;
using QSB.Messaging;
using QSB.Player;
using QSB.ServerSettings;
using QSB.Utility;
namespace QSB.SaveSync.Messages;
@ -21,6 +22,7 @@ internal class RequestGameStateMessage : QSBMessage
}
new GameStateMessage(From).Send();
new ServerSettingsMessage().Send();
var gameSave = PlayerData._currentGameSave;

View File

@ -0,0 +1,10 @@
using QSB.Utility;
using UnityEngine;
namespace QSB.ServerSettings;
internal class ServerSettingsManager : MonoBehaviour, IAddComponentOnStart
{
public static bool ServerShowPlayerNames;
public static bool ShowPlayerNames => (ServerShowPlayerNames || QSBCore.IsHost) && QSBCore.ShowPlayerNames;
}

View File

@ -0,0 +1,27 @@
using Mirror;
using QSB.Messaging;
namespace QSB.ServerSettings;
internal class ServerSettingsMessage : QSBMessage
{
private bool _showPlayerNames;
public ServerSettingsMessage()
=> _showPlayerNames = QSBCore.ShowPlayerNames;
public override void Serialize(NetworkWriter writer)
{
base.Serialize(writer);
writer.Write(_showPlayerNames);
}
public override void Deserialize(NetworkReader reader)
{
base.Deserialize(reader);
_showPlayerNames = reader.ReadBool();
}
public override void OnReceiveRemote()
=> ServerSettingsManager.ServerShowPlayerNames = _showPlayerNames;
}

View File

@ -1,7 +1,7 @@
using Mirror;
using QSB.Player;
using QSB.Player.TransformSync;
using QSB.Utility.VariableSync;
using QSB.WorldSync;
using UnityEngine;
namespace QSB.ShipSync;
@ -21,7 +21,7 @@ public class ShipThrusterVariableSyncer : NetworkBehaviour
public void Update()
{
if (QSBPlayerManager.LocalPlayer.FlyingShip)
if (PlayerTransformSync.LocalInstance && QSBPlayerManager.LocalPlayer.FlyingShip)
{
GetFromShip();
return;

View File

@ -4,6 +4,7 @@ using QSB.Messaging;
using QSB.Patches;
using QSB.TimeSync.Messages;
using QSB.Utility;
using UnityEngine;
namespace QSB.TimeSync.Patches;
@ -25,8 +26,8 @@ internal class TimePatches : QSBPatch
public static void PlayerCameraEffectController_WakeUp(PlayerCameraEffectController __instance)
{
// prevent funny thing when you pause while waking up
QSBInputManager.Instance.SetInputsEnabled(false);
Delay.RunWhen(() => !__instance._isOpeningEyes, () => QSBInputManager.Instance.SetInputsEnabled(true));
Locator.GetPauseCommandListener().AddPauseCommandLock();
Delay.RunWhen(() => !__instance._isOpeningEyes, () => Locator.GetPauseCommandListener().RemovePauseCommandLock());
}
[HarmonyPrefix]

View File

@ -8,6 +8,7 @@ using QSB.Messaging;
using QSB.Player;
using QSB.Player.Messages;
using QSB.TimeSync.Messages;
using QSB.TimeSync.Patches;
using QSB.Utility;
using QSB.WorldSync;
using System;
@ -46,7 +47,6 @@ public class WakeUpSync : NetworkBehaviour
{
OWTime.SetTimeScale(1f);
OWTime.SetMaxDeltaTime(0.06666667f);
OWTime.SetFixedTimestep(0.01666667f);
Locator.GetActiveCamera().enabled = true;
CurrentState = State.NotLoaded;
CurrentReason = null;
@ -214,7 +214,6 @@ public class WakeUpSync : NetworkBehaviour
CurrentState = State.FastForwarding;
CurrentReason = reason;
OWTime.SetMaxDeltaTime(0.033333335f);
OWTime.SetFixedTimestep(0.033333335f);
TimeSyncUI.TargetTime = _serverTime;
TimeSyncUI.Start(TimeSyncType.FastForwarding, reason);
}
@ -245,7 +244,6 @@ public class WakeUpSync : NetworkBehaviour
{
OWTime.SetTimeScale(1f);
OWTime.SetMaxDeltaTime(0.06666667f);
OWTime.SetFixedTimestep(0.01666667f);
Locator.GetActiveCamera().enabled = true;
CurrentState = State.Loaded;
CurrentReason = null;

126
QSB/Translations/zh_CN.json Normal file
View File

@ -0,0 +1,126 @@
{
"Language": "CHINESE_SIMPLE",
"MainMenuHost": "开启多人游戏",
"MainMenuConnect": "连接至多人游戏",
"PauseMenuDisconnect": "断开连接",
"PauseMenuStopHosting": "关闭服务器",
"PublicIPAddress": "公共IP地址\n\n您的多人存档数据将被覆盖",
"ProductUserID": "用户ID\n\n您的多人存档数据将被覆盖",
"Connect": "连接",
"Cancel": "取消",
"HostExistingOrNewOrCopy": "您想使用一个现有的多人探险存档,或是开启一个新的存档,还是把现有的单人探险存档复制到多人探险存档?",
"HostNewOrCopy": "您想开启一个新的存档,还是把现有的单人探险存档复制到多人探险存档?",
"HostExistingOrNew": "您想使用一个现有的多人探险存档,还是开启一个新的存档?",
"ExistingSave": "现有存档",
"NewSave": "新建存档",
"CopySave": "复制存档",
"DisconnectAreYouSure": "您确定要断开连接吗?\n将会退出至主菜单。",
"Yes": "是",
"No": "否",
"StopHostingAreYouSure": "您确定要停止服务器吗?\n将会与所有人断开连接并且使他们退出至主菜单。",
"CopyProductUserIDToClipboard": "开启服务器\n其他玩家需要使用您的用户ID连接至服务器用户ID是\n{0}\n您想要复制到剪切板吗",
"Connecting": "连接中……",
"OK": "好的",
"ServerRefusedConnection": "服务器拒绝了您的连接\n{0}",
"ClientDisconnectWithError": "客户端发生错误导致连接断开\n{0}",
"QSBVersionMismatch": "QSB版本号不匹配。客户端{0},服务端:{1}",
"OWVersionMismatch": "《星际拓荒》版本号不匹配。(客户端:{0},服务端:{1}",
"DLCMismatch": "DLC安装情况不匹配。客户端{0},服务端:{1}",
"GameProgressLimit": "游戏中时间太久了。",
"AddonMismatch": "插件不匹配(客户端:{0}插件,服务端:{1}插件)",
"IncompatibleMod": "使用了不兼容/不允许的模组,检测到的第一个模组是{0}",
"PlayerJoinedTheGame": "{0}加入了游戏!",
"PlayerWasKicked": "{0}被踢出了游戏。",
"KickedFromServer": "被踢出了游戏,理由是:{0}",
"RespawnPlayer": "复活玩家",
"TimeSyncTooFarBehind": "{0}\n快进以匹配服务器时间……",
"TimeSyncWaitingForStartOfServer": "等待服务器开始……",
"TimeSyncTooFarAhead": "{0}\n暂停等待以匹配服务器时间……",
"TimeSyncWaitForAllToReady": "等待开始轮回……",
"TimeSyncWaitForAllToDie": "等待轮回结束……",
"GalaxyMapEveryoneNotPresent": "现在还不是时候。需要所有人见证这个时刻。",
"YouAreDead": "你死了。",
"WaitingForRespawn": "等待某人将你复活……",
"WaitingForAllToDie": "等待{0}死亡……",
"AttachToShip": "固定在飞船上",
"DetachFromShip": "取消固定",
"DeathMessages": {
"Default": [
"{0}死了",
"{0}被杀了"
],
"Impact": [
"{0}忘记使用推进器了",
"{0}拥抱了地面",
"{0}狠狠的撞上了地面",
"{0}爆炸了",
"{0}因为撞击而死",
"{0}撞击地面过猛"
],
"Asphyxiation": [
"{0}忘记了需要呼吸",
"{0}窒息了",
"{0}窒息而死",
"{0}忘记了怎么呼吸",
"{0}忘记检查氧气了",
"{0}把空气用完了",
"{0}把氧气用完了",
"{0}不再需要呼吸了"
],
"Energy": [
"{0}被烹饪了"
],
"Supernova": [
"{0}没有时间了",
"{0}被烧掉了",
"{0}被超新星烤熟了",
"{0}升华了",
"{0}遗忘了时间"
],
"Digestion": [
"{0}被吃掉了",
"{0}发现了一条鱼",
"{0}遇到了邪恶生物",
"{0}惹错了鱼",
"{0}被消化了",
"{0}因为消化系统而死"
],
"Crushed": [
"{0}被压死了",
"{0}被压扁了",
"{0}埋葬了自己",
"{0}没能及时逃离",
"{0}想在沙子中游泳",
"{0}小看了沙子的力量",
"{0}卡在沙子下面了"
],
"Lava": [
"{0}在岩浆中死去",
"{0}融化了",
"{0}尝试在岩浆中游泳",
"{0}掉进了岩浆",
"{0}因为岩浆而死",
"{0}在岩浆中游泳",
"{0}被岩浆烧毁"
],
"BlackHole": [
"{0}尝试追寻自己的记忆"
],
"DreamExplosion": [
"{0}爆炸了",
"{0}是第一个吃螃蟹的人",
"{0}被炸死了",
"{0}被炸了",
"{0}因为爆炸而死",
"{0}使用了错误的文物"
],
"CrushedByElevator": [
"{0}被粉碎了",
"{0}被挤扁了",
"{0}被电梯撞击",
"{0}站在了电梯下",
"{0}变成了一个平板人",
"{0}被电梯挤扁了"
]
}
}

View File

@ -1,4 +1,6 @@
using OWML.Common;
using QSB.EchoesOfTheEye.DreamLantern;
using QSB.EchoesOfTheEye.DreamLantern.WorldObjects;
using QSB.ItemSync.WorldObjects.Items;
using QSB.Messaging;
using QSB.Player;

View File

@ -5,6 +5,8 @@ using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
#pragma warning disable CS0618
namespace QSB.Utility;
public static class DebugLog

View File

@ -31,11 +31,11 @@ public static class DeterministicManager
}
};
public static void OnWorldObjectsReady()
public static void OnWorldObjectsAdded()
{
if (QSBCore.DebugSettings.DumpWorldObjects)
{
using (var file = File.CreateText(Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, "world objects.csv")))
using (var file = File.CreateText(Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, $"[{DebugLog.ProcessInstanceId}] world objects.csv")))
{
file.WriteLine("world object,deterministic path");
foreach (var worldObject in QSBWorldSync.GetWorldObjects())
@ -51,7 +51,7 @@ public static class DeterministicManager
}
}
using (var file = File.CreateText(Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, "cache.csv")))
using (var file = File.CreateText(Path.Combine(QSBCore.Helper.Manifest.ModFolderPath, $"[{DebugLog.ProcessInstanceId}] cache.csv")))
{
file.WriteLine("name,instance id,sibling index,parent,parent instance id");
foreach (var (transform, (siblingIndex, parent)) in _cache)
@ -267,7 +267,7 @@ public static class DeterministicManager
}
/// <summary>
/// only call this before world objects ready
/// only call this before world objects added
/// </summary>
public static string DeterministicPath(this Component component)
{
@ -298,7 +298,7 @@ public static class DeterministicManager
}
/// <summary>
/// only call this before world objects ready
/// only call this before world objects added
/// </summary>
public static IEnumerable<T> SortDeterministic<T>(this IEnumerable<T> components) where T : Component
=> components.OrderBy(DeterministicPath);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using Object = UnityEngine.Object;
@ -214,5 +215,23 @@ public static class Extensions
}
}
// https://stackoverflow.com/a/24031467
public static string GetMD5Hash(this IEnumerable<string> list)
{
using var md5 = System.Security.Cryptography.MD5.Create();
var longString = string.Concat(list);
var bytes = Encoding.ASCII.GetBytes(longString);
var hashBytes = md5.ComputeHash(bytes);
var sb = new StringBuilder();
for (var i = 0; i < hashBytes.Length; i++)
{
sb.Append(hashBytes[i].ToString("X2"));
}
return sb.ToString();
}
#endregion
}

View File

@ -4,6 +4,7 @@ using UnityEngine.UI;
namespace QSB.Utility;
[UsedInUnityProject]
public class ZOverride : MonoBehaviour
{
private const string shaderTestMode = "unity_GUIZTestMode";

View File

@ -20,6 +20,7 @@ namespace QSB.WorldSync;
public static class QSBWorldSync
{
public static WorldObjectManager[] Managers;
public static string WorldObjectsHash { get; private set; }
/// <summary>
/// Set when all WorldObjectManagers have called Init() on all their objects (AKA all the objects are created)
@ -82,8 +83,14 @@ public static class QSBWorldSync
AllObjectsAdded = true;
DebugLog.DebugWrite("World Objects added.", MessageType.Success);
DeterministicManager.OnWorldObjectsAdded();
WorldObjectsHash = WorldObjects.Select(x => x.GetType().Name).GetMD5Hash();
DebugLog.DebugWrite($"WorldObject hash is {WorldObjectsHash}");
if (!QSBCore.IsHost)
{
new WorldObjectsHashMessage().Send();
new RequestLinksMessage().Send();
}
@ -96,8 +103,6 @@ public static class QSBWorldSync
AllObjectsReady = true;
DebugLog.DebugWrite("World Objects ready.", MessageType.Success);
DeterministicManager.OnWorldObjectsReady();
if (!QSBCore.IsHost)
{
new RequestInitialStatesMessage().Send();
@ -247,7 +252,7 @@ public static class QSBWorldSync
if (WorldObjects[objectId] is not TWorldObject worldObject)
{
DebugLog.ToConsole($"Error - {typeof(TWorldObject).Name} id {objectId} is actually {WorldObjects[objectId].GetType().Name}.", MessageType.Error);
DebugLog.ToConsole($"Error - WorldObject id {objectId} is {WorldObjects[objectId].GetType().Name}, expected {typeof(TWorldObject).Name}.", MessageType.Error);
return default;
}

View File

@ -0,0 +1,30 @@
using OWML.Common;
using QSB.Messaging;
using QSB.Player.Messages;
using QSB.Utility;
namespace QSB.WorldSync;
/// <summary>
/// sends QSBWorldSync.WorldObjectsHash to the server for sanity checking
/// </summary>
internal class WorldObjectsHashMessage : QSBMessage<string>
{
public WorldObjectsHashMessage() : base(QSBWorldSync.WorldObjectsHash) => To = 0;
public override void OnReceiveRemote()
{
var serverHash = QSBWorldSync.WorldObjectsHash;
if (serverHash != Data)
{
// oh fuck oh no oh god
DebugLog.ToConsole($"Kicking {From} because their WorldObjects hash is wrong. (server:{serverHash}, client:{Data})", MessageType.Error);
new PlayerKickMessage(From, $"WorldObject hash error. (Server:{serverHash}, Client:{Data})").Send();
}
else
{
DebugLog.DebugWrite($"WorldObject hash from {From} verified!", MessageType.Success);
}
}
}

View File

@ -1,7 +1,29 @@
{
"enabled": true,
"settings": {
"defaultServerIP": "localhost",
"incompatibleModsAllowed": false
"defaultServerIP": {
"title": "Last Entered IP/ID",
"type": "text",
"value": "localhost",
"tooltip": "Used if you leave the connect prompt blank."
},
"incompatibleModsAllowed": {
"title": "Incompatible Mods Allowed",
"type": "toggle",
"value": false,
"tooltip": "Kicks players if they have certain mods."
},
"showPlayerNames": {
"title": "Show Player Names",
"type": "toggle",
"value": true,
"tooltip": "Shows player names in the HUD and the map view."
},
"shipDamage": {
"title": "Ship Damage",
"type": "toggle",
"value": true,
"tooltip": "Take impact damage when inside the ship."
}
}
}

View File

@ -7,8 +7,8 @@
"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.22.0",
"owmlVersion": "2.7.0",
"version": "0.23.0",
"owmlVersion": "2.8.0",
"dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ],
"pathsToPreserve": [ "debugsettings.json", "storage.json" ]
}

View File

@ -99,7 +99,6 @@ See [TRANSLATING.md](TRANSLATING.md)
- Clone QSB's source
- Open the file `DevEnv.targets` in your favorite text editor
- (optional if copying built dlls manually) Edit the entry `<OwmlDir>` to point to your OWML directory (it is installed inside the Mod Manager directory)
- (optional if no unity project) Edit the entry `<GameDir>` to point to the directory where Outer Wilds is installed
- (optional if no unity project) Edit the entry `<UnityAssetsDir>` to point to the Assets folder of the QSB unity project
- Open the project solution file `QSB.sln` in Visual Studio 2022
@ -171,7 +170,7 @@ The template for this file is this :
### Authors
- [\_nebula](https://github.com/misternebula) - Developer of v0.3 onwards
- [\_nebula](https://github.com/misternebula) - Developer of v0.3.0 onwards
- [JohnCorby](https://github.com/JohnCorby) - Co-developer of v0.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.
@ -180,9 +179,10 @@ The template for this file is this :
- [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.
- [Xen](https://github.com/xen-42) - French translation, and help with particle effects and sounds.
- [ShoosGun](https://github.com/ShoosGun) - Portuguese translation.
- [DertolleDude](https://github.com/DertolleDude) - German translation.
- [SakuradaYuki](https://github.com/SakuradaYuki) - Chinese 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.

View File

@ -10,13 +10,13 @@ QSB can only be translated to the languages Outer Wilds supports - so if you don
- Russian
- Portuguese (Brazil)
- German
- Chinese (Simplified)
### Un-translated languages :
- Spanish (Latin American)
- Italian
- Polish
- Japanese
- Chinese (Simplified)
- Korean
- Turkish