diff --git a/LevelImposter/Assets/Assets b/LevelImposter/Assets/Assets index 8e5be716..fcaa5b1c 100644 Binary files a/LevelImposter/Assets/Assets and b/LevelImposter/Assets/Assets differ diff --git a/LevelImposter/Assets/Assets.manifest b/LevelImposter/Assets/Assets.manifest index 801ecd57..d1a8d272 100644 --- a/LevelImposter/Assets/Assets.manifest +++ b/LevelImposter/Assets/Assets.manifest @@ -1,7 +1,10 @@ ManifestFileVersion: 0 -CRC: 2386985233 +CRC: 2742641410 AssetBundleManifest: AssetBundleInfos: Info_0: Name: shop Dependencies: {} + Info_1: + Name: loadingbar + Dependencies: {} diff --git a/LevelImposter/Assets/loadingbar b/LevelImposter/Assets/loadingbar new file mode 100644 index 00000000..bf421337 Binary files /dev/null and b/LevelImposter/Assets/loadingbar differ diff --git a/LevelImposter/Assets/loadingbar.manifest b/LevelImposter/Assets/loadingbar.manifest new file mode 100644 index 00000000..9f2d819e --- /dev/null +++ b/LevelImposter/Assets/loadingbar.manifest @@ -0,0 +1,77 @@ +ManifestFileVersion: 0 +CRC: 3049509670 +Hashes: + AssetFileHash: + serializedVersion: 2 + Hash: 2e5e38824854ae56f754be848fb8fa52 + TypeTreeHash: + serializedVersion: 2 + Hash: 7c653b85788f9e6027325f678c0f4f02 +HashAppended: 0 +ClassTypes: +- Class: 1 + Script: {instanceID: 0} +- Class: 4 + Script: {instanceID: 0} +- Class: 21 + Script: {instanceID: 0} +- Class: 23 + Script: {instanceID: 0} +- Class: 28 + Script: {instanceID: 0} +- Class: 48 + Script: {instanceID: 0} +- Class: 114 + Script: {fileID: 11500000, guid: 9541d86e2fd84c1d9990edf0852d74ab, type: 3} +- Class: 114 + Script: {fileID: 11500000, guid: 71c1514a6bd24e1e882cebbe1904ce04, type: 3} +- Class: 114 + Script: {fileID: 11500000, guid: e27d931aa83cda344a1633cd123a82c9, type: 3} +- Class: 115 + Script: {instanceID: 0} +- Class: 128 + Script: {instanceID: 0} +- Class: 212 + Script: {instanceID: 0} +- Class: 213 + Script: {instanceID: 0} +- Class: 224 + Script: {instanceID: 0} +- Class: 331 + Script: {instanceID: 0} +SerializeReferenceClassIdentifiers: +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.FaceInfo_Legacy +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.FontAssetCreationSettings +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.KerningTable +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.TMP_Character +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.TMP_FontFeatureTable +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.TMP_FontWeightPair +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.TMP_GlyphAdjustmentRecord +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.TMP_GlyphPairAdjustmentRecord +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.TMP_GlyphValueRecord +- AssemblyName: Unity.TextMeshPro + ClassName: TMPro.VertexGradient +- AssemblyName: UnityEngine.CoreModule + ClassName: UnityEngine.Events.PersistentCallGroup +- AssemblyName: UnityEngine.TextCoreModule + ClassName: UnityEngine.TextCore.FaceInfo +- AssemblyName: UnityEngine.TextCoreModule + ClassName: UnityEngine.TextCore.Glyph +- AssemblyName: UnityEngine.TextCoreModule + ClassName: UnityEngine.TextCore.GlyphMetrics +- AssemblyName: UnityEngine.TextCoreModule + ClassName: UnityEngine.TextCore.GlyphRect +- AssemblyName: UnityEngine.UI + ClassName: UnityEngine.UI.MaskableGraphic/CullStateChangedEvent +Assets: +- Assets/Prefabs/LoadingBar.prefab +Dependencies: [] diff --git a/LevelImposter/Assets/shop b/LevelImposter/Assets/shop index 1081ba73..87721262 100644 Binary files a/LevelImposter/Assets/shop and b/LevelImposter/Assets/shop differ diff --git a/LevelImposter/Assets/shop.manifest b/LevelImposter/Assets/shop.manifest index 0b107693..9553dfe7 100644 --- a/LevelImposter/Assets/shop.manifest +++ b/LevelImposter/Assets/shop.manifest @@ -1,9 +1,9 @@ ManifestFileVersion: 0 -CRC: 3599790525 +CRC: 4005544444 Hashes: AssetFileHash: serializedVersion: 2 - Hash: e2dbbc72aa56e5fcbee1c2078a7f2c27 + Hash: 95736c394c97a65fc511c7395fb99918 TypeTreeHash: serializedVersion: 2 Hash: afd525e58be886efe4d69cac465cee90 diff --git a/LevelImposter/Builders/BuildRouter.cs b/LevelImposter/Builders/BuildRouter.cs index 7a9fb83a..0c86611a 100644 --- a/LevelImposter/Builders/BuildRouter.cs +++ b/LevelImposter/Builders/BuildRouter.cs @@ -53,11 +53,20 @@ public class BuildRouter new TriggerAreaBuilder(), new TriggerConsoleBuilder(), new TriggerStartBuilder(), + new TriggerDeathBuilder(), + new TriggerShakeBuilder(), new TriggerBuilder(), + new CustomTextBuilder(), new ColorBuilder() }; + /// + /// Patch me to add your own custom builders. + /// Builders should implement IElemBuilder. + /// + public BuildRouter() { } + /// /// Passes LIElement data through the build /// stack to construct a GameObject. diff --git a/LevelImposter/Builders/Generic/CustomTextBuilder.cs b/LevelImposter/Builders/Generic/CustomTextBuilder.cs new file mode 100644 index 00000000..c4bb5035 --- /dev/null +++ b/LevelImposter/Builders/Generic/CustomTextBuilder.cs @@ -0,0 +1,68 @@ +using LevelImposter.Core; +using System.Collections.Generic; +using UnityEngine; + +namespace LevelImposter.Builders +{ + /// + /// Replaces String in the Translation Controller with Custom Text + /// + public class CustomTextBuilder : IElemBuilder + { + private readonly Dictionary _customTextDB = new Dictionary + { + { "MedHello", StringNames.MedHello }, + { "SamplesPress", StringNames.SamplesPress }, + { "SamplesSelect", StringNames.SamplesSelect }, + { "MedETA", StringNames.MedETA }, + { "BadResult", StringNames.BadResult }, + { "SamplesThanks", StringNames.SamplesThanks }, + { "SamplesComplete", StringNames.SamplesComplete }, + { "More", StringNames.More }, + { "SamplesAdding", StringNames.SamplesAdding }, + { "TakeBreak", StringNames.TakeBreak }, + { "GrabCoffee", StringNames.GrabCoffee }, + { "DontNeedWait", StringNames.DontNeedWait }, + { "DoSomethingElse", StringNames.DoSomethingElse }, + { "ReactorNominal", StringNames.ReactorNominal }, + { "ReactorHoldToStop", StringNames.ReactorHoldToStop }, + { "ReactorWaiting", StringNames.ReactorWaiting }, + }; + + public void Build(LIElement elem, GameObject obj) + { + // Get Custom Text + var customText = elem.properties.customText; + if (customText == null || customText.Count <= 0) + return; + + // ShipStatus + var shipStatus = LIShipStatus.Instance; + if (shipStatus == null) + throw new MissingShipException(); + + // Replace Custom Text + foreach (var (textID, text) in customText) + { + // Skip Empty Text + if (string.IsNullOrEmpty(textID) || string.IsNullOrEmpty(text)) + continue; + + // Find String Name + bool hasTextID = _customTextDB.TryGetValue(textID, out StringNames stringName); + if (!hasTextID) + { + LILogger.Warn($"Unknown custom text '{textID}'"); + continue; + } + + // Replace Text + shipStatus.Renames.Add(stringName, text); + LILogger.Info($"Custom Text '{stringName}' >>> '{text}'"); + } + + } + + public void PostBuild() { } + } +} diff --git a/LevelImposter/Builders/Sab/SabConsoleBuilder.cs b/LevelImposter/Builders/Sab/SabConsoleBuilder.cs index b467cfc6..4206d519 100644 --- a/LevelImposter/Builders/Sab/SabConsoleBuilder.cs +++ b/LevelImposter/Builders/Sab/SabConsoleBuilder.cs @@ -52,7 +52,7 @@ public void Build(LIElement elem, GameObject obj) Console console = obj.AddComponent(); console.ConsoleId = 0; console.Image = spriteRenderer; - console.onlyFromBelow = elem.properties.onlyFromBelow == null ? true : (bool)elem.properties.onlyFromBelow; + console.onlyFromBelow = elem.properties.onlyFromBelow ?? true; console.usableDistance = elem.properties.range ?? 1.0f; console.Room = systemType; console.TaskTypes = prefabConsole.TaskTypes; diff --git a/LevelImposter/Builders/Trigger/TriggerDeathBuilder.cs b/LevelImposter/Builders/Trigger/TriggerDeathBuilder.cs new file mode 100644 index 00000000..bfe52f3b --- /dev/null +++ b/LevelImposter/Builders/Trigger/TriggerDeathBuilder.cs @@ -0,0 +1,25 @@ +using LevelImposter.Core; +using UnityEngine; + +namespace LevelImposter.Builders +{ + public class TriggerDeathBuilder : IElemBuilder + { + public void Build(LIElement elem, GameObject obj) + { + if (elem.type != "util-triggerdeath") + return; + + // Colliders + Collider2D[] colliders = obj.GetComponentsInChildren(); + foreach (Collider2D collider in colliders) + collider.isTrigger = true; + + // Trigger Area + LIDeathArea deathArea = obj.AddComponent(); + deathArea.SetCreateDeadBody(elem.properties.createDeadBody ?? true); + } + + public void PostBuild() { } + } +} diff --git a/LevelImposter/Builders/Trigger/TriggerShakeBuilder.cs b/LevelImposter/Builders/Trigger/TriggerShakeBuilder.cs new file mode 100644 index 00000000..7478961a --- /dev/null +++ b/LevelImposter/Builders/Trigger/TriggerShakeBuilder.cs @@ -0,0 +1,28 @@ +using LevelImposter.Core; +using UnityEngine; + +namespace LevelImposter.Builders +{ + public class TriggerShakeBuilder : IElemBuilder + { + public void Build(LIElement elem, GameObject obj) + { + if (elem.type != "util-triggershake") + return; + + // Colliders + Collider2D[] colliders = obj.GetComponentsInChildren(); + foreach (Collider2D collider in colliders) + collider.isTrigger = true; + + // Trigger Area + LIShakeArea shakeArea = obj.AddComponent(); + shakeArea.SetParameters( + elem.properties.shakeAmount ?? 0.03f, + elem.properties.shakePeriod ?? 400.0f + ); + } + + public void PostBuild() { } + } +} diff --git a/LevelImposter/Core/Components/GIFAnimator.cs b/LevelImposter/Core/Components/GIFAnimator.cs index 2d72910a..46464402 100644 --- a/LevelImposter/Core/Components/GIFAnimator.cs +++ b/LevelImposter/Core/Components/GIFAnimator.cs @@ -33,6 +33,7 @@ public GIFAnimator(IntPtr intPtr) : base(intPtr) private bool _defaultLoopGIF = false; private bool _isAnimating = false; + private int _frame = 0; private GIFFile? _gifData = null; private SpriteRenderer? _spriteRenderer; private Coroutine? _animationCoroutine = null; @@ -99,7 +100,8 @@ public void Stop(bool reversed = false) if (_spriteRenderer != null && _gifData != null) { - _spriteRenderer.sprite = _gifData.GetFrameSprite(reversed ? _gifData.Frames.Count - 1 : 0); + _frame = reversed ? _gifData.Frames.Count - 1 : 0; + _spriteRenderer.sprite = _gifData.GetFrameSprite(_frame); _spriteRenderer.enabled = true; } } @@ -117,7 +119,6 @@ private IEnumerator CoAnimate(bool repeat, bool reverse) yield break; _isAnimating = true; _spriteRenderer.enabled = true; - int t = 0; while (_isAnimating) { // Wait for main thread @@ -125,15 +126,20 @@ private IEnumerator CoAnimate(bool repeat, bool reverse) yield return null; // Render sprite - int frame = reverse ? _gifData.Frames.Count - t - 1 : t; - _spriteRenderer.sprite = _gifData.GetFrameSprite(frame); + _spriteRenderer.sprite = _gifData.GetFrameSprite(_frame); // Wait for next frame - yield return new WaitForSeconds(_gifData.Frames[frame].Delay); + yield return new WaitForSeconds(_gifData.Frames[_frame].Delay); - // Update time - t = (t + 1) % _gifData.Frames.Count; - if (t == 0 && !repeat) + // Update frame index + _frame = reverse ? _frame - 1 : _frame + 1; + + // Keep frame in bounds + bool isOutOfBounds = _frame < 0 || _frame >= _gifData.Frames.Count; + _frame = (_frame + _gifData.Frames.Count) % _gifData.Frames.Count; + + // Stop if out of bounds + if (isOutOfBounds && !repeat) Stop(!reverse); } } diff --git a/LevelImposter/Core/Components/LIDeathArea.cs b/LevelImposter/Core/Components/LIDeathArea.cs new file mode 100644 index 00000000..1e13d7ef --- /dev/null +++ b/LevelImposter/Core/Components/LIDeathArea.cs @@ -0,0 +1,94 @@ +using Reactor.Networking.Attributes; +using System; +using System.Linq; +using UnityEngine; + +namespace LevelImposter.Core +{ + /// + /// Object that kills all players that enter it's range + /// + public class LIDeathArea : PlayerArea + { + public LIDeathArea(IntPtr intPtr) : base(intPtr) + { + } + + private bool _createDeadBody = true; + + public void SetCreateDeadBody(bool value) + { + _createDeadBody = value; + } + + public void KillAllPlayers() + { + if (CurrentPlayersIDs == null) + return; + + // Only the host can handle kill triggers? + if (!AmongUsClient.Instance.AmHost) + return; + + // Iterate over all players in the area + byte[] playerIDs = CurrentPlayersIDs.ToArray(); // <-- Copy to avoid mutation during iteration + foreach (byte playerID in playerIDs) + { + // Get Player by ID + PlayerControl? player = GetPlayer(playerID); + if (player == null) + continue; + + // Fire RPC to kill player + RPCTriggerDeath(player, _createDeadBody); + } + } + + [MethodRpc((uint)LIRpc.KillPlayer)] + public static void RPCTriggerDeath(PlayerControl player, bool createDeadBody) + { + if (player == null || player.Data.IsDead) + return; + LILogger.Info($"[RPC] Trigger killing {player.name}"); + + // Kill Player + player.Die(DeathReason.Kill, false); + + // Play Kill Sound (if I'm the Player) + if (player.AmOwner) + PlayKillSound(); + + // Create Dead Body + if (createDeadBody) + CreateDeadBody(player); + } + + private static void CreateDeadBody(PlayerControl player) + { + // Create/Disable Dead Body + DeadBody deadBody = Instantiate(GameManager.Instance.DeadBodyPrefab); + deadBody.enabled = false; + deadBody.ParentId = player.PlayerId; + + // Set Colors + foreach (SpriteRenderer renderer in deadBody.bodyRenderers) + player.SetPlayerMaterialColors(renderer); + player.SetPlayerMaterialColors(deadBody.bloodSplatter); + + // Set Offset + Vector3 bodyOffset = player.KillAnimations.First().BodyOffset; + Vector3 bodyPosition = player.transform.position + bodyOffset; + bodyPosition.z = bodyPosition.y / 1000f; + deadBody.transform.position = bodyPosition; + + // Enable Dead Body + deadBody.enabled = true; + } + + private static void PlayKillSound() + { + AudioClip killSFX = PlayerControl.LocalPlayer.KillSfx; + SoundManager.Instance.PlaySound(killSFX, false, 0.8f); + } + } +} diff --git a/LevelImposter/Core/Components/LIShakeArea.cs b/LevelImposter/Core/Components/LIShakeArea.cs new file mode 100644 index 00000000..4817e7f2 --- /dev/null +++ b/LevelImposter/Core/Components/LIShakeArea.cs @@ -0,0 +1,53 @@ +using System; +using UnityEngine; + +namespace LevelImposter.Core +{ + /// + /// Object that shakes the screen when players enter it's range + /// + public class LIShakeArea : PlayerArea + { + public LIShakeArea(IntPtr intPtr) : base(intPtr) + { + } + + private float _shakeAmount = 0.03f; + private float _shakePeriod = 400.0f; + + public void SetParameters(float shakeAmount, float shakePeriod) + { + _shakeAmount = shakeAmount; + _shakePeriod = shakePeriod; + } + + public void SetEnabled(bool enabled) + { + this.enabled = enabled; + SetShakeEnabled(enabled && IsLocalPlayerInside); + } + + private void SetShakeEnabled(bool enabled) + { + FollowerCamera camera = Camera.main.GetComponent(); + if (camera != null) + { + camera.shakeAmount = enabled ? _shakeAmount : 0.0f; + camera.shakePeriod = enabled ? _shakePeriod : 0.0f; + } + } + + protected override void OnPlayerEnter(PlayerControl player) + { + LILogger.Info("Player Entered"); + if (player.AmOwner) + SetShakeEnabled(true); + } + + protected override void OnPlayerExit(PlayerControl player) + { + if (player.AmOwner) + SetShakeEnabled(false); + } + } +} diff --git a/LevelImposter/Core/Components/LIShipStatus.cs b/LevelImposter/Core/Components/LIShipStatus.cs index 5114962b..29a7e65e 100644 --- a/LevelImposter/Core/Components/LIShipStatus.cs +++ b/LevelImposter/Core/Components/LIShipStatus.cs @@ -238,20 +238,46 @@ private IEnumerator CoLoadingScreen() GameObject loadingBean = DestroyableSingleton.Instance.GameLoadAnimation; Color sabColor = fullScreen.color; - // Loading + // Create LoadingBar + if (LoadingBar.Instance == null) + Instantiate(MapUtils.LoadAssetBundle("loadingbar"), DestroyableSingleton.Instance.transform); + if (LoadingBar.Instance == null) + LILogger.Warn("Missing LoadingBar asset bundle!"); + + // Show Loading Screen LILogger.Info($"Showing loading screen (Freeplay={isFreeplay})"); if (isFreeplay) { fullScreen.color = new Color(0, 0, 0, 0.9f); fullScreen.gameObject.SetActive(true); } + LoadingBar.Instance?.SetMapName(CurrentMap?.name ?? "Loading..."); + LoadingBar.Instance?.SetVisible(true); while (!IsReady) { loadingBean.SetActive(true); + + // Approximate Progress + if (SpriteLoader.Instance?.RenderCount > 0) + { + double renderTotal = SpriteLoader.Instance?.RenderTotal ?? 1; + if (renderTotal == 0) + renderTotal = 1; + double renderCount = renderTotal - (SpriteLoader.Instance?.RenderCount ?? 0); + float progress = (float)(renderCount / renderTotal); + LoadingBar.Instance?.SetProgress(progress); + LoadingBar.Instance?.SetStatus($"{Math.Round(progress * 100)}% ({renderCount}/{renderTotal})"); + } + else + { + LoadingBar.Instance?.SetProgress(1); + LoadingBar.Instance?.SetStatus("waiting for host"); + } + yield return null; } - // Sabotage + // Revert Loading Screen LILogger.Info($"Hiding loading screen (Freeplay={isFreeplay})"); if (isFreeplay) { @@ -259,6 +285,7 @@ private IEnumerator CoLoadingScreen() fullScreen.gameObject.SetActive(false); } loadingBean.SetActive(false); + LoadingBar.Instance?.SetVisible(false); } /// diff --git a/LevelImposter/Core/Components/LITriggerArea.cs b/LevelImposter/Core/Components/LITriggerArea.cs index cd555a80..10b1d44c 100644 --- a/LevelImposter/Core/Components/LITriggerArea.cs +++ b/LevelImposter/Core/Components/LITriggerArea.cs @@ -1,13 +1,11 @@ using System; -using System.Collections.Generic; -using UnityEngine; namespace LevelImposter.Core { /// /// Object that fires a trigger when the player enters/exits it's range /// - public class LITriggerArea : MonoBehaviour + public class LITriggerArea : PlayerArea { public LITriggerArea(IntPtr intPtr) : base(intPtr) { @@ -16,7 +14,6 @@ public LITriggerArea(IntPtr intPtr) : base(intPtr) private const string ENTER_TRIGGGER_ID = "onEnter"; private const string EXIT_TRIGGER_ID = "onExit"; - private List? _currentPlayersIDs = new(); private bool _isClientSide = false; /// @@ -28,36 +25,20 @@ public void SetClientSide(bool isClientSide) _isClientSide = isClientSide; } - public void OnTriggerEnter2D(Collider2D collider) + protected override void OnPlayerEnter(PlayerControl player) { - PlayerControl? player = collider.GetComponent(); - if (player == null) - return; - - bool triggerServerSided = _currentPlayersIDs?.Count <= 0 && !_isClientSide; - bool triggerClientSided = MapUtils.IsLocalPlayer(collider.gameObject) && _isClientSide; + bool triggerServerSided = CurrentPlayersIDs?.Count <= 1 && !_isClientSide; + bool triggerClientSided = player.AmOwner && _isClientSide; if (triggerClientSided || triggerServerSided) LITriggerable.Trigger(transform.gameObject, ENTER_TRIGGGER_ID, null); - - if (_currentPlayersIDs?.Contains(player.PlayerId) ?? false) - _currentPlayersIDs?.Add(player.PlayerId); } - public void OnTriggerExit2D(Collider2D collider) - { - PlayerControl? player = collider.GetComponent(); - if (player == null) - return; - - _currentPlayersIDs?.RemoveAll(id => id == player.PlayerId); - bool triggerServer = _currentPlayersIDs?.Count <= 0 && !_isClientSide; - bool triggerClient = MapUtils.IsLocalPlayer(collider.gameObject) && _isClientSide; - if (triggerClient || triggerServer) - LITriggerable.Trigger(transform.gameObject, EXIT_TRIGGER_ID, null); - } - public void OnDestroy() + protected override void OnPlayerExit(PlayerControl player) { - _currentPlayersIDs = null; + bool triggerServerSided = CurrentPlayersIDs?.Count <= 0 && !_isClientSide; + bool triggerClientSided = player.AmOwner && _isClientSide; + if (triggerClientSided || triggerServerSided) + LITriggerable.Trigger(transform.gameObject, EXIT_TRIGGER_ID, null); } } } diff --git a/LevelImposter/Core/Components/LITriggerable.cs b/LevelImposter/Core/Components/LITriggerable.cs index fd253fdd..c461445a 100644 --- a/LevelImposter/Core/Components/LITriggerable.cs +++ b/LevelImposter/Core/Components/LITriggerable.cs @@ -234,6 +234,11 @@ private void OnTrigger(PlayerControl? orgin, int stackSize = 0) trigger.StopTimer(); break; + // Death + case "killArea": + SetComponentsEnabled(true); + break; + // Door case "open": SetDoorOpen(true); @@ -358,6 +363,16 @@ private void SetComponentsEnabled(bool? isEnabled = null, bool? isLooped = null) LITeleporter? teleporter = GetComponent(); if (teleporter != null) teleporter.enabled = isEnabled ?? !teleporter.enabled; + + // Kill Area + LIDeathArea? deathArea = GetComponent(); + if (deathArea != null) + deathArea.KillAllPlayers(); + + // Trigger Shake + LIShakeArea? triggerShake = GetComponent(); + if (triggerShake != null) + triggerShake.SetEnabled(isEnabled ?? !triggerShake.enabled); } /// diff --git a/LevelImposter/Core/Components/PlayerArea.cs b/LevelImposter/Core/Components/PlayerArea.cs new file mode 100644 index 00000000..d6230334 --- /dev/null +++ b/LevelImposter/Core/Components/PlayerArea.cs @@ -0,0 +1,85 @@ +using Il2CppInterop.Runtime.Attributes; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace LevelImposter.Core +{ + /// + /// Object that tracks the players that are inside it's range + /// + public class PlayerArea : MonoBehaviour + { + public PlayerArea(IntPtr intPtr) : base(intPtr) + { + } + + private List? _currentPlayersIDs = new(); + private bool _isLocalPlayerInside = false; + + [HideFromIl2Cpp] + protected List? CurrentPlayersIDs => _currentPlayersIDs; + protected bool IsLocalPlayerInside => _isLocalPlayerInside; + + /// + /// Gets a player by it's ID + /// + /// Player ID to search + /// The cooresponding PlayerControl or null if it can't be found + protected PlayerControl? GetPlayer(byte playerID) + { + foreach (PlayerControl player in PlayerControl.AllPlayerControls) + if (player.PlayerId == playerID) + return player; + return null; + } + + /// + /// Called when a player enters the collider + /// + /// Player that entered the collider + virtual protected void OnPlayerEnter(PlayerControl player) + { + } + + /// + /// Called when a player exits the collider + /// + /// Player that exited the collider + virtual protected void OnPlayerExit(PlayerControl player) + { + } + + public void OnTriggerEnter2D(Collider2D collider) + { + PlayerControl? player = collider.GetComponent(); + if (player == null) + return; + + _currentPlayersIDs?.Add(player.PlayerId); + if (player.AmOwner) + _isLocalPlayerInside = true; + + if (enabled) + OnPlayerEnter(player); + } + + public void OnTriggerExit2D(Collider2D collider) + { + PlayerControl? player = collider.GetComponent(); + if (player == null) + return; + + _currentPlayersIDs?.RemoveAll(id => id == player.PlayerId); + if (player.AmOwner) + _isLocalPlayerInside = false; + + if (enabled) + OnPlayerExit(player); + } + public void OnDestroy() + { + _currentPlayersIDs = null; + } + } +} diff --git a/LevelImposter/Core/Components/SpriteLoader.cs b/LevelImposter/Core/Components/SpriteLoader.cs index dd2ac287..9843fe82 100644 --- a/LevelImposter/Core/Components/SpriteLoader.cs +++ b/LevelImposter/Core/Components/SpriteLoader.cs @@ -28,8 +28,10 @@ public SpriteLoader(IntPtr intPtr) : base(intPtr) private Stack? _spriteCache = new(); private int _renderCount = 0; + private int _renderTotal = 0; public int RenderCount => _renderCount; + public int RenderTotal => _renderTotal; /// /// Searches for a Sprite in cache by GUID @@ -59,6 +61,7 @@ public void Clean() { OnLoad = null; _spriteCache?.Clear(); + _renderTotal = 0; } /// @@ -183,9 +186,10 @@ private IEnumerator CoLoadSpriteAsync(Stream? imgStream, Action? on { { _renderCount++; + _renderTotal = Math.Max(_renderTotal, _renderCount); yield return null; yield return null; - while (!LagLimiter.ShouldContinue(1)) + while (!LagLimiter.ShouldContinue(20)) yield return null; // Search Cache diff --git a/LevelImposter/Core/Models/LIConstants.cs b/LevelImposter/Core/Models/LIConstants.cs index 54a43159..10383ae0 100644 --- a/LevelImposter/Core/Models/LIConstants.cs +++ b/LevelImposter/Core/Models/LIConstants.cs @@ -8,8 +8,9 @@ public static class LIConstants public const StringNames MAP_STRING_NAME = (StringNames)392001; // StringName that placeholdes the map names public const string MAP_NAME = "Random Custom Map"; // Name to populate Constants.MapNames public const float PLAYER_POS = -5.0f; // Z value of the player - public const int CONNECTION_TIMEOUT = 20; // Maximum time for host to wait (AmongUsClient.MAX_CLIENT_WAIT_TIME) - public const int MAX_LOAD_TIME = 12; // Maximum time to build map before aborting + public const int MAX_HOST_TIMEOUT = 40; // Maximum time for host to wait (AmongUsClient.MAX_CLIENT_WAIT_TIME) + public const int MAX_LOAD_TIME = 35; // Maximum time to build map before aborting + public const int MAX_CONNECTION_TIMEOUT = 15; // Maximum time to wait for a client connection public const int ELEM_WARN_TIME = 200; // Time to warn the user (in ms) when an element is taking too long to load public const bool FREEPLAY_FLUSH_CACHE = true; // Whether to flush the cache when entering freeplay maps public const bool OVERRIDE_HORSE_MODE = false; // Whether to override the horse mode setting diff --git a/LevelImposter/Core/Models/LIProperties.cs b/LevelImposter/Core/Models/LIProperties.cs index 453744a7..8131bd22 100644 --- a/LevelImposter/Core/Models/LIProperties.cs +++ b/LevelImposter/Core/Models/LIProperties.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; namespace LevelImposter.Core { @@ -17,6 +18,7 @@ public class LIProperties public LIColor? highlightColor { get; set; } public int? triggerCount { get; set; } public bool? triggerLoop { get; set; } + public bool? createDeadBody { get; set; } // Sprite public Guid? spriteID { get; set; } @@ -25,6 +27,10 @@ public class LIProperties public LIColor? color { get; set; } public bool? loopGIF { get; set; } + // Shake + public float? shakeAmount { get; set; } + public float? shakePeriod { get; set; } + // Legacy [Obsolete("Use spriteID instead")] public string? spriteData { get; set; } @@ -47,6 +53,9 @@ public class LIProperties public float? scrollingXSpeed { get; set; } public float? scrollingYSpeed { get; set; } + // Custom Text + public Dictionary? customText { get; set; } + // Minigame public LIMinigameSprite[]? minigames { get; set; } public LIMinigameProps? minigameProps { get; set; } diff --git a/LevelImposter/Core/Models/LIRpc.cs b/LevelImposter/Core/Models/LIRpc.cs index 6dde0552..ffd755bb 100644 --- a/LevelImposter/Core/Models/LIRpc.cs +++ b/LevelImposter/Core/Models/LIRpc.cs @@ -12,10 +12,11 @@ public enum LIRpc { FireTrigger = 94, // Fires a global trigger on an object - TeleportPlayer, // Uses a util-tele object + TeleportPlayer, // Used by util-tele object SyncMapID, // Syncs the map ID in the lobby SyncRandomSeed, // Syncs a random seed for util-triggerrand ResetPlayer, // Resets the player on Ctrl-R E S - DownloadCheck // Warns the host that the client is still downloading the map + DownloadCheck, // Warns the host that the client is still downloading the map + KillPlayer, // Used by util-triggerdeath object } } diff --git a/LevelImposter/Core/Patches/Fixes/DeconControlPatch.cs b/LevelImposter/Core/Patches/Fixes/DeconControlPatch.cs new file mode 100644 index 00000000..fab1334e --- /dev/null +++ b/LevelImposter/Core/Patches/Fixes/DeconControlPatch.cs @@ -0,0 +1,45 @@ +using HarmonyLib; +using UnityEngine; + +namespace LevelImposter.Core +{ + /// + /// Allows decon controls to bypass physics collisions. + /// + [HarmonyPatch(typeof(DeconControl), nameof(DeconControl.CanUse))] + public class DeconControlPatch + { + public static void Postfix( + DeconControl __instance, + [HarmonyArgument(0)] GameData.PlayerInfo playerInfo, + [HarmonyArgument(1)] ref bool canUse, + [HarmonyArgument(2)] ref bool couldUse, + ref float __result) + { + // Custom Maps Only + if (LIShipStatus.Instance == null) + return; + // Ignore if the system is not idle + if (__instance.System.CurState != DeconSystem.States.Idle) + return; + + // Check if the player can use the decon + couldUse = playerInfo.Object.CanMove && !playerInfo.IsDead; + canUse = (couldUse && __instance.cooldown == 0f); + __result = float.MaxValue; + + // Check if the player is close enough to use the decon + if (canUse) + { + // Get Adjusted Position + Vector2 truePosition = playerInfo.Object.GetTruePosition(); + Vector3 position = __instance.transform.position; + position.y -= 0.1f; // <-- Adjust for player height + + // Compare Distance + __result = Vector2.Distance(truePosition, position); + canUse &= (__result <= __instance.UsableDistance); + } + } + } +} diff --git a/LevelImposter/Core/Patches/Fixes/HorsePatch.cs b/LevelImposter/Core/Patches/Fixes/HorsePatch.cs index cb92a7c0..11c12550 100644 --- a/LevelImposter/Core/Patches/Fixes/HorsePatch.cs +++ b/LevelImposter/Core/Patches/Fixes/HorsePatch.cs @@ -7,6 +7,7 @@ namespace LevelImposter.Core /// /// Disables the april-fools horse mode due to incompatibility with lots of mods. /// + /* [HarmonyPatch(typeof(Constants), nameof(Constants.ShouldHorseAround))] public static class HorsePatch { @@ -19,6 +20,7 @@ public static bool Prefix(ref bool __result) return false; } } + */ #pragma warning restore CS0162 } \ No newline at end of file diff --git a/LevelImposter/Core/Patches/Loading/LoadingShipPatch.cs b/LevelImposter/Core/Patches/Loading/LoadingShipPatch.cs index c562a811..a1790028 100644 --- a/LevelImposter/Core/Patches/Loading/LoadingShipPatch.cs +++ b/LevelImposter/Core/Patches/Loading/LoadingShipPatch.cs @@ -32,7 +32,7 @@ public static class LoadingShipTimerPatch { public static void Postfix(AmongUsClient __instance) { - __instance.MAX_CLIENT_WAIT_TIME = LIConstants.CONNECTION_TIMEOUT; + __instance.MAX_CLIENT_WAIT_TIME = LIConstants.MAX_HOST_TIMEOUT; } } } diff --git a/LevelImposter/Core/Patches/Ship/ShipStatusPatch.cs b/LevelImposter/Core/Patches/Ship/ShipStatusPatch.cs index cc0568cf..2c4ebe01 100644 --- a/LevelImposter/Core/Patches/Ship/ShipStatusPatch.cs +++ b/LevelImposter/Core/Patches/Ship/ShipStatusPatch.cs @@ -11,6 +11,8 @@ public static class ShipStatusPatch { public static void Prefix(ShipStatus __instance) { + //UnityToMapGenerator.GenerateMap(__instance); + if (MapUtils.GetCurrentMapType() == MapType.LevelImposter) __instance.gameObject.AddComponent(); else if (!MapLoader.IsFallback) diff --git a/LevelImposter/Core/Patches/Ship/SporePatch.cs b/LevelImposter/Core/Patches/Ship/SporePatch.cs index 70d25169..28fd782e 100644 --- a/LevelImposter/Core/Patches/Ship/SporePatch.cs +++ b/LevelImposter/Core/Patches/Ship/SporePatch.cs @@ -20,7 +20,7 @@ public static bool Prefix( { if (LIShipStatus.Instance == null) return true; - if (callId != (byte)RpcCalls.TriggerSpores || callId != (byte)RpcCalls.CheckSpore) + if (callId != (byte)RpcCalls.TriggerSpores && callId != (byte)RpcCalls.CheckSpore) return true; // Find spores diff --git a/LevelImposter/Core/Patches/Triggers/ConsolePatch.cs b/LevelImposter/Core/Patches/Triggers/ConsolePatch.cs index 0a59e643..517f6540 100644 --- a/LevelImposter/Core/Patches/Triggers/ConsolePatch.cs +++ b/LevelImposter/Core/Patches/Triggers/ConsolePatch.cs @@ -24,6 +24,16 @@ public static bool Prefix(MonoBehaviour __instance) if (LIShipStatus.Instance == null) return true; + // Get IUsable + var usable = __instance.TryCast(); + if (usable == null) + return true; + + // Check if the player can use the console + usable.CanUse(PlayerControl.LocalPlayer.Data, out bool canUse, out _); + if (!canUse) + return true; + // Update Last Console MinigamePatch.LastConsole = __instance.gameObject; diff --git a/LevelImposter/Core/Patches/Utils/ConnectionPatch.cs b/LevelImposter/Core/Patches/Utils/ConnectionPatch.cs new file mode 100644 index 00000000..67f430c6 --- /dev/null +++ b/LevelImposter/Core/Patches/Utils/ConnectionPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using Hazel.Udp; + +namespace LevelImposter.Core +{ + /// + /// Increases the max timeout for the low level connection to the server. + /// + [HarmonyPatch(typeof(UnityUdpClientConnection), nameof(UnityUdpClientConnection.ConnectAsync))] + public static class TimeoutPatch + { + public static void Postfix(UnityUdpClientConnection __instance) + { + __instance.DisconnectTimeoutMs = LIConstants.MAX_CONNECTION_TIMEOUT * 1000; + } + } +} diff --git a/LevelImposter/Core/Utils/GCHandler.cs b/LevelImposter/Core/Utils/GCHandler.cs index 5b0d7708..18bb3b72 100644 --- a/LevelImposter/Core/Utils/GCHandler.cs +++ b/LevelImposter/Core/Utils/GCHandler.cs @@ -55,8 +55,8 @@ public static bool IsLowMemory() { Process process = Process.GetCurrentProcess(); bool isLow = process.PrivateMemorySize64 > MAX_MEMORY; - if (isLow) - LILogger.Msg("Warning: Low on memory"); + //if (isLow) + // LILogger.Msg("Warning: Low on memory"); return isLow; } diff --git a/LevelImposter/LevelImposter.cs b/LevelImposter/LevelImposter.cs index 584326ca..686321a0 100644 --- a/LevelImposter/LevelImposter.cs +++ b/LevelImposter/LevelImposter.cs @@ -21,6 +21,7 @@ public partial class LevelImposter : BasePlugin public const string ID = "com.DigiWorm.LevelImposter"; public HarmonyLib.Harmony Harmony { get; } = new HarmonyLib.Harmony(ID); + public static string DisplayVersion { get; } = Version.Contains('+') ? Version.Substring(0, Version.IndexOf('+')) : Version; public override void Load() { @@ -48,6 +49,8 @@ public override void Load() ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); @@ -64,6 +67,7 @@ public override void Load() ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); + ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(usableInterface); // Patch Methods diff --git a/LevelImposter/LevelImposter.csproj b/LevelImposter/LevelImposter.csproj index 45c7661c..9636905b 100644 --- a/LevelImposter/LevelImposter.csproj +++ b/LevelImposter/LevelImposter.csproj @@ -1,7 +1,7 @@ LevelImposter - 0.18.0 + 0.19.0 Custom Among Us Mapping Studio DigiWorm @@ -13,15 +13,15 @@ C:\Program Files (x86)\Steam\steamapps\common\Among Us - 2023.11.28 + 2024.6.4 - + - + diff --git a/LevelImposter/Shop/Components/LoadingBar.cs b/LevelImposter/Shop/Components/LoadingBar.cs new file mode 100644 index 00000000..ae103acc --- /dev/null +++ b/LevelImposter/Shop/Components/LoadingBar.cs @@ -0,0 +1,82 @@ +using System; +using UnityEngine; + +namespace LevelImposter.Shop +{ + public class LoadingBar : MonoBehaviour + { + public LoadingBar(IntPtr intPtr) : base(intPtr) + { + } + + public static LoadingBar? Instance { get; private set; } + + private GameObject? _loadingBar = null; + private TMPro.TMP_Text? _mapText = null; + private TMPro.TMP_Text? _statusText = null; + + /// + /// Sets the name of the map being loaded + /// + /// Name of the map + public void SetMapName(string mapName) + { + _mapText?.SetText(mapName); + } + + /// + /// Sets the status text of the loading bar + /// + /// Text to display + public void SetStatus(string status) + { + _statusText?.SetText(status); + } + + /// + /// Sets the progress of the loading bar + /// + /// Percentage of completion, from 0 to 1 + public void SetProgress(float percent) + { + if (_loadingBar == null) + return; + + _loadingBar.transform.localPosition = new Vector3(percent - 1, 0, 0); + } + + /// + /// Sets the visibility of the loading bar + /// + /// True iff the loading bar should be visible + public void SetVisible(bool visible) + { + gameObject.SetActive(visible); + } + + public void Awake() + { + // Singleton + if (Instance != null) + { + Destroy(gameObject); + return; + } + Instance = this; + + _loadingBar = transform.Find("BarMask").Find("Bar").gameObject; + _mapText = transform.Find("MapText").GetComponent(); + _statusText = transform.Find("StatusText").GetComponent(); + + //DontDestroyOnLoad(gameObject); + } + public void OnDestroy() + { + Instance = null; + + _loadingBar = null; + _mapText = null; + _statusText = null; + } + } +} \ No newline at end of file diff --git a/LevelImposter/Shop/Components/ShopManager.cs b/LevelImposter/Shop/Components/ShopManager.cs index a3beee56..3108604a 100644 --- a/LevelImposter/Shop/Components/ShopManager.cs +++ b/LevelImposter/Shop/Components/ShopManager.cs @@ -24,6 +24,7 @@ public ShopManager(IntPtr intPtr) : base(intPtr) private Tab _currentTab = Tab.None; private GameObject? _overlay = null; + private SpriteRenderer? _overlayBackground = null; private TMPro.TMP_Text? _overlayText = null; private Scroller? _scroller = null; private SpriteRenderer? _title = null; @@ -93,6 +94,10 @@ public void SetTab(Tab tab) // Clear the shop Clear(); + // Show Loading Spinner + SetOverlayText("Retrieving Maps..."); + SetOverlayEnabled(true, false); + // Switch on the tab Action callback = tab switch { @@ -128,6 +133,9 @@ private IEnumerator CoSetDownloadsTab() AddBanner(metadata); yield return null; } + + // Hide Loading Spinner + SetOverlayEnabled(false); } } @@ -168,6 +176,9 @@ private void OnAPIRespose(LIMetadata[] maps, Tab tab) Clear(); foreach (LIMetadata map in maps) AddBanner(map); + + // Hide Loading Spinner + SetOverlayEnabled(false); } /// @@ -256,9 +267,12 @@ public static void RegenerateFallbackMap() /// Toggles the overlay /// /// true if the overlay should be visible - public void SetOverlayEnabled(bool isEnabled) + /// true if the overlay background should be visible + public void SetOverlayEnabled(bool isEnabled, bool isBackgroundEnabled = true) { _overlay?.SetActive(isEnabled); + if (_overlayBackground != null) + _overlayBackground.enabled = isBackgroundEnabled; } /// @@ -310,6 +324,7 @@ public void Awake() Instance = this; _overlay = transform.Find("Overlay").gameObject; + _overlayBackground = _overlay?.GetComponent(); _overlayText = _overlay?.transform.Find("Text").GetComponent(); _scroller = transform.Find("Scroll/Scroller").GetComponent(); _title = _scroller?.transform.Find("Inner/Title").GetComponent(); diff --git a/LevelImposter/Shop/Models/LIUpdate.cs b/LevelImposter/Shop/Models/LIUpdate.cs index fabcbd90..3c7fbc3e 100644 --- a/LevelImposter/Shop/Models/LIUpdate.cs +++ b/LevelImposter/Shop/Models/LIUpdate.cs @@ -13,7 +13,7 @@ public bool isCurrent { get { - return tag.Equals(LevelImposter.Version); + return tag.Equals(LevelImposter.DisplayVersion); } } } diff --git a/LevelImposter/Shop/Patches/PingPatch.cs b/LevelImposter/Shop/Patches/PingPatch.cs index 91297c80..1dd7bbe2 100644 --- a/LevelImposter/Shop/Patches/PingPatch.cs +++ b/LevelImposter/Shop/Patches/PingPatch.cs @@ -32,28 +32,26 @@ public static void Postfix(PingTracker __instance) StringBuilder pingBuilder = new(); - // Shrink all to fit - pingBuilder.Append(""); - - // LevelImposter "Logo" - if (isInLobby) - pingBuilder.Append($"LevelImposter v{LevelImposter.Version}\n"); - // Existing Ping/Mods pingBuilder.Append(__instance.text.text); if (!__instance.text.text.EndsWith("\n")) pingBuilder.Append("\n"); + // LevelImposter "Logo" + if (isInLobby) + pingBuilder.Append($"LevelImposter v{LevelImposter.DisplayVersion}\n"); + // Map Name - pingBuilder.Append($"{mapName}\n"); + pingBuilder.Append($"{mapName}"); // Map Author if (isFallback && isInLobby) - pingBuilder.Append($"by ???"); + pingBuilder.Append(""); else if (isPublished) - pingBuilder.Append($"by {currentMap.authorName}"); + pingBuilder.Append($"\nby {currentMap.authorName}"); else - pingBuilder.Append($"(Freeplay Only)"); + pingBuilder.Append($"\n(Freeplay Only)"); + __instance.text.text = pingBuilder.ToString(); } diff --git a/LevelImposter/Shop/Patches/VersionPatch.cs b/LevelImposter/Shop/Patches/VersionPatch.cs index be16c490..6b2bbaf8 100644 --- a/LevelImposter/Shop/Patches/VersionPatch.cs +++ b/LevelImposter/Shop/Patches/VersionPatch.cs @@ -49,7 +49,7 @@ public static void Postfix() logoText.fontSize = 1.5f; logoText.alignment = TextAlignmentOptions.BottomLeft; logoText.raycastTarget = false; - logoText.SetText("v" + LevelImposter.Version); + logoText.SetText("v" + LevelImposter.DisplayVersion); } private static Sprite GetLogoSprite() diff --git a/LevelImposter/Shop/Util/GitHubAPI.cs b/LevelImposter/Shop/Util/GitHubAPI.cs index 6668b4ca..83a26123 100644 --- a/LevelImposter/Shop/Util/GitHubAPI.cs +++ b/LevelImposter/Shop/Util/GitHubAPI.cs @@ -86,7 +86,7 @@ private static bool IsUpdateForbidden(GHRelease[] releases) public static bool IsCurrent(GHRelease release) { string versionString = release.name.Split(" ")[1]; - return versionString == LevelImposter.Version || LevelImposter.Version.Contains(DEV_VERSION_FLAG); + return versionString == LevelImposter.DisplayVersion || LevelImposter.DisplayVersion.Contains(DEV_VERSION_FLAG); } /// diff --git a/LevelImposter/Shop/Util/LevelImposterAPI.cs b/LevelImposter/Shop/Util/LevelImposterAPI.cs index ccd01e8b..d3c50f59 100644 --- a/LevelImposter/Shop/Util/LevelImposterAPI.cs +++ b/LevelImposter/Shop/Util/LevelImposterAPI.cs @@ -33,7 +33,7 @@ public static void Request(string url, Action callback, Action onE if (response == null) onError("Invalid API Response"); else if (response.v != API_VERSION) - onError($"You are running on an older version of LevelImposter {LevelImposter.Version}. Update to get access to the API."); + onError($"You are running on an older version of LevelImposter {LevelImposter.DisplayVersion}. Update to get access to the API."); else if (!string.IsNullOrEmpty(response.error)) onError(response.error); else