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
@ -38,4 +38,4 @@ Once you are happy with it, go to [this page](https://github.com/misternebula/qu
Scroll down to the "Propose new file" box. In the title box (default "Create new file") write something along the lines of "Add translation for (language)".
Press the big green "Propose new file", and we'll review it soon!
Press the big green "Propose new file", and we'll review it soon!