├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml ├── .deepsource.toml ├── LevelImposter ├── Assets │ ├── Assets │ ├── shop │ ├── MapIcon.png │ ├── loadingbar │ ├── LobbyConsole.png │ ├── UpdateButton.png │ ├── defaultthumbnail │ ├── LevelImposterLogo.png │ ├── UpdateButtonHover.png │ ├── Assets.manifest │ └── defaultthumbnail.manifest ├── DB │ ├── Models │ │ ├── TaskLength.cs │ │ └── SerializedAssetDB.cs │ ├── Patches │ │ └── DBPatch.cs │ └── Sub │ │ ├── PathDB.cs │ │ ├── ObjectDB.cs │ │ ├── SubDB.cs │ │ ├── TaskDB.cs │ │ └── SoundDB.cs ├── AssetLoader │ ├── Queue │ │ ├── ICachable.cs │ │ └── ItemCache.cs │ ├── Loadables │ │ ├── LoadableAudio.cs │ │ ├── LoadedTexture.cs │ │ ├── LoadedSprite.cs │ │ ├── LoadableTexture.cs │ │ └── LoadableSprite.cs │ ├── Loaders │ │ ├── GIFLoader.cs │ │ └── WAVLoader.cs │ ├── AudioLoader.cs │ └── SpriteLoader.cs ├── Properties │ └── launchSettings.json ├── Trigger │ ├── ITriggerHandle.cs │ ├── TriggerHandles │ │ ├── MeetingTriggerHandle.cs │ │ ├── TeleportTriggerHandle.cs │ │ ├── RepeatTriggerHandle.cs │ │ ├── DeathTriggerHandle.cs │ │ ├── AnimTriggerHandle.cs │ │ ├── GateTriggerHandle.cs │ │ ├── DoorTriggerHandle.cs │ │ ├── SoundTriggerHandles.cs │ │ ├── ValueTriggerHandle.cs │ │ ├── RandomTriggerHandle.cs │ │ ├── TriggerPropogationHandle.cs │ │ └── TimerTriggerHandle.cs │ └── TriggerSignal.cs ├── Core │ ├── Models │ │ ├── LIAnimProperty.cs │ │ ├── MapType.cs │ │ ├── LISpriteAnimationFrame.cs │ │ ├── LITrigger.cs │ │ ├── LIAnimTarget.cs │ │ ├── LISpriteAnimation.cs │ │ ├── LICollider.cs │ │ ├── Point.cs │ │ ├── LIAnimKeyframe.cs │ │ ├── LISpriteAtlas.cs │ │ ├── LIMinigameSprite.cs │ │ ├── LIMapProperties.cs │ │ ├── LIColor.cs │ │ ├── LISound.cs │ │ ├── LIMinigameProps.cs │ │ ├── LIMap.cs │ │ ├── LIElement.cs │ │ ├── LIMetadata.cs │ │ ├── LIRpc.cs │ │ ├── MapAssetDB.cs │ │ ├── LIConstants.cs │ │ └── Layer.cs │ ├── Utils │ │ ├── Values │ │ │ ├── IBoolValue.cs │ │ │ ├── DelegateBoolValue.cs │ │ │ ├── BasicBoolValue.cs │ │ │ └── ComparatorValue.cs │ │ ├── MissingShipException.cs │ │ ├── DataBlock │ │ │ ├── IMemoryBlock.cs │ │ │ ├── ExistingMemoryBlock.cs │ │ │ └── PoolableMemoryBlock.cs │ │ ├── Stores │ │ │ ├── ZIPEntryStore.cs │ │ │ ├── FileStore.cs │ │ │ ├── MemoryStore.cs │ │ │ ├── FileChunkStore.cs │ │ │ └── IDataStore.cs │ │ ├── MapObjectDB.cs │ │ ├── StreamExtensions.cs │ │ ├── GameState.cs │ │ ├── ModCompatibility.cs │ │ ├── GameObjectCoroutineManager.cs │ │ ├── GCHandler.cs │ │ └── RandomizerSync.cs │ ├── Patches │ │ ├── Utils │ │ │ ├── ExitGamePatch.cs │ │ │ ├── ConnectionPatch.cs │ │ │ ├── DevelopmentPatch.cs │ │ │ ├── RoomUIPatch.cs │ │ │ ├── QuickChatPatch.cs │ │ │ ├── MinigamePatch.cs │ │ │ └── LateInitPatch.cs │ │ ├── Fixes │ │ │ ├── SFXLoadingPatch.cs │ │ │ ├── MeetingOverlayPatch.cs │ │ │ ├── ExileTextPatch.cs │ │ │ ├── NodeMinigamePatch.cs │ │ │ ├── AdminTablePatch.cs │ │ │ ├── HorsePatch.cs │ │ │ ├── PlatformMagnitudePatch.cs │ │ │ ├── SoundPatch.cs │ │ │ ├── DoorIDPatch.cs │ │ │ ├── MinimumTaskPatch.cs │ │ │ ├── FreeplayOptionsPatch.cs │ │ │ ├── PolusCamPatch.cs │ │ │ ├── LadderPatch.cs │ │ │ ├── DummyVotePatch.cs │ │ │ └── DeconControlPatch.cs │ │ ├── Loading │ │ │ ├── LoadingCameraPatch.cs │ │ │ └── LoadingShipPatch.cs │ │ ├── Ship │ │ │ ├── ShipStatusPatch.cs │ │ │ ├── MixupPatch.cs │ │ │ ├── LadderPatch.cs │ │ │ ├── SporePatch.cs │ │ │ └── BinocularsPatch.cs │ │ ├── Animations │ │ │ ├── CamFollowPatch.cs │ │ │ ├── CamPatch.cs │ │ │ └── DoorPatch.cs │ │ ├── Triggers │ │ │ ├── MeetingPatch.cs │ │ │ ├── SabEndPatch.cs │ │ │ └── ConsolePatch.cs │ │ └── ModCompatibility │ │ │ └── VentPatch.cs │ └── Components │ │ ├── MapObjectData.cs │ │ ├── EditableLadderConsole.cs │ │ ├── LIShakeArea.cs │ │ ├── LIFloat.cs │ │ ├── LITriggerArea.cs │ │ ├── LagLimiter.cs │ │ ├── LIStar.cs │ │ ├── LITriggerSpawnable.cs │ │ ├── PlayerArea.cs │ │ └── LIScroll.cs ├── Shop │ ├── Models │ │ ├── LIConfig.cs │ │ ├── LICallback.cs │ │ ├── GHAsset.cs │ │ ├── LIUpdate.cs │ │ └── GHRelease.cs │ ├── Patches │ │ ├── LobbyMenuPatch.cs │ │ ├── ButtonPatch.cs │ │ ├── MapChangePatch.cs │ │ ├── ShopPatch.cs │ │ ├── UpdateButtonPatch.cs │ │ ├── PingPatch.cs │ │ ├── MapButtonsPatch.cs │ │ ├── LobbySyncPatch.cs │ │ ├── DownloadManagerPatch.cs │ │ └── VersionPatch.cs │ ├── Components │ │ └── Spinner.cs │ ├── Builders │ │ ├── ShopBuilder.cs │ │ ├── MainMenuBuilder.cs │ │ └── LobbyConsoleBuilder.cs │ ├── Util │ │ └── ThumbnailCache.cs │ └── IO │ │ └── LISerializer.cs ├── Builders │ ├── Util │ │ ├── ScrollBuilder.cs │ │ ├── FloatBuilder.cs │ │ ├── DummyBuilder.cs │ │ ├── LayerBuilder.cs │ │ ├── PlayerMoverBuilder.cs │ │ ├── BinocularsBuilder.cs │ │ ├── CamBuilder.cs │ │ ├── SabotageOptionsBuilder.cs │ │ ├── OneWayColliderBuilder.cs │ │ ├── TeleBuilder.cs │ │ ├── EjectHandBuilder.cs │ │ ├── EjectBuilder.cs │ │ ├── StepSoundBuilder.cs │ │ ├── FilterBuilder.cs │ │ ├── PhysicsObjectBuilder.cs │ │ ├── AmbientSoundBuilder.cs │ │ └── StarfieldBuilder.cs │ ├── Trigger │ │ ├── TriggerAnimBuilder.cs │ │ ├── TriggerStartBuilder.cs │ │ ├── TriggerDeathBuilder.cs │ │ ├── TriggerShakeBuilder.cs │ │ ├── TriggerAreaBuilder.cs │ │ └── TriggerConsoleBuilder.cs │ ├── Generic │ │ ├── ColorBuilder.cs │ │ ├── MinigameSpriteBuilder.cs │ │ ├── TransformBuilder.cs │ │ ├── MapPropertiesBuilder.cs │ │ └── ColliderBuilder.cs │ ├── Minimap │ │ ├── MinimapSpriteBuilder.cs │ │ ├── RoomNameBuilder.cs │ │ └── AdminMapBuilder.cs │ ├── Other │ │ └── DecBuilder.cs │ ├── IElemBuilder.cs │ └── Task │ │ └── TaskBuilder.cs └── LevelImposter.csproj ├── .vscode ├── settings.json └── tasks.json ├── .editorconfig ├── .config └── dotnet-tools.json ├── nuget.config └── LevelImposter.sln /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: digiworm -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "csharp" -------------------------------------------------------------------------------- /LevelImposter/Assets/Assets: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/Assets -------------------------------------------------------------------------------- /LevelImposter/Assets/shop: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/shop -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeLens": false, 3 | "dotnet.defaultSolution": "LevelImposter.sln" 4 | } -------------------------------------------------------------------------------- /LevelImposter/Assets/MapIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/MapIcon.png -------------------------------------------------------------------------------- /LevelImposter/Assets/loadingbar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/loadingbar -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0059: Unnecessary assignment of a value 4 | dotnet_diagnostic.IDE0059.severity = none 5 | -------------------------------------------------------------------------------- /LevelImposter/Assets/LobbyConsole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/LobbyConsole.png -------------------------------------------------------------------------------- /LevelImposter/Assets/UpdateButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/UpdateButton.png -------------------------------------------------------------------------------- /LevelImposter/Assets/defaultthumbnail: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/defaultthumbnail -------------------------------------------------------------------------------- /LevelImposter/Assets/LevelImposterLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/LevelImposterLogo.png -------------------------------------------------------------------------------- /LevelImposter/Assets/UpdateButtonHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigiWorm0/LevelImposter/HEAD/LevelImposter/Assets/UpdateButtonHover.png -------------------------------------------------------------------------------- /LevelImposter/DB/Models/TaskLength.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.DB; 2 | 3 | public enum TaskLength 4 | { 5 | Common, 6 | Short, 7 | Long 8 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Queue/ICachable.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.AssetLoader; 2 | 3 | public interface ICachable 4 | { 5 | public string ID { get; } 6 | } -------------------------------------------------------------------------------- /LevelImposter/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "LevelImposter-BepInEx": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/ITriggerHandle.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Trigger; 2 | 3 | public interface ITriggerHandle 4 | { 5 | public void OnTrigger(TriggerSignal signal); 6 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIAnimProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIAnimProperty 7 | { 8 | public LIAnimKeyframe[]? keyframes { get; set; } 9 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Values/IBoolValue.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Core; 2 | 3 | public interface IBoolValue 4 | { 5 | public const int MAX_DEPTH = 255; // Prevent infinite loops 6 | 7 | public bool GetValue(int depth = 0); 8 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Values/DelegateBoolValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | public class DelegateBoolValue(Func getValue) : IBoolValue 6 | { 7 | public bool GetValue(int depth) => getValue(); 8 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/MapType.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Core; 2 | 3 | public enum MapType 4 | { 5 | Skeld = 0, 6 | Mira, 7 | Polus, 8 | Dleks, 9 | Airship, 10 | Fungle, 11 | Submerged, 12 | LevelImposter 13 | } -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "6.0.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ], 10 | "rollForward": false 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LISpriteAnimationFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LISpriteAnimationFrame 7 | { 8 | public Guid spriteID { get; set; } 9 | public float delay { get; set; } 10 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LITrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LITrigger 7 | { 8 | public string id { get; set; } 9 | public Guid? elemID { get; set; } 10 | public string? triggerID { get; set; } 11 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loadables/LoadableAudio.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | 3 | namespace LevelImposter.AssetLoader; 4 | 5 | public readonly struct LoadableAudio(string id, IDataStore dataStore) : ICachable 6 | { 7 | public string ID => id; 8 | public IDataStore DataStore => dataStore; 9 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIAnimTarget.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | [Serializable] 7 | public class LIAnimTarget 8 | { 9 | public Guid id { get; set; } 10 | public Dictionary properties { get; set; } 11 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LISpriteAnimation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LISpriteAnimation 7 | { 8 | public Guid id { get; set; } 9 | public string? type { get; set; } 10 | public LISpriteAnimationFrame[] frames { get; set; } 11 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Models/LIConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | [Serializable] 7 | public class LIConfig 8 | { 9 | public string? LastMapJoined { get; set; } 10 | public Dictionary? RandomWeights { get; set; } 11 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/MissingShipException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class MissingShipException() : Exception(EXCEPTION_STRING) 7 | { 8 | public const string EXCEPTION_STRING = "LIShipStatus.GetInstanceOrNull() or LIShipStatus.ShipStatus is null"; 9 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LICollider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LICollider 7 | { 8 | public Guid id { get; set; } 9 | public bool blocksLight { get; set; } 10 | public bool isSolid { get; set; } 11 | public Point[] points { get; set; } 12 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/DataBlock/IMemoryBlock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Represents a block of data in memory. 7 | /// Must be disposed of after use to free resources. 8 | /// 9 | public interface IMemoryBlock : IDisposable 10 | { 11 | public byte[] Get(); 12 | } -------------------------------------------------------------------------------- /LevelImposter/Assets/Assets.manifest: -------------------------------------------------------------------------------- 1 | ManifestFileVersion: 0 2 | CRC: 97968246 3 | AssetBundleManifest: 4 | AssetBundleInfos: 5 | Info_0: 6 | Name: defaultthumbnail 7 | Dependencies: {} 8 | Info_1: 9 | Name: loadingbar 10 | Dependencies: {} 11 | Info_2: 12 | Name: shop 13 | Dependencies: {} 14 | -------------------------------------------------------------------------------- /LevelImposter/Core/Models/Point.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | [Serializable] 7 | public class Point 8 | { 9 | public float x { get; set; } 10 | public float y { get; set; } 11 | 12 | public Vector2 toVector() 13 | { 14 | return new Vector2(x, y); 15 | } 16 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIAnimKeyframe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIAnimKeyframe 7 | { 8 | //public int id { get; set; } // Not needed at runtime 9 | public float t { get; set; } 10 | public float value { get; set; } 11 | public string? nextCurve { get; set; } 12 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/MeetingTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Trigger; 2 | 3 | public class MeetingTriggerHandle : ITriggerHandle 4 | { 5 | public void OnTrigger(TriggerSignal signal) 6 | { 7 | if (signal.TriggerID != "callMeeting") 8 | return; 9 | 10 | PlayerControl.LocalPlayer.CmdReportDeadBody(null); 11 | } 12 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LISpriteAtlas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LISpriteAtlas 7 | { 8 | public Guid id { get; set; } 9 | public Guid assetID { get; set; } 10 | public int x { get; set; } 11 | public int y { get; set; } 12 | public int w { get; set; } 13 | public int h { get; set; } 14 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loadables/LoadedTexture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.AssetLoader; 6 | 7 | public class LoadedTexture(Texture2D texture) 8 | { 9 | public Texture2D Texture => texture; 10 | 11 | public static implicit operator Texture2D(LoadedTexture loadedTexture) => loadedTexture.Texture; 12 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIMinigameSprite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIMinigameSprite 7 | { 8 | public Guid id { get; set; } 9 | public string type { get; set; } 10 | public Guid? spriteID { get; set; } 11 | 12 | // Legacy 13 | [Obsolete("Use spriteID instead")] public string? spriteData { get; set; } 14 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Models/LICallback.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | [Serializable] 7 | public class LICallback 8 | { 9 | [JsonPropertyName("v")] public int Version { get; set; } 10 | [JsonPropertyName("error")] public string? Error { get; set; } 11 | [JsonPropertyName("data")] public T? Data { get; set; } 12 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/ScrollBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | internal class ScrollBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-blankscroll") 11 | return; 12 | 13 | obj.AddComponent(); 14 | } 15 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/LobbyMenuPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Shop; 4 | 5 | /* 6 | * Initializes a new Map Console in the Lobby 7 | */ 8 | 9 | [HarmonyPatch(typeof(LobbyBehaviour), nameof(LobbyBehaviour.Start))] 10 | public static class LobbyMenuInitPatch 11 | { 12 | public static void Postfix() 13 | { 14 | LobbyConsoleBuilder.Build(); 15 | } 16 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Trigger/TriggerAnimBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class TriggerAnimBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-triggeranim") 11 | return; 12 | 13 | obj.AddComponent(); 14 | } 15 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/FloatBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | internal class FloatBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-blankfloat") 11 | return; 12 | 13 | // Build Floating Parent 14 | obj.AddComponent(); 15 | } 16 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Components/Spinner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | /// 7 | /// Just a simple spinning object 8 | /// 9 | public class Spinner(IntPtr intPtr) : MonoBehaviour(intPtr) 10 | { 11 | private readonly float _speed = -90f; 12 | 13 | public void Update() 14 | { 15 | transform.Rotate(0, 0, _speed * Time.deltaTime); 16 | } 17 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Models/GHAsset.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | [Serializable] 7 | public class GHAsset 8 | { 9 | [JsonPropertyName("name")] public string? Name { get; set; } 10 | [JsonPropertyName("size")] public int Size { get; set; } 11 | 12 | [JsonPropertyName("browser_download_url")] 13 | public string? BrowserDownloadURL { get; set; } 14 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loadables/LoadedSprite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.AssetLoader; 6 | 7 | public class LoadedSprite(Sprite sprite, LoadedTexture texture) 8 | { 9 | public Sprite Sprite => sprite; 10 | public LoadedTexture Texture => texture; 11 | 12 | public static implicit operator Sprite(LoadedSprite loadedSprite) => loadedSprite.Sprite; 13 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Models/SerializedAssetDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace LevelImposter.DB; 5 | 6 | [Serializable] 7 | public class SerializedAssetDB 8 | { 9 | public List ObjectDB { get; set; } 10 | public List TaskDB { get; set; } 11 | public List SoundDB { get; set; } 12 | public List PathDB { get; set; } 13 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/ButtonPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Shop; 4 | 5 | /* 6 | * Replaces the Inventory 7 | * Button in the Main Menu 8 | * with the Map Shop Button 9 | */ 10 | [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))] 11 | public static class ButtonPatch 12 | { 13 | public static void Postfix() 14 | { 15 | MainMenuBuilder.Build(); 16 | } 17 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Stores/ZIPEntryStore.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | public class ZIPEntryStore(string zipFilePath, string zipEntryName) : IDataStore 6 | { 7 | public IMemoryBlock LoadToMemory() 8 | { 9 | using var stream = OpenStream(); 10 | return stream.ToDataBlock(); 11 | } 12 | 13 | public Stream OpenStream() 14 | { 15 | return new ZIPEntryStream(zipFilePath, zipEntryName); 16 | } 17 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/DataBlock/ExistingMemoryBlock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// A wrapper for an existing byte array as a memory block. 8 | /// 9 | /// The existing byte array. 10 | internal class ExistingMemoryBlock(byte[] data) : IMemoryBlock 11 | { 12 | public void Dispose() {} 13 | 14 | public byte[] Get() 15 | { 16 | return data; 17 | } 18 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Models/LIUpdate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | [Serializable] 7 | public class LIUpdate 8 | { 9 | [JsonPropertyName("name")] public string? Name { get; set; } 10 | [JsonPropertyName("tag")] public string? Tag { get; set; } 11 | [JsonPropertyName("downloadURL")] public string? DownloadURL { get; set; } 12 | 13 | public bool IsCurrent => Tag?.Equals(LevelImposter.DisplayVersion) ?? false; 14 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the LevelImposter Among Us Mod 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is the new feature?** 11 | A clear and concise description of the feature you would like to be added. 12 | 13 | **Why should this feature be added?** 14 | What makes this feature useful for others? 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIMapProperties.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIMapProperties 7 | { 8 | public string? bgColor { get; set; } 9 | public string? exileID { get; set; } 10 | public bool? showPingIndicator { get; set; } 11 | public bool? pixelArtMode { get; set; } 12 | public bool? preloadAllGIFs { get; set; } 13 | public bool? triggerLogging { get; set; } 14 | public bool? triggerDetectStackOverflow { get; set; } 15 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/ExitGamePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Shop; 3 | 4 | namespace LevelImposter.Core 5 | { 6 | /// 7 | /// Unloads the map from memory after disconnecting from a game. 8 | /// 9 | [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.ExitGame))] 10 | public static class ExitGamePatch 11 | { 12 | public static void Postfix() 13 | { 14 | MapLoader.UnloadMap(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIColor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | [Serializable] 7 | public class LIColor 8 | { 9 | public float r { get; set; } 10 | public float g { get; set; } 11 | public float b { get; set; } 12 | public float a { get; set; } 13 | 14 | public Color ToUnity() 15 | { 16 | return new Color( 17 | r / 255.0f, 18 | g / 255.0f, 19 | b / 255.0f, 20 | a 21 | ); 22 | } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LISound.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LISound 7 | { 8 | public Guid id { get; set; } 9 | public string? type { get; set; } 10 | public float volume { get; set; } 11 | public Guid? dataID { get; set; } 12 | public string? channel { get; set; } 13 | public bool isPreset { get; set; } 14 | public string? presetID { get; set; } 15 | 16 | // Legacy 17 | [Obsolete("Use dataID instead")] public string? data { get; set; } 18 | } -------------------------------------------------------------------------------- /LevelImposter/Assets/defaultthumbnail.manifest: -------------------------------------------------------------------------------- 1 | ManifestFileVersion: 0 2 | CRC: 3906825273 3 | Hashes: 4 | AssetFileHash: 5 | serializedVersion: 2 6 | Hash: dd7c544d36b8a90bf97ef2b73dc7d192 7 | TypeTreeHash: 8 | serializedVersion: 2 9 | Hash: 4df8c69f2c8a6ad9e3cbf82bb83e4dc3 10 | HashAppended: 0 11 | ClassTypes: 12 | - Class: 28 13 | Script: {instanceID: 0} 14 | - Class: 213 15 | Script: {instanceID: 0} 16 | SerializeReferenceClassIdentifiers: [] 17 | Assets: 18 | - Assets/Sprites/DefaultThumbnail.png 19 | Dependencies: [] 20 | -------------------------------------------------------------------------------- /LevelImposter/Builders/Generic/ColorBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | /// 7 | /// Adds color to SpriteRenderers 8 | /// 9 | public class ColorBuilder : IElemBuilder 10 | { 11 | public void OnBuild(LIElement elem, GameObject obj) 12 | { 13 | var spriteRenderer = obj.GetComponent(); 14 | if (spriteRenderer) 15 | spriteRenderer.color = elem.properties.color?.ToUnity() ?? Color.white; 16 | } 17 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Patches/DBPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.DB; 5 | 6 | /* 7 | * Loads all ship prefabs 8 | * to memory so that au assets 9 | * can be stored in a db 10 | */ 11 | [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.Awake))] 12 | public static class DBPatch 13 | { 14 | public static void Postfix() 15 | { 16 | var dbObj = new GameObject("AssetDB"); 17 | Object.DontDestroyOnLoad(dbObj); 18 | dbObj.AddComponent(); 19 | } 20 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/TeleportTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | 3 | namespace LevelImposter.Trigger; 4 | 5 | public class TeleportTriggerHandle : ITriggerHandle 6 | { 7 | public void OnTrigger(TriggerSignal signal) 8 | { 9 | if (signal.TriggerID != "teleportonce") 10 | return; 11 | 12 | // Get Teleporter 13 | var teleporter = signal.TargetObject.GetComponentOrThrow(); 14 | 15 | // Teleport players in area 16 | teleporter.TeleportOnce(); 17 | } 18 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/RepeatTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Trigger; 2 | 3 | public class RepeatTriggerHandle : ITriggerHandle 4 | { 5 | public void OnTrigger(TriggerSignal signal) 6 | { 7 | if (signal.TriggerID != "repeat") 8 | return; 9 | 10 | // Fire Trigger 11 | for (var i = 1; i <= 8; i++) 12 | { 13 | TriggerSignal newSignal = new(signal.TargetObject, $"onRepeat {i}", signal); 14 | TriggerSystem.GetInstance().FireTrigger(newSignal); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Sub/PathDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.DB; 4 | 5 | /// 6 | /// Database of string paths for Transforms 7 | /// 8 | public class PathDB(SerializedAssetDB serializedDB) : SubDB(serializedDB) 9 | { 10 | public override void Load() 11 | { 12 | DB.PathDB.ForEach(elem => { Add(elem.ID, elem.Paths); }); 13 | } 14 | 15 | [Serializable] 16 | public class DBElement 17 | { 18 | public string ID { get; set; } 19 | public string[] Paths { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Trigger/TriggerStartBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class TriggerStartBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-triggerstart") 11 | return; 12 | 13 | // TODO: Add onHideAndSeekStart & onClassicStart 14 | var trigger = obj.AddComponent(); 15 | trigger.SetTrigger(obj, "onStart"); 16 | obj.SetActive(true); 17 | } 18 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/ConnectionPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Hazel.Udp; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Increases the max timeout for the low level connection to the server. 8 | /// 9 | [HarmonyPatch(typeof(UnityUdpClientConnection), nameof(UnityUdpClientConnection.ConnectAsync))] 10 | public static class TimeoutPatch 11 | { 12 | public static void Postfix(UnityUdpClientConnection __instance) 13 | { 14 | __instance.DisconnectTimeoutMs = LIConstants.MAX_CONNECTION_TIMEOUT * 1000; 15 | } 16 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Queue/ItemCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace LevelImposter.AssetLoader; 4 | 5 | public class ItemCache 6 | { 7 | private readonly Dictionary _cachedItems = new(); 8 | public int Count => _cachedItems.Count; 9 | 10 | public void Add(string id, T asset) 11 | { 12 | _cachedItems[id] = asset; 13 | } 14 | 15 | public T? Get(string id) 16 | { 17 | return _cachedItems.GetValueOrDefault(id); 18 | } 19 | 20 | public void Clear() 21 | { 22 | _cachedItems.Clear(); 23 | } 24 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/MapChangePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Core; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | /* 7 | * Switches custom map to fallback 8 | * if a vanilla map is selected 9 | */ 10 | [HarmonyPatch(typeof(GameOptionsMapPicker), nameof(GameOptionsMapPicker.SelectMap), typeof(int))] 11 | public static class MapChangePatch 12 | { 13 | public static void Postfix() 14 | { 15 | if (GameState.IsCustomMapSelected) 16 | return; 17 | 18 | MapLoader.SetFallback(true); 19 | MapSync.SyncMapID(); 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Models/GHRelease.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | [Serializable] 7 | public class GHRelease 8 | { 9 | [JsonPropertyName("tag_name")] public string? TagName { get; set; } 10 | [JsonPropertyName("name")] public string? Name { get; set; } 11 | [JsonPropertyName("body")] public string? Body { get; set; } 12 | [JsonPropertyName("assets")] public GHAsset[]? Assets { get; set; } 13 | 14 | public override string ToString() 15 | { 16 | return Name ?? base.ToString() ?? "GHRelease"; 17 | } 18 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Generic/MinigameSpriteBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | /// 7 | /// Adds the MinigameSprites component (if needed) 8 | /// 9 | public class MinigameSpriteBuilder : IElemBuilder 10 | { 11 | public void OnBuild(LIElement elem, GameObject obj) 12 | { 13 | if (elem.properties.minigames == null && elem.properties.minigameProps == null) 14 | return; 15 | var minigameSprites = obj.AddComponent(); 16 | minigameSprites.Init(elem); 17 | } 18 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/MapObjectData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Il2CppInterop.Runtime.Attributes; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | public class MapObjectData(IntPtr intPtr) : MonoBehaviour(intPtr) 8 | { 9 | [HideFromIl2Cpp] public LIElement Element { get; private set; } = new(); 10 | 11 | [HideFromIl2Cpp] public Guid ID => Element.id; 12 | [HideFromIl2Cpp] public LIProperties Properties => Element.properties; 13 | 14 | [HideFromIl2Cpp] 15 | public void SetSourceElement(LIElement sourceElement) 16 | { 17 | Element = sourceElement; 18 | } 19 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIMinigameProps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIMinigameProps 7 | { 8 | public LIColor? reactorColorGood { get; set; } 9 | public LIColor? reactorColorBad { get; set; } 10 | public LIColor? lightsColorOn { get; set; } 11 | public LIColor? lightsColorOff { get; set; } 12 | public LIColor? fuelColor { get; set; } 13 | public LIColor? fuelBgColor { get; set; } 14 | public LIColor? weaponsColor { get; set; } 15 | public string? qrCodeText { get; set; } 16 | public bool? isStarfieldEnabled { get; set; } 17 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ "push", "pull_request" ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v6 11 | with: 12 | submodules: true 13 | 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@v5 16 | with: 17 | dotnet-version: 8.x 18 | 19 | - name: Run the Cake script 20 | uses: cake-build/cake-action@v3 21 | 22 | - uses: actions/upload-artifact@v5 23 | with: 24 | name: LevelImposter.dll 25 | path: LevelImposter/bin/Release/net6.0/LevelImposter.dll 26 | -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Stores/FileStore.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Represents an entire file as a data store. 7 | /// See for chunked file access. 8 | /// 9 | /// The path to the file. 10 | public class FileStore(string filePath) : IDataStore 11 | { 12 | public IMemoryBlock LoadToMemory() 13 | { 14 | using var stream = OpenStream(); 15 | return stream.ToDataBlock(); 16 | } 17 | 18 | public Stream OpenStream() 19 | { 20 | return File.OpenRead(filePath); 21 | } 22 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/SFXLoadingPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Disable SFX during map loading sequence. 7 | /// 8 | [HarmonyPatch(typeof(Constants), nameof(Constants.ShouldPlaySfx))] 9 | public class SFXLoadingPatch 10 | { 11 | public static bool Prefix(ref bool __result) 12 | { 13 | if (!LIShipStatus.IsInstance()) 14 | return true; 15 | if (LIShipStatus.GetInstance().IsReady) 16 | return true; 17 | 18 | // Prevent SFX from playing 19 | __result = false; 20 | return false; 21 | } 22 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/DummyBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class DummyBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-dummy") 11 | return; 12 | 13 | // ShipStatus 14 | var shipStatus = LIShipStatus.GetInstance().ShipStatus; 15 | 16 | // Add Location 17 | shipStatus.DummyLocations = MapUtils.AddToArr(shipStatus.DummyLocations, obj.transform); 18 | 19 | // TODO: Customize each dummy location with name/outfit 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/ShopPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Shop; 6 | 7 | /* 8 | * Replaces the Inventory 9 | * menu with the Map Shop 10 | */ 11 | [HarmonyPatch(typeof(PlayerCustomizationMenu), nameof(PlayerCustomizationMenu.Start))] 12 | public static class ShopPatch 13 | { 14 | public static bool Prefix(PlayerCustomizationMenu __instance) 15 | { 16 | if (GameState.IsInLobby) 17 | return true; 18 | 19 | Object.Destroy(__instance.gameObject); 20 | ShopBuilder.Build(); 21 | return false; 22 | } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/LayerBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | internal class LayerBuilder : IElemBuilder 8 | { 9 | private readonly Dictionary _typeLayers = new() 10 | { 11 | { "util-ghostcollider", Layer.Default }, 12 | { "util-binocularscollider", Layer.UICollider } 13 | }; 14 | 15 | public void OnBuild(LIElement elem, GameObject obj) 16 | { 17 | if (!_typeLayers.ContainsKey(elem.type)) 18 | return; 19 | 20 | obj.layer = (int)_typeLayers[elem.type]; 21 | } 22 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/UpdateButtonPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Core; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | /* 7 | * Adds the update button to 8 | * the Main Menu screen 9 | */ 10 | [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))] 11 | public static class UpdateButtonPatch 12 | { 13 | public static void Postfix() 14 | { 15 | GitHubAPI.GetLatestRelease(release => 16 | { 17 | if (!GitHubAPI.IsCurrent(release)) 18 | UpdateButtonBuilder.Build(); 19 | }, error => { LILogger.Warn("Failed to check for updates: " + error); }); 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | [Serializable] 7 | public class LIMap : LIMetadata 8 | { 9 | // LIM2 10 | [JsonIgnore] public const int LIM_VERSION = 3; 11 | public LIElement[] elements { get; set; } 12 | public LIMapProperties properties { get; set; } 13 | public LISpriteAtlas[] spriteAtlases { get; set; } 14 | 15 | [JsonIgnore] 16 | public bool isLegacy 17 | { 18 | get => v < LIM_VERSION; 19 | set => v = value ? 1 : LIM_VERSION; 20 | } 21 | 22 | [JsonIgnore] public MapAssetDB? mapAssetDB { get; set; } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/MeetingOverlayPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Fixes a bug when PoolablePlayer runs Awake 7 | /// too early while meeting prefabs are being instantiated. 8 | /// 9 | [HarmonyPatch(typeof(MeetingCalledAnimation), nameof(MeetingCalledAnimation.Initialize))] 10 | public static class MeetingOverlayPatch 11 | { 12 | public static void Prefix(MeetingCalledAnimation __instance) 13 | { 14 | if (!LIShipStatus.IsInstance()) 15 | return; 16 | 17 | // Re-initialize player parts 18 | __instance.playerParts.InitBody(); 19 | } 20 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Stores/MemoryStore.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Stores a block of data in memory. 8 | /// 9 | /// The raw data to store. 10 | public class MemoryStore(byte[] rawData) : IDataStore 11 | { 12 | public IMemoryBlock LoadToMemory() 13 | { 14 | // We can reuse the existing data block since the data is already in memory. 15 | return new ExistingMemoryBlock(rawData); 16 | } 17 | 18 | public Stream OpenStream() 19 | { 20 | return new MemoryStream(rawData); 21 | } 22 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report undesired behaviours in the LevelImposter Among Us Mod 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What Happened? 11 | A clear and concise description of what the bug is. 12 | 13 | ## What Should Have Happened? 14 | A clear and concise description of what you expected to happen. 15 | 16 | ## Versions 17 | - **LevelImposter:** [Insert LI Version] 18 | - **Among Us:** [Insert AU Version] 19 | 20 | ## Log Output *(If Applicable)* 21 | ``` 22 | [Insert Log Output Here] 23 | ``` 24 | 25 | ## Additional Details 26 | Any additional details or screenshots you feel should be mentioned 27 | -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Stores/FileChunkStore.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Represents a specific chunk (offset + length) of a file as a data store. 7 | /// A stream opened from this store will only allow access to the specified chunk. 8 | /// 9 | public class FileChunkStore(string filePath, long offset, long length) : IDataStore 10 | { 11 | public IMemoryBlock LoadToMemory() 12 | { 13 | using var stream = OpenStream(); 14 | return stream.ToDataBlock(); 15 | } 16 | 17 | public Stream OpenStream() 18 | { 19 | return new FileChunkStream(filePath, offset, length); 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/EditableLadderConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Allows Ladders to have a different cooldown 7 | /// 8 | public class EditableLadderConsole(IntPtr intPtr) : Ladder(intPtr) 9 | { 10 | private float _cooldownDuration = 5.0f; 11 | 12 | public override float MaxCoolDown => _cooldownDuration; 13 | 14 | /// 15 | /// Sets the cooldown duration of the ladder 16 | /// 17 | /// Duration in seconds 18 | public void SetCooldownDuration(float duration) 19 | { 20 | _cooldownDuration = duration; 21 | } 22 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/ExileTextPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Fixes a bug where the ExileController.completeString would reference 7 | /// undefined memory when overloaded by LIExileController. 8 | /// 9 | [HarmonyPatch(typeof(ExileController), nameof(ExileController.Begin))] 10 | public class ExileTextPatch 11 | { 12 | public static string LastExileText { get; private set; } = string.Empty; 13 | 14 | public static void Postfix(ExileController __instance) 15 | { 16 | if (!LIShipStatus.IsInstance()) 17 | return; 18 | 19 | LastExileText = __instance.completeString; 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/DeathTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | 4 | namespace LevelImposter.Trigger; 5 | 6 | public class DeathTriggerHandle : ITriggerHandle 7 | { 8 | public void OnTrigger(TriggerSignal signal) 9 | { 10 | if (signal.TriggerID != "killArea") 11 | return; 12 | 13 | // Get LIDeathArea 14 | var deathArea = signal.TargetObject.GetComponent(); 15 | 16 | // Check if the area is a LIDeathArea 17 | if (deathArea == null) 18 | throw new Exception($"{signal.TargetObject} is missing an LIDeathArea"); 19 | 20 | // Kill all players in area 21 | deathArea.KillAllPlayers(); 22 | } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIElement 7 | { 8 | public Guid id { get; set; } 9 | public string name { get; set; } 10 | public string type { get; set; } 11 | public float x { get; set; } 12 | public float y { get; set; } 13 | public float z { get; set; } 14 | public float xScale { get; set; } 15 | public float yScale { get; set; } 16 | public float rotation { get; set; } 17 | 18 | public LIProperties properties { get; set; } 19 | public Guid? parentID { get; set; } 20 | 21 | public override string ToString() 22 | { 23 | return name + "(" + type + ")[" + id + "]"; 24 | } 25 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Trigger/TriggerDeathBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class TriggerDeathBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-triggerdeath") 11 | return; 12 | 13 | // Colliders 14 | Collider2D[] colliders = obj.GetComponentsInChildren(); 15 | foreach (var collider in colliders) 16 | collider.isTrigger = true; 17 | 18 | // Trigger Area 19 | var deathArea = obj.AddComponent(); 20 | deathArea.SetCreateDeadBody(elem.properties.createDeadBody ?? true); 21 | } 22 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/NodeMinigamePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Fixes a bug where WeatherSwitchGame renames 7 | /// all of the node minigame objects to their translated names. 8 | /// 9 | [HarmonyPatch(typeof(WeatherSwitchGame), nameof(WeatherSwitchGame.Start))] 10 | public class NodeMinigamePatch 11 | { 12 | public static void Postfix(WeatherSwitchGame __instance) 13 | { 14 | if (!LIShipStatus.IsInstance()) 15 | return; 16 | 17 | // Rename all of the nodes to generic names. 18 | for (var i = 0; i < __instance.Controls.Length; i++) 19 | __instance.Controls[i].name = $"Node{i}"; 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Loading/LoadingCameraPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.AssetLoader; 3 | using LevelImposter.Shop; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Disables camera movement while still in loading screen. 9 | /// 10 | [HarmonyPatch(typeof(FollowerCamera), nameof(FollowerCamera.Update))] 11 | public static class LoadingCameraPatch 12 | { 13 | public static bool Prefix(FollowerCamera __instance) 14 | { 15 | if (!LIShipStatus.IsInstance()) 16 | return true; 17 | 18 | if (!MapLoader.IsLoading) 19 | return true; 20 | 21 | __instance.centerPosition = __instance.transform.position; 22 | return false; 23 | } 24 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Ship/ShipStatusPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Shop; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Appends the LIShipStatus component to LevelImposter maps. 8 | /// 9 | [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Awake))] 10 | public static class ShipStatusPatch 11 | { 12 | public static void Prefix(ShipStatus __instance) 13 | { 14 | //UnityToMapGenerator.GenerateMap(__instance); 15 | 16 | if (GameState.IsCustomMapSelected) 17 | __instance.gameObject.AddComponent(); 18 | else if (!MapLoader.IsFallback) 19 | LILogger.Msg("Another mod has changed the current map state"); 20 | } 21 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Animations/CamFollowPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Modifies the surveillance camera panel (util-cams*) 7 | /// to follow a moving camera (util-cam) 8 | /// 9 | [HarmonyPatch(typeof(PlanetSurveillanceMinigame), nameof(PlanetSurveillanceMinigame.Update))] 10 | public static class CamFollowPatch 11 | { 12 | public static void Postfix(PlanetSurveillanceMinigame __instance) 13 | { 14 | if (!LIShipStatus.IsInstance()) 15 | return; 16 | 17 | var survCamera = __instance.survCameras[__instance.currentCamera]; 18 | __instance.Camera.transform.position = survCamera.transform.position + survCamera.Offset; 19 | } 20 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/PingPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | /* 7 | * Gives credit to map makers 8 | * through on-screen text in 9 | * the bottom left corner of the screen 10 | */ 11 | [HarmonyPatch(typeof(HudManager), nameof(HudManager.Start))] 12 | public static class PingPatch 13 | { 14 | public static void Postfix(HudManager __instance) 15 | { 16 | if (LobbyVersionTag.Instance != null) 17 | return; 18 | 19 | // Create Lobby Version Tag 20 | var lobbyVersionTag = new GameObject("LobbyVersionTag"); 21 | lobbyVersionTag.AddComponent(); 22 | lobbyVersionTag.transform.SetParent(__instance.transform); 23 | } 24 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/PlayerMoverBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class PlayerMoverBuilder : IElemBuilder 7 | { 8 | private uint _playerMoverCounter = 1; 9 | 10 | public void OnBuild(LIElement elem, GameObject obj) 11 | { 12 | if (elem.type != "util-playermover") 13 | return; 14 | 15 | // Colliders 16 | Collider2D[] colliders = obj.GetComponentsInChildren(); 17 | foreach (var collider in colliders) 18 | collider.isTrigger = true; 19 | 20 | // Add Component 21 | var playerMover = obj.AddComponent(); 22 | playerMover.SetObjectID(_playerMoverCounter++); 23 | } 24 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/AdminTablePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Uses isActiveAndEnabled as a dependency for MapConsole.CanUse. 7 | /// Needed for enabling/disabling the Admin Table via triggers. 8 | /// 9 | [HarmonyPatch(typeof(MapConsole), nameof(MapConsole.CanUse))] 10 | public class AdminTablePatch 11 | { 12 | public static void Postfix( 13 | Console __instance, 14 | [HarmonyArgument(1)] ref bool canUse, 15 | [HarmonyArgument(2)] ref bool couldUse) 16 | { 17 | if (!LIShipStatus.IsInstance()) 18 | return; 19 | 20 | canUse &= __instance.isActiveAndEnabled; 21 | couldUse &= __instance.isActiveAndEnabled; 22 | } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/HorsePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core 4 | { 5 | #pragma warning disable CS0162 // Uses constants, so ignore unreachable code warning 6 | 7 | /// 8 | /// Disables the april-fools horse mode due to incompatibility with lots of mods. 9 | /// 10 | /* 11 | [HarmonyPatch(typeof(Constants), nameof(Constants.ShouldHorseAround))] 12 | public static class HorsePatch 13 | { 14 | public static bool Prefix(ref bool __result) 15 | { 16 | if (!LIConstants.OVERRIDE_HORSE_MODE) 17 | return true; 18 | 19 | __result = LIConstants.ENABLE_HORSE_MODE; 20 | return false; 21 | } 22 | } 23 | */ 24 | 25 | #pragma warning restore CS0162 26 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/DataBlock/PoolableMemoryBlock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Creates a memory block by renting a byte array from the shared array pool. 8 | /// 9 | /// The size of the memory block to rent. 10 | internal class PoolableMemoryBlock(int size) : IMemoryBlock 11 | { 12 | private static readonly ArrayPool Pool = ArrayPool.Shared; 13 | private readonly byte[] _data = Pool.Rent(size); 14 | 15 | /// 16 | /// Returns the rented memory back to the pool. 17 | /// 18 | public void Dispose() 19 | { 20 | Pool.Return(_data); 21 | } 22 | 23 | public byte[] Get() 24 | { 25 | return _data; 26 | } 27 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Trigger/TriggerShakeBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class TriggerShakeBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-triggershake") 11 | return; 12 | 13 | // Colliders 14 | Collider2D[] colliders = obj.GetComponentsInChildren(); 15 | foreach (var collider in colliders) 16 | collider.isTrigger = true; 17 | 18 | // Trigger Area 19 | var shakeArea = obj.AddComponent(); 20 | shakeArea.SetParameters( 21 | elem.properties.shakeAmount ?? 0.03f, 22 | elem.properties.shakePeriod ?? 400.0f 23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | [Serializable] 6 | public class LIMetadata 7 | { 8 | public int v { get; set; } 9 | public string id { get; set; } 10 | public int? idVersion { get; set; } 11 | public string name { get; set; } 12 | public string description { get; set; } 13 | public string authorID { get; set; } 14 | public string authorName { get; set; } 15 | public bool isPublic { get; set; } 16 | public bool isVerified { get; set; } 17 | public long createdAt { get; set; } 18 | public string downloadURL { get; set; } 19 | public string thumbnailURL { get; set; } 20 | public Guid? remixOf { get; set; } 21 | 22 | public override string ToString() 23 | { 24 | return $"{name}[{id}]"; 25 | } 26 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Ship/MixupPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Builders; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Normally, mushroom mixup is handled by 8 | /// FungleShipStatus. This bypasses that 9 | /// dependency by supplying it's own system. 10 | /// 11 | [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.IsMushroomMixupActive))] 12 | public static class MixupPatch 13 | { 14 | public static bool Prefix(PlayerControl __instance, ref bool __result) 15 | { 16 | if (!LIShipStatus.IsInstance()) 17 | return true; 18 | 19 | __result = (SabMixupBuilder.SabotageSystem?.IsActive ?? false) || 20 | __instance.CurrentOutfitType == PlayerOutfitType.MushroomMixup; 21 | return false; 22 | } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/DevelopmentPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Hazel.Udp; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Decreases the minimum player count to start the game. 8 | /// 9 | [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Start))] 10 | public static class MinPlayerPatch 11 | { 12 | public static void Postfix(GameStartManager __instance) 13 | { 14 | if (LIConstants.IS_DEVELOPMENT_BUILD) 15 | __instance.MinPlayers = 1; 16 | } 17 | } 18 | 19 | /// 20 | /// Disables the end game condition check. 21 | /// 22 | [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] 23 | public static class EndGamePatch 24 | { 25 | public static bool Prefix() 26 | { 27 | return !LIConstants.IS_DEVELOPMENT_BUILD; 28 | } 29 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Trigger/TriggerAreaBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class TriggerAreaBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-triggerarea") 11 | return; 12 | 13 | // Colliders 14 | Collider2D[] colliders = obj.GetComponentsInChildren(); 15 | foreach (var collider in colliders) 16 | collider.isTrigger = true; 17 | 18 | // Ghost 19 | if (elem.properties.isGhostEnabled ?? false) 20 | obj.layer = (int)Layer.Default; 21 | 22 | // Trigger Area 23 | var triggerArea = obj.AddComponent(); 24 | triggerArea.SetClientSide(elem.properties.triggerClientSide != false); 25 | } 26 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/PlatformMagnitudePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Bypasses the magnitude maximum when calling MovingPlatformBehaviour.Use. 7 | /// 8 | [HarmonyPatch(typeof(MovingPlatformBehaviour), nameof(MovingPlatformBehaviour.Use), typeof(PlayerControl))] 9 | public static class PlatformMagnitudePatch 10 | { 11 | public static bool Prefix([HarmonyArgument(0)] PlayerControl player, MovingPlatformBehaviour __instance) 12 | { 13 | if (!LIShipStatus.IsInstance()) 14 | return true; 15 | if (player.Data.IsDead || player.Data.Disconnected || __instance.Target) 16 | return true; 17 | 18 | __instance.IsDirty = true; 19 | __instance.StartCoroutine(__instance.UsePlatform(player)); 20 | 21 | return false; 22 | } 23 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/BinocularsBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | internal class BinocularsBuilder : IElemBuilder 7 | { 8 | public static float OrthographicSize = 3f; 9 | public static Vector2 LastBinocularsPos = Vector2.zero; 10 | public static Vector3 CameraOffset = Vector2.zero; 11 | 12 | public void OnBuild(LIElement elem, GameObject obj) 13 | { 14 | if (!(elem.type == "util-cams4")) 15 | return; 16 | 17 | // Building is done by UtilBuilder, this handles Binoculars properties 18 | OrthographicSize = elem.properties.camZoom ?? 3.0f; 19 | LastBinocularsPos = Vector2.zero; 20 | CameraOffset = new Vector3( 21 | elem.properties.camXOffset ?? 0, 22 | elem.properties.camYOffset ?? 0, 23 | 0 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Values/BasicBoolValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Trigger; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | public class BasicBoolValue(Guid id, bool value) : IBoolValue 7 | { 8 | private bool _value = value; 9 | 10 | public bool GetValue(int depth) 11 | { 12 | return _value; 13 | } 14 | 15 | public void SetValue(bool value, TriggerSignal sourceSignal) 16 | { 17 | _value = value; 18 | OnValueChange(sourceSignal); 19 | } 20 | 21 | private void OnValueChange(TriggerSignal sourceSignal) 22 | { 23 | // Get Target Object 24 | var targetObj = MapObjectDB.Get(id); 25 | if (targetObj == null) 26 | return; 27 | 28 | // Fire Trigger 29 | var signal = new TriggerSignal(targetObj, "onChange", sourceSignal); 30 | TriggerSystem.GetInstance().FireTrigger(signal); 31 | } 32 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Builders/ShopBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace LevelImposter.Shop; 7 | 8 | public static class ShopBuilder 9 | { 10 | private static GameObject? _mapShopPrefab; 11 | 12 | private static GameObject GetShopPrefab() 13 | { 14 | if (_mapShopPrefab == null) 15 | _mapShopPrefab = MapUtils.LoadAssetBundle("shop"); 16 | if (_mapShopPrefab == null) 17 | throw new Exception("The \"shop\" asset bundle was not found in assembly"); 18 | return _mapShopPrefab; 19 | } 20 | 21 | public static void Build() 22 | { 23 | var mapShop = Object.Instantiate(GetShopPrefab()); 24 | mapShop.transform.SetParent(Camera.main.transform, false); 25 | mapShop.transform.localPosition = new Vector3(0, 0, -990.0f); 26 | } 27 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIRpc.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Core; 2 | // Among Us 0 - 65 3 | // TOR 60 - 73, 100 - 149 4 | // Las Monjas 60 - 69, 75 - 194 5 | // StellaRoles 60 - 169 6 | // ToU 100 - 210, 220 - 251 7 | // Submerged 210 - 214 8 | 9 | // LI 90 - 99 (Wow we really need to coordinate this better) 10 | 11 | public enum LIRpc 12 | { 13 | FireTrigger = 90, // Fires a global trigger on an object 14 | UpdateObjectPos, // Used by util-physics object 15 | SyncMapID, // Syncs the map ID in the lobby 16 | SyncRandomSeed, // Syncs a random seed for util-triggerrand 17 | ResetPlayer, // Resets the player on Ctrl-R E S 18 | DownloadCheck, // Warns the host that the client is still downloading the map 19 | KillPlayer, // Used by util-triggerdeath object 20 | SyncPlayerMover, // Used by LIPlayerMover to sync player transforms 21 | Reserved98, 22 | Reserved99 23 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/AnimTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | 3 | namespace LevelImposter.Trigger; 4 | 5 | public class AnimTriggerHandle : ITriggerHandle 6 | { 7 | private GameObjectCoroutineManager _animManager = new(); 8 | 9 | public void OnTrigger(TriggerSignal signal) 10 | { 11 | if (signal.TriggerID != "playAnim" && 12 | signal.TriggerID != "stopAnim" && 13 | signal.TriggerID != "pauseAnim") 14 | return; 15 | 16 | // Get Component 17 | if (!signal.TargetObject.TryGetComponent(out TriggerAnim animator)) 18 | return; 19 | 20 | // Handle 21 | if (signal.TriggerID == "playAnim") 22 | animator.Play(signal); 23 | else if (signal.TriggerID == "stopAnim") 24 | animator.Stop(); 25 | else if (signal.TriggerID == "pauseAnim") 26 | animator.Pause(); 27 | } 28 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/MapButtonsPatch.cs: -------------------------------------------------------------------------------- 1 | using AmongUs.GameOptions; 2 | using HarmonyLib; 3 | using LevelImposter.Core; 4 | 5 | namespace LevelImposter.Shop; 6 | 7 | /* 8 | * Prevents NullReferenceException when 9 | * creating a new lobby 10 | */ 11 | [HarmonyPatch(typeof(GameOptionsMapPicker), nameof(GameOptionsMapPicker.SelectMap), typeof(int))] 12 | public static class MapButtonsPatch 13 | { 14 | public static bool Prefix(GameOptionsMapPicker __instance) 15 | { 16 | // Check if the map is custom 17 | if (!GameState.IsCustomMapSelected) 18 | return true; 19 | 20 | // Switches the map to Skeld 21 | GameOptionsManager.Instance.CurrentGameOptions.SetByte(ByteOptionNames.MapId, (byte)MapType.Skeld); 22 | 23 | // Re-runs the method 24 | __instance.SelectMap((byte)MapType.Skeld); 25 | 26 | // Aborts the method 27 | return false; 28 | } 29 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/SoundPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HarmonyLib; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Makes ambient sounds play under the "Music" channel instead of "SFX" channel. 9 | /// 10 | [HarmonyPatch(typeof(AmbientSoundPlayer), nameof(AmbientSoundPlayer.Start))] 11 | public static class SoundStartPatch 12 | { 13 | public static bool Prefix(AmbientSoundPlayer __instance) 14 | { 15 | if (!LIShipStatus.IsInstance()) 16 | return true; 17 | 18 | var soundName = __instance.name + __instance.GetInstanceID(); 19 | SoundManager.Instance.PlayDynamicSound( 20 | soundName, 21 | __instance.AmbientSound, 22 | true, 23 | new Action(__instance.Dynamics), 24 | SoundManager.Instance.MusicChannel 25 | ); 26 | return false; 27 | } 28 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/GateTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Builders; 2 | using LevelImposter.Core; 3 | 4 | namespace LevelImposter.Trigger; 5 | 6 | public class GateTriggerHandle : ITriggerHandle 7 | { 8 | private const string ON_TRUE = "onTrue"; 9 | private const string ON_FALSE = "onFalse"; 10 | 11 | public void OnTrigger(TriggerSignal signal) 12 | { 13 | if (signal.TriggerID != "triggerGate") 14 | return; 15 | 16 | // Get Value 17 | var elementData = signal.TargetObject.GetLIData(); 18 | var valueObj = ValueBuilder.GetBoolOfID(elementData.Properties.triggerGateValueID); 19 | var value = valueObj.GetValue(0); 20 | 21 | // Fire Trigger 22 | var triggerID = value ? ON_TRUE : ON_FALSE; 23 | TriggerSignal newSignal = new(signal.TargetObject, triggerID, signal); 24 | TriggerSystem.GetInstance().FireTrigger(newSignal); 25 | } 26 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Loading/LoadingShipPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using InnerNet; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Waits for the ship to finish loading before sending the client ready packet. 8 | /// 9 | [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.SendClientReady))] 10 | public static class LoadingShipPatch 11 | { 12 | public static bool Prefix(InnerNetClient __instance) 13 | { 14 | // Continue if not in a game 15 | if (!LIShipStatus.IsInstance()) 16 | return true; 17 | 18 | // Continue if ship is already loaded 19 | if (LIShipStatus.GetInstance().IsReady) 20 | return true; 21 | 22 | // Wait for ship to finish loading, then send packet 23 | MapUtils.WaitForShip(LIConstants.MAX_LOAD_TIME, __instance.SendClientReady); 24 | 25 | // Don't send packet 26 | return false; 27 | } 28 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/DoorTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Trigger; 5 | 6 | public class DoorTriggerHandle : ITriggerHandle 7 | { 8 | public void OnTrigger(TriggerSignal signal) 9 | { 10 | if (signal.TriggerID == "open") 11 | SetDoorOpen(signal.TargetObject, true); 12 | else if (signal.TriggerID == "close") 13 | SetDoorOpen(signal.TargetObject, false); 14 | } 15 | 16 | private void SetDoorOpen(GameObject gameObject, bool isOpen) 17 | { 18 | // Get the PlainDoor component 19 | var doorComponent = gameObject.GetComponent(); 20 | 21 | // Check if the object has a PlainDoor component 22 | if (doorComponent == null) 23 | throw new Exception($"{gameObject} does not have a PlainDoor component"); 24 | 25 | // Set the door state 26 | doorComponent.SetDoorway(isOpen); 27 | } 28 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Stores/IDataStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | public interface IDataStore 8 | { 9 | /// 10 | /// Loads the entire data into memory as an 11 | /// This is required for formats such as PNG 12 | /// that require all data to be present in memory 13 | /// (since they're loaded by Unity APIs). 14 | /// 15 | /// To avoid double-buffering, this is provided separately from . 16 | /// Once the data is no longer needed, it must be disposed to free memory. 17 | /// 18 | /// The byte array containing the data. 19 | public IMemoryBlock LoadToMemory(); 20 | 21 | /// 22 | /// Opens a stream to read the data. 23 | /// 24 | /// >A Stream to read the data. 25 | public Stream OpenStream(); 26 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/SoundTriggerHandles.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | 3 | namespace LevelImposter.Trigger; 4 | 5 | public class SoundTriggerHandle : ITriggerHandle 6 | { 7 | public void OnTrigger(TriggerSignal signal) 8 | { 9 | if (signal.TriggerID != "playonce" && 10 | signal.TriggerID != "playloop" && 11 | signal.TriggerID != "stop") 12 | return; 13 | 14 | // Get Component 15 | var triggerSound = signal.TargetObject.GetComponentOrThrow(); 16 | 17 | // Run Sounds 18 | switch (signal.TriggerID) 19 | { 20 | case "playonce": 21 | triggerSound.Play(false); 22 | break; 23 | case "playloop": 24 | triggerSound.Play(true); 25 | break; 26 | case "stop": 27 | triggerSound.Stop(); 28 | break; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Generic/TransformBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | /// 7 | /// Configures the Transform on the GameObject 8 | /// 9 | public class TransformBuilder : IElemBuilder 10 | { 11 | public void OnPreBuild(LIElement elem, GameObject obj) 12 | { 13 | obj.layer = (int)Layer.Ship; 14 | obj.transform.localPosition = new Vector3(elem.x, elem.y, elem.z); 15 | obj.transform.localRotation = Quaternion.Euler(0, 0, elem.rotation); 16 | obj.transform.localScale = new Vector3(elem.xScale, elem.yScale, 1.0f); 17 | } 18 | 19 | public void OnBuild(LIElement elem, GameObject obj) 20 | { 21 | // Scale Z position by Y if not a util-layer 22 | // Layers will mess up the Z position 23 | if (elem.type != "util-layer") 24 | obj.transform.position = MapUtils.ScaleZPositionByY(obj.transform.position); 25 | } 26 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/MapAssetDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | public class MapAssetDB 9 | { 10 | public Dictionary DB { get; } = new(); 11 | 12 | public void Add(Guid id, byte[] rawData) 13 | { 14 | DB.Add(id, new MemoryStore(rawData)); 15 | } 16 | 17 | public void Add(Guid id, FileChunkStore fileChunkStore) 18 | { 19 | DB.Add(id, fileChunkStore); 20 | } 21 | 22 | public void Add(Guid id, IDataStore streamable) 23 | { 24 | DB.Add(id, streamable); 25 | } 26 | 27 | public IDataStore? Get(Guid? id) 28 | { 29 | if (id == null) 30 | return null; 31 | DB.TryGetValue((Guid)id, out var result); 32 | if (result == null) 33 | LILogger.Warn($"No such map asset with id {id}"); 34 | return result; 35 | } 36 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/LIConstants.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Core; 2 | 3 | /// 4 | /// A list of constants used within LevelImposter 5 | /// 6 | public static class LIConstants 7 | { 8 | public const StringNames MAP_STRING_NAME = (StringNames)392001; // StringName that placeholdes the map names 9 | public const string MAP_NAME = "Random Custom Map"; // Name to populate Constants.MapNames 10 | public const float PLAYER_POS = -5.0f; // Z value of the player 11 | public const int MAX_LOAD_TIME = 9; // Maximum time to build map before aborting 12 | public const int MAX_CONNECTION_TIMEOUT = 10; // Maximum time to wait for a client connection 13 | 14 | public const int 15 | ELEM_WARN_TIME = 200; // Time to warn the user (in ms) when an element is taking too long to load 16 | 17 | public const bool FREEPLAY_FLUSH_CACHE = true; // Whether to flush the cache when entering freeplay maps 18 | public const bool IS_DEVELOPMENT_BUILD = false; // Whether this is a development build 19 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/CamBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using LevelImposter.DB; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | public class CamBuilder : IElemBuilder 8 | { 9 | public void OnBuild(LIElement elem, GameObject obj) 10 | { 11 | if (elem.type != "util-cam") 12 | return; 13 | 14 | // Prefab 15 | var prefab = AssetDB.GetObject(elem.type); 16 | if (prefab == null) 17 | return; 18 | var prefabCam = prefab.GetComponent(); 19 | 20 | // Sprite 21 | MapUtils.CloneSprite(obj, prefab, true); 22 | 23 | // Camera 24 | var survCam = obj.AddComponent(); 25 | survCam.CamName = elem.name; 26 | survCam.Offset = new Vector3( 27 | elem.properties.camXOffset ?? 0, 28 | elem.properties.camYOffset ?? 0 29 | ); 30 | survCam.CamSize = elem.properties.camZoom ?? 3; 31 | survCam.OnAnim = prefabCam.OnAnim; 32 | survCam.OffAnim = prefabCam.OffAnim; 33 | } 34 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Models/Layer.cs: -------------------------------------------------------------------------------- 1 | namespace LevelImposter.Core; 2 | 3 | internal enum Layer 4 | { 5 | /// 6 | /// Only visible in light, interacts with ghosts 7 | /// 8 | Default, 9 | TransparentFX, 10 | IgnoreRaycast, 11 | 12 | /// 13 | /// Made-up layer for physics objects so they collide with each other 14 | /// Camera is modified to render this layer 15 | /// 16 | Physics, 17 | 18 | Water, 19 | 20 | /// 21 | /// Full-brightness and always visible 22 | /// 23 | UI, 24 | Players = 8, 25 | Ship, 26 | 27 | /// 28 | /// Only visible in shadow, blocks light 29 | /// 30 | Shadow, 31 | 32 | /// 33 | /// Automatically hidden by util-display objects 34 | /// 35 | Objects, 36 | 37 | ShortObjects, 38 | IlluminatedBlocking, 39 | Ghost, 40 | UICollider, 41 | DrawShadows, 42 | KeyMapper, 43 | MusicTriggers, 44 | Notifications 45 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/DoorIDPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using HarmonyLib; 3 | using Hazel; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Increases the maximum door ID from 31 to 63 9 | /// by using the unused 6th bit. 10 | /// 11 | [HarmonyPatch(typeof(DoorsSystemType), nameof(DoorsSystemType.UpdateSystem))] 12 | public static class DoorIDPatch 13 | { 14 | public static bool Prefix(DoorsSystemType __instance, [HarmonyArgument(1)] MessageReader msgReader) 15 | { 16 | if (!LIShipStatus.IsInstance()) 17 | return true; 18 | 19 | var b = msgReader.ReadByte(); 20 | var id = b & 63; // <-- This is the only change 21 | var num = b & 192; 22 | 23 | if (num != 64) 24 | return true; 25 | 26 | var openableDoor = ShipStatus.Instance.AllDoors.First(d => d.Id == id); 27 | openableDoor?.SetDoorway(true); 28 | if (openableDoor == null) 29 | LILogger.Warn($"Door ID {id} not found!"); 30 | 31 | __instance.IsDirty = true; 32 | return false; 33 | } 34 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/MinimumTaskPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HarmonyLib; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Removes the minimum task limit on ShipStatus. 8 | /// 9 | [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Begin))] 10 | public class MinimumTaskPatch 11 | { 12 | public static void Prefix(ShipStatus __instance) 13 | { 14 | if (!LIShipStatus.IsInstance()) 15 | return; 16 | 17 | // Get Counts 18 | var shortTaskCount = __instance.ShortTasks.Count; 19 | var longTaskCount = __instance.LongTasks.Count; 20 | var commonTaskCount = __instance.CommonTasks.Count; 21 | 22 | // Update Game Options 23 | var currentOptions = GameOptionsManager.Instance.currentNormalGameOptions; 24 | currentOptions.NumShortTasks = Math.Min(currentOptions.NumShortTasks, shortTaskCount); 25 | currentOptions.NumLongTasks = Math.Min(currentOptions.NumLongTasks, longTaskCount); 26 | currentOptions.NumCommonTasks = Math.Min(currentOptions.NumCommonTasks, commonTaskCount); 27 | } 28 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Trigger/TriggerConsoleBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using LevelImposter.DB; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | public class TriggerConsoleBuilder : IElemBuilder 8 | { 9 | public void OnBuild(LIElement elem, GameObject obj) 10 | { 11 | if (elem.type != "util-triggerconsole") 12 | return; 13 | 14 | // Prefab 15 | var prefab = AssetDB.GetObject("util-computer"); 16 | if (prefab == null) 17 | return; 18 | var prefabRenderer = prefab.GetComponent(); 19 | 20 | // Sprite 21 | var rend = obj.GetComponent(); 22 | obj.layer = (int)Layer.ShortObjects; 23 | if (rend == null) 24 | { 25 | LILogger.Warn($"{elem.name} is missing a sprite."); 26 | return; 27 | } 28 | 29 | rend.material = prefabRenderer.material; 30 | 31 | // Console 32 | var console = obj.AddComponent(); 33 | console.Init(elem); 34 | 35 | // Colliders 36 | MapUtils.CreateDefaultColliders(obj, prefab); 37 | } 38 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Animations/CamPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using PowerTools; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Toggles a GIF animation alongside the 8 | /// regular animation components on surveillance cameras. 9 | /// 10 | [HarmonyPatch(typeof(SurvCamera), nameof(SurvCamera.SetAnimation))] 11 | public static class CamAnimationPatch 12 | { 13 | private const float ANIM_SPEED = 1f; 14 | 15 | public static bool Prefix([HarmonyArgument(0)] bool on, SurvCamera __instance) 16 | { 17 | if (!LIShipStatus.IsInstance()) 18 | return true; 19 | 20 | // Animation 21 | var spriteAnim = __instance.GetComponent(); 22 | var animator = __instance.GetComponent(); 23 | var clipAnim = on ? __instance.OnAnim : __instance.OffAnim; 24 | if (spriteAnim != null && clipAnim != null) 25 | { 26 | spriteAnim.Play(clipAnim); 27 | } 28 | else if (animator != null) 29 | { 30 | animator.PlayType(on ? "camsActive" : "camsInactive"); 31 | } 32 | 33 | return false; 34 | } 35 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/FreeplayOptionsPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HarmonyLib; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Changes the freeplay settings on LI maps. 8 | /// 9 | [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Start))] 10 | public static class FreeplayOptionsPatch 11 | { 12 | public static void Postfix() 13 | { 14 | // Only execute in custom freeplay maps 15 | if (!LIShipStatus.IsInstance()) 16 | return; 17 | if (!GameState.IsInFreeplay) 18 | return; 19 | 20 | // Set the freeplay settings 21 | var gameManager = GameManager.Instance; 22 | var gameOptions = gameManager.LogicOptions.TryCast(); 23 | if (gameOptions == null) 24 | throw new Exception("Failed to cast game options to NormalGameOptionsV07"); 25 | 26 | gameOptions.GameOptions.NumEmergencyMeetings = 5; 27 | gameOptions.GameOptions.DiscussionTime = 0; 28 | gameOptions.GameOptions.EmergencyCooldown = 0; 29 | gameOptions.GameOptions.VotingTime = 20; 30 | gameOptions.GameOptions.KillCooldown = 5; 31 | } 32 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/PolusCamPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Applies SurvCamera.CamSize to PlanetSurveillanceMinigame.Camera. 8 | /// 9 | [HarmonyPatch(typeof(PlanetSurveillanceMinigame), nameof(PlanetSurveillanceMinigame.NextCamera))] 10 | public static class PolusCamPatch 11 | { 12 | public static void Postfix(PlanetSurveillanceMinigame __instance) 13 | { 14 | if (!LIShipStatus.IsInstance()) 15 | return; 16 | 17 | // Get the Camera 18 | var survCamera = __instance.survCameras[__instance.currentCamera]; 19 | 20 | // Set Size 21 | __instance.Camera.orthographicSize = survCamera.CamSize; 22 | 23 | // Set Screen Clear 24 | __instance.Camera.clearFlags = CameraClearFlags.SolidColor; 25 | __instance.Camera.backgroundColor = Camera.main.backgroundColor; 26 | 27 | // Set Z Index 28 | var pos = __instance.Camera.transform.position; 29 | __instance.Camera.transform.position = new Vector3( 30 | pos.x, 31 | pos.y, 32 | -0.1f 33 | ); 34 | } 35 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Triggers/MeetingPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Builders; 3 | using LevelImposter.Trigger; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Calls "onButton" and "onReport" triggers when a meeting is called. 9 | /// 10 | [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.StartMeeting))] 11 | public static class MeetingPatch 12 | { 13 | public const string BUTTON_TRIGGER_ID = "onButton"; 14 | public const string REPORT_TRIGGER_ID = "onReport"; 15 | 16 | public static void Postfix( 17 | [HarmonyArgument(0)] PlayerControl reporter, 18 | [HarmonyArgument(1)] NetworkedPlayerInfo target) 19 | { 20 | if (!LIShipStatus.IsInstance()) 21 | return; 22 | 23 | // Get trigger object 24 | var triggerObj = MeetingOptionsBuilder.TriggerObject; 25 | var triggerID = target == null ? BUTTON_TRIGGER_ID : REPORT_TRIGGER_ID; 26 | 27 | // Call trigger 28 | if (triggerObj != null) 29 | { 30 | TriggerSignal signal = new(triggerObj, triggerID, reporter); 31 | TriggerSystem.GetInstance().FireTrigger(signal); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/RoomUIPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Builders; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Hides rooms from the RoomTracker that are hidden by the map. 8 | /// This is done by temporarily removing the room collider. 9 | /// I hate this, but I can't find a better way to do it. 10 | /// 11 | [HarmonyPatch(typeof(RoomTracker), nameof(RoomTracker.FixedUpdate))] 12 | public static class RoomUIPatch 13 | { 14 | public static void Prefix() 15 | { 16 | if (!LIShipStatus.IsInstance()) 17 | return; 18 | 19 | // Remove room colliders 20 | foreach (var roomData in RoomBuilder.RoomDB) 21 | if (!roomData.isUIVisible && roomData.shipRoom != null) 22 | roomData.shipRoom.roomArea = null; 23 | } 24 | 25 | public static void Postfix() 26 | { 27 | if (!LIShipStatus.IsInstance()) 28 | return; 29 | 30 | // Add room colliders 31 | foreach (var roomData in RoomBuilder.RoomDB) 32 | if (!roomData.isUIVisible && roomData.shipRoom != null) 33 | roomData.shipRoom.roomArea = roomData.collider; 34 | } 35 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/ValueTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Builders; 3 | using LevelImposter.Core; 4 | 5 | namespace LevelImposter.Trigger; 6 | 7 | public class ValueTriggerHandle : ITriggerHandle 8 | { 9 | public void OnTrigger(TriggerSignal signal) 10 | { 11 | if (signal.TriggerID != "setValueTrue" && 12 | signal.TriggerID != "setValueFalse" && 13 | signal.TriggerID != "toggleValue") 14 | return; 15 | 16 | // Get Element Data 17 | var elementData = signal.TargetObject.GetLIData(); 18 | 19 | // Get Value 20 | var valueObj = (BasicBoolValue)ValueBuilder.GetBoolOfID(elementData.ID); 21 | if (valueObj == null) 22 | throw new Exception("Value object is not a BasicBoolValue"); 23 | 24 | // Run Operation 25 | var newValue = signal.TriggerID switch 26 | { 27 | "setValueTrue" => true, 28 | "setValueFalse" => false, 29 | "toggleValue" => !valueObj.GetValue(0), 30 | _ => throw new Exception("Invalid trigger ID") 31 | }; 32 | 33 | // Set Value 34 | valueObj.SetValue(newValue, signal); 35 | } 36 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loaders/GIFLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.AssetLoader; 6 | 7 | public static class GIFLoader 8 | { 9 | /// 10 | /// Loads a GIF image from a stream. 11 | /// 12 | /// Loadable sprite object 13 | /// A fully-loaded GIFFile containing the image data 14 | public static LoadedGIFTexture Load(LoadableTexture loadable) 15 | { 16 | // Get whether to add to GC 17 | var addToGC = loadable.Options?.AddToGC ?? true; 18 | 19 | // Create new file 20 | var gifFile = new GIFFile(loadable.ID); 21 | if (addToGC) 22 | GCHandler.Register(gifFile); 23 | 24 | // Load the GIF file from the stream 25 | using var imgStream = loadable.DataStore.OpenStream(); 26 | gifFile.Load(imgStream, addToGC); 27 | 28 | // Return the GIF file 29 | return new LoadedGIFTexture(gifFile); 30 | } 31 | 32 | public class LoadedGIFTexture(GIFFile gifFile) : LoadedTexture(gifFile.GetFrameTexture(0)) 33 | { 34 | public GIFFile GIFFile => gifFile; 35 | } 36 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/Values/ComparatorValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Builders; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | public class ComparatorValue(Guid? value1ID, Guid? value2ID, ComparatorValue.Operation operation) 7 | : IBoolValue 8 | { 9 | public enum Operation 10 | { 11 | AND, 12 | OR, 13 | XOR, 14 | NOT 15 | } 16 | 17 | public bool GetValue(int depth) 18 | { 19 | // Check for infinite loops 20 | if (depth > IBoolValue.MAX_DEPTH) 21 | throw new Exception("Infinite loop value loop detected. Check your value dependencies."); 22 | 23 | // Get values 24 | var value1 = ValueBuilder.GetBoolOfID(value1ID).GetValue(depth + 1); 25 | 26 | // Only get value2 if it's needed 27 | var value2 = operation != Operation.NOT && ValueBuilder.GetBoolOfID(value2ID).GetValue(depth + 1); 28 | 29 | // Perform operation 30 | return operation switch 31 | { 32 | Operation.AND => value1 && value2, 33 | Operation.OR => value1 || value2, 34 | Operation.XOR => value1 ^ value2, 35 | Operation.NOT => !value1, 36 | _ => false 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Sub/ObjectDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.DB; 7 | 8 | /// 9 | /// Database of Among Us GameObjects 10 | /// 11 | public class ObjectDB(SerializedAssetDB serializedDB) : SubDB(serializedDB) 12 | { 13 | public override void LoadShip(ShipStatus shipStatus, MapType mapType) 14 | { 15 | DB.ObjectDB.ForEach(elem => 16 | { 17 | if (mapType != elem.MapType) 18 | return; 19 | 20 | // Transform 21 | var transform = shipStatus.transform.Find(elem.Path); 22 | if (transform == null) 23 | { 24 | LILogger.Warn($"ObjectDB could not find {elem.ID} in {shipStatus.name}"); 25 | return; 26 | } 27 | 28 | Add(elem.ID, transform.gameObject); 29 | }); 30 | } 31 | 32 | [Serializable] 33 | public class DBElement 34 | { 35 | public string ID { get; set; } 36 | public string Path { get; set; } 37 | public int Map { get; set; } 38 | 39 | [JsonIgnore] public MapType MapType => (MapType)Map; 40 | } 41 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/SabotageOptionsBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.AssetLoader; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | internal class SabotageOptionsBuilder : IElemBuilder 8 | { 9 | public const string SABOTAGE_SOUND_NAME = "sabotageSound"; 10 | 11 | 12 | public SabotageOptionsBuilder() 13 | { 14 | TriggerObject = null; 15 | } 16 | 17 | public static GameObject? TriggerObject { get; private set; } 18 | 19 | public void OnBuild(LIElement elem, GameObject obj) 20 | { 21 | if (elem.type != "util-sabotages") 22 | return; 23 | 24 | // ShipStatus 25 | var shipStatus = LIShipStatus.GetInstance().ShipStatus; 26 | 27 | // Singleton 28 | if (TriggerObject != null) 29 | { 30 | LILogger.Warn("Only 1 util-sabotages object can be placed per map"); 31 | return; 32 | } 33 | 34 | TriggerObject = obj; 35 | 36 | // Sabotage Sound 37 | var sabotageSound = MapUtils.FindSound(elem.properties.sounds, SABOTAGE_SOUND_NAME); 38 | if (sabotageSound != null) 39 | shipStatus.SabotageSound = WAVLoader.Load(sabotageSound) ?? shipStatus.SabotageSound; 40 | } 41 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Ship/LadderPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Hazel; 3 | using LevelImposter.Builders; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Normally, ladders are handled by 9 | /// AirshipStatus. This bypasses that 10 | /// dependency by supplying it's own 11 | /// ladder listings. 12 | /// 13 | [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.HandleRpc))] 14 | public static class LadderPatch 15 | { 16 | public static bool Prefix( 17 | [HarmonyArgument(0)] byte callId, 18 | [HarmonyArgument(1)] MessageReader reader, 19 | PlayerPhysics __instance) 20 | { 21 | if (!LIShipStatus.IsInstance()) 22 | return true; 23 | if (callId != (byte)RpcCalls.ClimbLadder) 24 | return true; 25 | 26 | var ladderId = reader.ReadByte(); 27 | var climbLadderSid = reader.ReadByte(); 28 | var isFound = LadderBuilder.TryGetLadder(ladderId, out var ladder); 29 | 30 | if (isFound) 31 | { 32 | __instance.ClimbLadder(ladder, climbLadderSid); 33 | return false; 34 | } 35 | 36 | LILogger.Warn($"[RPC] Could not find a ladder of id: {ladderId}"); 37 | return true; 38 | } 39 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/OneWayColliderBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | internal class OneWayColliderBuilder : IElemBuilder 7 | { 8 | public void OnBuild(LIElement elem, GameObject obj) 9 | { 10 | if (elem.type != "util-onewaycollider") 11 | return; 12 | 13 | // Room Component 14 | var systemType = RoomBuilder.GetParentOrDefault(elem); 15 | var shipRoom = RoomBuilder.GetShipRoom(systemType); 16 | if (shipRoom == null) 17 | { 18 | LILogger.Warn($"{elem.name} has no room attatched."); 19 | return; 20 | } 21 | 22 | // Iterate through shadow children 23 | for (var i = 0; i < obj.transform.childCount; i++) 24 | { 25 | var child = obj.transform.GetChild(i); 26 | var isShadow = child.gameObject.layer == (int)Layer.Shadow; 27 | 28 | // Add Component to Shadows 29 | if (isShadow) 30 | { 31 | var shadowComponent = child.gameObject.AddComponent(); 32 | shadowComponent.RoomCollider = shipRoom.roomArea; 33 | shadowComponent.IgnoreImpostor = elem.properties.isImposterIgnored ?? false; 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/RandomTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Trigger; 5 | 6 | public class RandomTriggerHandle : ITriggerHandle 7 | { 8 | private int _randomOffset; 9 | 10 | public void OnTrigger(TriggerSignal signal) 11 | { 12 | if (signal.TriggerID != "random") 13 | return; 14 | 15 | // Get source element 16 | var objectData = signal.TargetObject.GetLIData(); 17 | 18 | // Get a random value 19 | // Seed is synced across all clients, so the same value is generated on all clients 20 | var randVal = RandomizerSync.GetRandom(objectData.ID, _randomOffset++); 21 | 22 | // Get the random chance (0 - 1) 23 | var randomChance = 1.0f / (objectData.Properties.triggerCount ?? 2); 24 | 25 | // Get the trigger index based on the random value (0 - triggerCount) 26 | var triggerIndex = Mathf.FloorToInt(randVal / randomChance); 27 | 28 | // Get the trigger ID 29 | var targetID = "onRandom " + (triggerIndex + 1); 30 | 31 | // Create & Fire Trigger 32 | TriggerSignal newSignal = new( 33 | signal.TargetObject, 34 | targetID, 35 | signal 36 | ); 37 | TriggerSystem.GetInstance().FireTrigger(newSignal); 38 | } 39 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Minimap/MinimapSpriteBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | public class MinimapSpriteBuilder : IElemBuilder 7 | { 8 | public MinimapSpriteBuilder() 9 | { 10 | SabCount = 0; 11 | } 12 | 13 | public static int SabCount { get; private set; } 14 | 15 | public void OnBuild(LIElement elem, GameObject obj) 16 | { 17 | if (elem.type != "util-minimapsprite") 18 | return; 19 | 20 | // ShipStatus 21 | var shipStatus = LIShipStatus.GetShip(); 22 | 23 | // Minimap 24 | var mapBehaviour = MinimapBuilder.GetMinimap(); 25 | var infectedOverlay = mapBehaviour.infectedOverlay; 26 | var taskOverlay = mapBehaviour.taskOverlay; 27 | var imposterOnly = elem.properties.imposterOnly == true; 28 | var parentTransform = imposterOnly ? infectedOverlay.transform : taskOverlay.transform; 29 | if (imposterOnly) 30 | SabCount++; 31 | 32 | // GameObject 33 | var mapScale = shipStatus.MapScale; 34 | obj.layer = (int)Layer.UI; 35 | obj.transform.SetParent(parentTransform, false); 36 | obj.transform.localPosition = new Vector3( 37 | elem.x / mapScale, 38 | elem.y / mapScale, 39 | elem.z 40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/QuickChatPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using AmongUs.QuickChat; 3 | using HarmonyLib; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Renames task names and other strings stored as SystemTypes. 9 | /// 10 | [HarmonyPatch(typeof(QuickChatContext), nameof(QuickChatContext.GetCurrentMapID))] 11 | public static class QuickChatPatch 12 | { 13 | public static bool Prefix(ref MapNames __result) 14 | { 15 | if (!LIShipStatus.IsInstance()) 16 | return true; 17 | 18 | __result = (MapNames)MapType.LevelImposter; 19 | return false; 20 | } 21 | } 22 | 23 | [HarmonyPatch(typeof(QuickChatContext), nameof(QuickChatContext.UpdateWithCurrentLobby))] 24 | public static class QuickChatMapPatch 25 | { 26 | public static void Postfix(QuickChatContext __instance) 27 | { 28 | __instance.locations = __instance.locations.AddItem(LIConstants.MAP_STRING_NAME).ToArray(); 29 | } 30 | } 31 | 32 | [HarmonyPatch(typeof(QuickChatMapRules), nameof(QuickChatMapRules.Evaluate))] 33 | public static class QuickChatRulesPatch 34 | { 35 | public static bool Prefix(QuickChatMapRules __instance, ref bool __result) 36 | { 37 | if (!LIShipStatus.IsInstance()) 38 | return true; 39 | 40 | __result = __instance.maps.Contains((MapNames)MapType.Skeld); 41 | return false; 42 | } 43 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/AudioLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using LevelImposter.Shop; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.AssetLoader; 7 | 8 | public class AudioLoader : AsyncQueue 9 | { 10 | private AudioLoader() 11 | { 12 | } 13 | 14 | public static AudioLoader Instance { get; } = new(); 15 | 16 | /// 17 | /// Loads an AudioClip asynchronously from the given asset ID. 18 | /// 19 | /// Asset ID of the audio clip to load 20 | /// Callback invoked when the audio clip is loaded 21 | public static void LoadAsync(Guid? assetID, Action onLoad) 22 | { 23 | if (assetID == null) 24 | return; 25 | 26 | // Get Data Store from AssetDB 27 | var soundDataStore = MapLoader.CurrentMap?.mapAssetDB?.Get(assetID); 28 | if (soundDataStore == null) 29 | return; 30 | 31 | // Create LoadableAudio 32 | var loadableAudio = new LoadableAudio(assetID?.ToString() ?? "", soundDataStore); 33 | 34 | // Enqueue Loadable 35 | Instance.AddToQueue(loadableAudio, onLoad); 36 | } 37 | 38 | protected override AudioClip Load(LoadableAudio loadable) 39 | { 40 | return WAVLoader.Load(loadable.DataStore, loadable.ID); 41 | } 42 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LIShakeArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Object that shakes the screen when players enter it's range 8 | /// 9 | public class LIShakeArea(IntPtr intPtr) : PlayerArea(intPtr) 10 | { 11 | private float _shakeAmount = 0.03f; 12 | private float _shakePeriod = 400.0f; 13 | 14 | public void OnEnable() 15 | { 16 | SetShakeEnabled(IsLocalPlayerInside); 17 | } 18 | 19 | public void OnDisable() 20 | { 21 | SetShakeEnabled(false); 22 | } 23 | 24 | public void SetParameters(float shakeAmount, float shakePeriod) 25 | { 26 | _shakeAmount = shakeAmount; 27 | _shakePeriod = shakePeriod; 28 | } 29 | 30 | private void SetShakeEnabled(bool enabled) 31 | { 32 | var camera = Camera.main.GetComponent(); 33 | if (camera != null) 34 | { 35 | camera.shakeAmount = enabled ? _shakeAmount : 0.0f; 36 | camera.shakePeriod = enabled ? _shakePeriod : 0.0f; 37 | } 38 | } 39 | 40 | public override void OnPlayerEnter(PlayerControl player) 41 | { 42 | if (player.AmOwner) 43 | SetShakeEnabled(true); 44 | } 45 | 46 | public override void OnPlayerExit(PlayerControl player) 47 | { 48 | if (player.AmOwner) 49 | SetShakeEnabled(false); 50 | } 51 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Triggers/SabEndPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using HarmonyLib; 3 | using LevelImposter.Builders; 4 | using LevelImposter.Trigger; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | /// 9 | /// Calls the trigger when the sabotage is finished. 10 | /// 11 | [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RemoveTask))] 12 | public static class SabEndPatch 13 | { 14 | private static readonly Dictionary _taskTriggerPairs = new() 15 | { 16 | { TaskTypes.FixLights, "onLightsEnd" }, 17 | { TaskTypes.ResetReactor, "onReactorEnd" }, 18 | { TaskTypes.RestoreOxy, "onOxygenEnd" }, 19 | { TaskTypes.FixComms, "onCommsEnd" }, 20 | { TaskTypes.MushroomMixupSabotage, "onMixupEnd" } 21 | }; 22 | 23 | public static void Postfix([HarmonyArgument(0)] PlayerTask task) 24 | { 25 | if (!LIShipStatus.IsInstance()) 26 | return; 27 | if (!_taskTriggerPairs.ContainsKey(task.TaskType)) 28 | return; 29 | 30 | // Fire Trigger 31 | var triggerName = _taskTriggerPairs[task.TaskType]; 32 | if (SabotageOptionsBuilder.TriggerObject != null) 33 | { 34 | TriggerSignal signal = new(SabotageOptionsBuilder.TriggerObject, triggerName, 35 | PlayerControl.LocalPlayer); 36 | TriggerSystem.GetInstance().FireTrigger(signal); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/TriggerPropogationHandle.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | 3 | namespace LevelImposter.Trigger; 4 | 5 | public class TriggerPropogationHandle : ITriggerHandle 6 | { 7 | public void OnTrigger(TriggerSignal signal) 8 | { 9 | // Get the object data 10 | var objectData = signal.TargetObject?.GetLIData(); 11 | 12 | // Check if the object has triggers 13 | var triggers = objectData?.Properties?.triggers; 14 | if (triggers == null) 15 | return; 16 | 17 | // Find cooresponding trigger 18 | foreach (var trigger in triggers) 19 | { 20 | // Check if the trigger has the triggerID 21 | if (trigger.id != signal.TriggerID) 22 | continue; 23 | 24 | // Check if the trigger should propogate 25 | if (trigger.elemID == null || trigger.triggerID == null) 26 | continue; 27 | 28 | // Get Object 29 | var targetObject = TriggerSystem.FindObject(trigger.elemID); 30 | if (targetObject == null) 31 | continue; 32 | 33 | // Create & Run Trigger 34 | TriggerSignal newSignal = new( 35 | targetObject, 36 | trigger.triggerID, 37 | signal 38 | ); 39 | TriggerSystem.GetInstance().FireTrigger(newSignal); 40 | return; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loadables/LoadableTexture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 3 | using Il2CppSystem.IO; 4 | using LevelImposter.Core; 5 | using UnityEngine; 6 | 7 | namespace LevelImposter.AssetLoader; 8 | 9 | public readonly struct LoadableTexture(string _id, IDataStore _dataStore) : ICachable 10 | { 11 | public string ID => _id; 12 | public IDataStore DataStore => _dataStore; 13 | public readonly TextureOptions Options { get; } = new(); 14 | 15 | public class TextureOptions 16 | { 17 | /// If true, the texture will use pixel art filtering (point filtering) 18 | public bool PixelArt { get; set; } = false; 19 | 20 | /// If true (default), the texture will be disposed automatically after the map is unloaded. 21 | /// If false, you must manage the texture's lifecycle manually. 22 | public bool AddToGC { get; set; } = true; 23 | } 24 | 25 | /// 26 | /// Creates a LoadableTexture from a byte array. 27 | /// 28 | /// Unique identifier to be used in caching. 29 | /// Byte array containing the image data. 30 | /// A LoadableTexture instance. 31 | public static LoadableTexture FromByteArray(string id, Il2CppArrayBase data) 32 | { 33 | var stream = new MemoryStore(data); 34 | return new LoadableTexture(id, stream); 35 | } 36 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loadables/LoadableSprite.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.AssetLoader; 5 | 6 | public readonly struct LoadableSprite(string _id, LoadableTexture _tex) : ICachable 7 | { 8 | public string ID => _id; 9 | public LoadableTexture Texture => _tex; 10 | public readonly SpriteOptions Options { get; } = new(); 11 | 12 | public class SpriteOptions 13 | { 14 | /// If set, defines the pivot point of the sprite. Otherwise, the center (0.5, 0.5) is used. 15 | public Vector2? Pivot { get; set; } 16 | 17 | /// If set, defines the portion of the texture to use for the sprite. Otherwise, the full texture is used. 18 | public Rect? Frame { get; set; } 19 | 20 | /// If true (default), the sprite will be disposed automatically after the map is unloaded. 21 | /// If false, you must manage the sprite's lifecycle manually. 22 | public bool AddToGC { get; set; } = true; 23 | } 24 | 25 | /// 26 | /// Creates a LoadableSprite from a LoadableTexture. 27 | /// Copies the ID and uses the provided texture. 28 | /// 29 | /// LoadableTexture to create the sprite from. 30 | /// A LoadableSprite instance. 31 | public static LoadableSprite FromLoadableTexture(LoadableTexture texture) 32 | { 33 | return new LoadableSprite(texture.ID, texture); 34 | } 35 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/LadderPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Sets Ladder Cooldown to editable value 7 | /// 8 | [HarmonyPatch(typeof(Ladder), nameof(Ladder.SetDestinationCooldown))] 9 | public static class DestinationLadderCooldownPatch 10 | { 11 | public static void Postfix(Ladder __instance) 12 | { 13 | if (__instance is not EditableLadderConsole editableLadder) 14 | return; 15 | 16 | __instance.Destination.CoolDown = editableLadder.MaxCoolDown; 17 | } 18 | } 19 | 20 | [HarmonyPatch(typeof(Ladder), nameof(Ladder.Use))] 21 | public static class LadderCooldownPatch 22 | { 23 | public static void Postfix(Ladder __instance) 24 | { 25 | if (__instance is not EditableLadderConsole editableLadder) 26 | return; 27 | 28 | if (__instance.CoolDown == __instance.MaxCoolDown) 29 | __instance.CoolDown = editableLadder.MaxCoolDown; 30 | } 31 | } 32 | 33 | [HarmonyPatch(typeof(Ladder))] 34 | [HarmonyPatch(nameof(Ladder.PercentCool), MethodType.Getter)] 35 | public static class LadderPercentCooldownPatch 36 | { 37 | public static bool Prefix(Ladder __instance, out float __result) 38 | { 39 | __result = 0; 40 | if (__instance is not EditableLadderConsole editableLadder) 41 | return true; 42 | 43 | __result = __instance.CoolDown / editableLadder.MaxCoolDown; 44 | return false; 45 | } 46 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/MinigamePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Loads all minigame sprites when a minigame is started. 8 | /// 9 | [HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))] 10 | public static class MinigamePatch 11 | { 12 | // Set by LevelImposter.ConsolePatch 13 | public static GameObject? LastConsole; 14 | 15 | public static void Postfix(Minigame __instance) 16 | { 17 | if (!LIShipStatus.IsInstance()) 18 | return; 19 | 20 | // Get Component 21 | var currentConsole = __instance.Console?.gameObject ?? LastConsole; 22 | var minigameSprites = currentConsole?.GetComponent(); 23 | 24 | // Handle Doors 25 | if (minigameSprites == null) 26 | { 27 | var doorConsole = currentConsole?.GetComponent(); 28 | minigameSprites = doorConsole?.MyDoor?.GetComponent(); 29 | } 30 | 31 | // Load Minigame 32 | minigameSprites?.LoadMinigame(__instance); 33 | } 34 | } 35 | 36 | 37 | 38 | /// 39 | /// When a minigame is closed, clear the last console. 40 | /// 41 | // [HarmonyPatch(typeof(Minigame), nameof(Minigame.Close), new Type[0])] 42 | // public static class MinigameClosePatch 43 | // { 44 | // public static void Postfix() 45 | // { 46 | // MinigamePatch.LastConsole = null; 47 | // } 48 | // } -------------------------------------------------------------------------------- /LevelImposter/Builders/Other/DecBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LevelImposter.Core; 3 | using LevelImposter.DB; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Builders; 7 | 8 | public class DecBuilder : IElemBuilder 9 | { 10 | private static readonly List _fixTypes = new() 11 | { 12 | "room-dropship" 13 | }; 14 | 15 | public void OnBuild(LIElement elem, GameObject obj) 16 | { 17 | var isDecoration = elem.type.StartsWith("dec-"); 18 | var isRoom = elem.type.StartsWith("room-"); 19 | if (!(isDecoration || isRoom)) 20 | return; 21 | 22 | // Prefab 23 | var prefab = AssetDB.GetObject(elem.type); 24 | if (prefab == null) 25 | return; 26 | 27 | // Sprite 28 | var spriteRenderer = MapUtils.CloneSprite(obj, prefab); 29 | 30 | // Fixes Pivot Offset Bug 31 | if (_fixTypes.Contains(elem.type)) 32 | { 33 | var sprite = Sprite.Create( 34 | spriteRenderer.sprite.texture, 35 | spriteRenderer.sprite.rect, 36 | new Vector2(0.5f, 0.5f), 37 | 100, 38 | 0, 39 | SpriteMeshType.FullRect 40 | ); 41 | spriteRenderer.sprite = sprite; 42 | sprite.hideFlags = HideFlags.HideAndDontSave; 43 | GCHandler.Register(sprite); 44 | } 45 | 46 | if (isRoom) 47 | obj.layer = (int)Layer.Ship; 48 | } 49 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/LobbySyncPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Core; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | /* 7 | * Synchronizes a random seed 8 | * value to all connected clients 9 | */ 10 | [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.Start))] 11 | public static class ClientJoinSyncPatch 12 | { 13 | public static void Postfix(PlayerControl __instance) 14 | { 15 | if (!AmongUsClient.Instance.AmHost) 16 | return; 17 | 18 | // Sync Random Seed 19 | RandomizerSync.SyncRandomSeed(); 20 | 21 | // This is a new Lobby 22 | if (__instance.AmOwner) 23 | { 24 | var wasFallback = MapLoader.IsFallback; 25 | var isNoMap = MapLoader.CurrentMap == null; 26 | 27 | // If the map was a fallback or no map is currently loaded 28 | if (wasFallback || isNoMap) 29 | { 30 | // Choose a new random map 31 | MapSync.RegenerateFallbackID(); 32 | return; 33 | } 34 | } 35 | 36 | // Sync the current map ID 37 | // TODO: Remember last map ID 38 | MapSync.SyncMapID(); 39 | } 40 | } 41 | 42 | // [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CreatePlayer))] 43 | // public static class ClientJoinSyncPatch 44 | // { 45 | // public static void Postfix() 46 | // { 47 | // RandomizerSync.SyncRandomSeed(); 48 | // MapSync.SyncMapID(); 49 | // } 50 | // } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/DummyVotePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | /// 6 | /// Forces dummies to vote with the local player in custom freeplay maps. 7 | /// 8 | [HarmonyPatch(typeof(DummyBehaviour), nameof(DummyBehaviour.Update))] 9 | public static class DummyVotePatch 10 | { 11 | public static bool Prefix(DummyBehaviour __instance) 12 | { 13 | // Only execute in custom freeplay maps 14 | if (!LIShipStatus.IsInstance()) 15 | return true; 16 | if (!GameState.IsInFreeplay) 17 | return true; 18 | 19 | // Check if the dummy is dead 20 | var playerData = __instance.myPlayer.Data; 21 | if (playerData == null || playerData.IsDead) 22 | return true; 23 | 24 | // Check if we are in a meeting 25 | if (!GameState.IsInMeeting) 26 | { 27 | __instance.voted = false; 28 | } 29 | // Check if the dummy has already voted 30 | else if (!__instance.voted) 31 | { 32 | // Check if the local player has voted 33 | var localPlayerState = MeetingHud.Instance.playerStates[0]; 34 | if (!localPlayerState.DidVote) 35 | return false; 36 | 37 | // Vote for the same player as the local player 38 | MeetingHud.Instance.CmdCastVote(playerData.PlayerId, localPlayerState.VotedFor); 39 | __instance.voted = true; 40 | } 41 | 42 | return false; 43 | } 44 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Builders/MainMenuBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using TMPro; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace LevelImposter.Shop; 8 | 9 | public static class MainMenuBuilder 10 | { 11 | private const string BUTTON_PATH = 12 | "MainMenuManager/MainUI/AspectScaler/LeftPanel/Main Buttons/Inventory Button"; 13 | 14 | private const string TEXT_PATH = "FontPlacer/Text_TMP"; 15 | private static Sprite? _menuIcon; 16 | 17 | private static readonly string[] ICON_PATHS = 18 | { 19 | "Highlight/Icon", 20 | "Inactive/Icon" 21 | }; 22 | 23 | public static void Build() 24 | { 25 | // Button 26 | var button = GameObject.Find(BUTTON_PATH); 27 | 28 | // Text 29 | var text = button.transform.Find(TEXT_PATH); 30 | text.GetComponent().text = "Maps"; 31 | Object.Destroy(text.GetComponent()); 32 | 33 | // Sprites 34 | foreach (var path in ICON_PATHS) 35 | { 36 | var icon = button.transform.Find(path); 37 | icon.GetComponent().sprite = GetIconSprite(); 38 | } 39 | } 40 | 41 | private static Sprite GetIconSprite() 42 | { 43 | if (_menuIcon == null) 44 | _menuIcon = MapUtils.LoadSpriteResource("MapIcon.png"); 45 | if (_menuIcon == null) 46 | throw new Exception("The \"MapIcon.png\" resource was not found in assembly"); 47 | return _menuIcon; 48 | } 49 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LIFloat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Object that oscillates up and down 8 | /// 9 | public class LIFloat(IntPtr intPtr) : MonoBehaviour(intPtr) 10 | { 11 | private float _height = 0.2f; 12 | private Vector3 _lastPosition; 13 | private float _speed = 2.0f; 14 | private float _yScale = 1; 15 | 16 | public void Awake() 17 | { 18 | // Get LI data 19 | var objectData = gameObject.GetLIData(); 20 | if (objectData == null) 21 | throw new Exception("LIFloat is missing LI data"); 22 | 23 | _height = objectData.Element.properties.floatingHeight ?? _height; 24 | _speed = objectData.Element.properties.floatingSpeed ?? _speed; 25 | _yScale = objectData.Element.yScale; 26 | } 27 | 28 | public void Update() 29 | { 30 | // Oscillate 31 | var time = Time.time; 32 | var value = (Mathf.Sin(time * _speed) + 1) * _yScale * _height / 2; 33 | var rotation = -transform.rotation.eulerAngles.z * Mathf.Deg2Rad; 34 | 35 | // Get new position 36 | var newPosition = new Vector3( 37 | Mathf.Sin(rotation) * value, 38 | Mathf.Cos(rotation) * value, 39 | 0 40 | ); 41 | 42 | // Move object by delta 43 | var delta = newPosition - _lastPosition; 44 | transform.position += delta; 45 | 46 | // Update last position 47 | _lastPosition = newPosition; 48 | } 49 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Utils/LateInitPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Shop; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Runs a variety of initialization tasks after the game has started. 9 | /// 10 | [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Awake))] 11 | public static class LateInitPatch 12 | { 13 | private static bool _hasInitialized; 14 | 15 | public static void Postfix() 16 | { 17 | if (_hasInitialized) 18 | return; 19 | 20 | // Add Mod Stamp (In case Reactor is missing) 21 | DestroyableSingleton.Instance.ShowModStamp(); 22 | 23 | // Increase player's max movement range 24 | // from (-50 - 50) to (-500 - 500) 25 | NetHelpers.XRange = new FloatRange(-500f, 500f); 26 | NetHelpers.YRange = new FloatRange(-500f, 500f); 27 | 28 | // Increase SystemTypes range to fix 29 | // ShipStatus.Deteriorate and ShipStatus.Serialize 30 | var allTypes = new SystemTypes[byte.MaxValue]; 31 | for (var i = 0; i < byte.MaxValue; i++) 32 | allTypes[i] = (SystemTypes)i; 33 | SystemTypeHelpers.AllTypes = allTypes; 34 | 35 | // Add Global Components that utilize 36 | // the Unity runtime in some way 37 | var apiParent = new GameObject("LevelImposter"); 38 | apiParent.AddComponent(); 39 | apiParent.AddComponent(); 40 | Object.DontDestroyOnLoad(apiParent); 41 | 42 | _hasInitialized = true; 43 | } 44 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/TeleBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Builders; 7 | 8 | internal class TeleBuilder : IElemBuilder 9 | { 10 | private readonly Dictionary _teleList = new(); 11 | 12 | public void OnBuild(LIElement elem, GameObject obj) 13 | { 14 | if (elem.type != "util-tele") 15 | return; 16 | 17 | // Colliders 18 | Collider2D[] colliders = obj.GetComponentsInChildren(); 19 | foreach (var collider in colliders) 20 | collider.isTrigger = true; 21 | if (elem.properties.isGhostEnabled ?? true) 22 | obj.layer = (int)Layer.Default; 23 | 24 | // Teleporter 25 | var teleporter = obj.AddComponent(); 26 | _teleList[elem.id] = teleporter; 27 | } 28 | 29 | public void OnPostBuild(LIElement elem, GameObject obj) 30 | { 31 | if (elem.type != "util-tele") 32 | return; 33 | 34 | // Get Target Teleporter 35 | var targetID = elem.properties.teleporter; 36 | if (targetID == null) 37 | return; 38 | var targetTeleporter = _teleList.GetValueOrDefault((Guid)targetID); 39 | 40 | // Get Teleporter 41 | var teleporter = _teleList.GetValueOrDefault(elem.id); 42 | if (teleporter == null || targetTeleporter == null) 43 | return; 44 | 45 | // Set Target Teleporter 46 | teleporter.SetTargetTeleporter(targetTeleporter); 47 | } 48 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/IElemBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | /// 7 | /// Constructs a specific subset of map elements or element features. 8 | /// Stored in the build stack located in BuildRouter._buildStack. 9 | /// 10 | public interface IElemBuilder 11 | { 12 | /// 13 | /// Parses and builds a GameObject based on LIElement data. 14 | /// 15 | /// Element to be parsed and built 16 | /// GameObject to append data and components to 17 | public void OnPreBuild(LIElement elem, GameObject obj) 18 | { 19 | } 20 | 21 | /// 22 | /// Parses and builds a GameObject based on LIElement data. 23 | /// 24 | /// Element to be parsed and built 25 | /// GameObject to append data and components to 26 | public void OnBuild(LIElement elem, GameObject obj) 27 | { 28 | } 29 | 30 | /// 31 | /// Final clean-up after an element has been built. 32 | /// 33 | /// Element to be parsed and built 34 | /// GameObject to append data and components to 35 | public void OnPostBuild(LIElement elem, GameObject obj) 36 | { 37 | } 38 | 39 | /// 40 | /// Final clean-up after all elements in a map have been built. 41 | /// 42 | public void OnCleanup() 43 | { 44 | } 45 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Fixes/DeconControlPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Allows decon controls to bypass physics collisions. 8 | /// 9 | [HarmonyPatch(typeof(DeconControl), nameof(DeconControl.CanUse))] 10 | public class DeconControlPatch 11 | { 12 | public static void Postfix( 13 | DeconControl __instance, 14 | [HarmonyArgument(0)] NetworkedPlayerInfo playerInfo, 15 | [HarmonyArgument(1)] ref bool canUse, 16 | [HarmonyArgument(2)] ref bool couldUse, 17 | ref float __result) 18 | { 19 | // Custom Maps Only 20 | if (!LIShipStatus.IsInstance()) 21 | return; 22 | // Ignore if the system is not idle 23 | if (__instance.System.CurState != DeconSystem.States.Idle) 24 | return; 25 | 26 | // Check if the player can use the decon 27 | couldUse = playerInfo.Object.CanMove && !playerInfo.IsDead; 28 | canUse = couldUse && __instance.cooldown == 0f; 29 | __result = float.MaxValue; 30 | 31 | // Check if the player is close enough to use the decon 32 | if (canUse) 33 | { 34 | // Get Adjusted Position 35 | var truePosition = playerInfo.Object.GetTruePosition(); 36 | var position = __instance.transform.position; 37 | position.y -= 0.1f; // <-- Adjust for player height 38 | 39 | // Compare Distance 40 | __result = Vector2.Distance(truePosition, position); 41 | canUse &= __result <= __instance.UsableDistance; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Sub/SubDB.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LevelImposter.Core; 3 | 4 | namespace LevelImposter.DB; 5 | 6 | /// 7 | /// A miniature DB designed to search and 8 | /// contain various types of objects within Among Us 9 | /// 10 | /// Type of object to contain 11 | public abstract class SubDB(SerializedAssetDB serializedDB) 12 | { 13 | private readonly Dictionary _data = new(); 14 | 15 | protected SerializedAssetDB DB { get; } = serializedDB; 16 | 17 | /// 18 | /// Loads any extra assets into the DB 19 | /// 20 | public virtual void Load() 21 | { 22 | } 23 | 24 | /// 25 | /// Loads each ship into the DB 26 | /// 27 | /// ShipStatus to load 28 | /// MapType of ShipStatus 29 | public virtual void LoadShip(ShipStatus shipStatus, MapType mapType) 30 | { 31 | } 32 | 33 | /// 34 | /// Gets an object from the DB 35 | /// 36 | /// Type ID of the object 37 | /// Object or null if not found 38 | public T? Get(string id) 39 | { 40 | _data.TryGetValue(id, out var result); 41 | return result; 42 | } 43 | 44 | /// 45 | /// Adds an object to the DB 46 | /// 47 | /// ID of the object 48 | /// Object to add 49 | protected void Add(string id, T obj) 50 | { 51 | _data.Add(id, obj); 52 | } 53 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/MapObjectDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Database of all map objects 9 | /// 10 | public class MapObjectDB 11 | { 12 | private readonly Dictionary _mapObjectDB = new(); 13 | 14 | /// 15 | /// Adds an object to the database 16 | /// 17 | /// Element ID 18 | /// Cooresponding GameObject 19 | public void AddObject(Guid guid, GameObject obj) 20 | { 21 | _mapObjectDB[guid] = obj; 22 | } 23 | 24 | /// 25 | /// Gets an object from the database 26 | /// 27 | /// Element ID 28 | /// The cooresponding GameObject or null if it wasn't found 29 | public GameObject? GetObject(Guid guid) 30 | { 31 | return _mapObjectDB.GetValueOrDefault(guid); 32 | } 33 | 34 | /// 35 | /// Gets an object from the database 36 | /// 37 | /// Element ID 38 | /// The cooresponding GameObject or null if it wasn't found 39 | public static GameObject? Get(Guid guid) 40 | { 41 | // Get Ship Status 42 | var shipStatus = LIShipStatus.GetInstanceOrNull(); 43 | if (shipStatus == null) 44 | { 45 | LILogger.Warn("Ship Status is missing"); 46 | return null; 47 | } 48 | 49 | // Get object 50 | return shipStatus.MapObjectDB.GetObject(guid); 51 | } 52 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | public static class StreamExtensions 8 | { 9 | /// 10 | /// Copies data from the given stream into memory as a data block. 11 | /// This implementation uses a to minimize allocations. 12 | /// 13 | /// Note: This requires the Stream implementation to support Length property. 14 | /// 15 | /// The stream to read from. 16 | /// The created data block. 17 | /// If the stream is too large or reading fails. 18 | public static IMemoryBlock ToDataBlock(this Stream stream) 19 | { 20 | // Check for int overflow 21 | if (stream.Length > int.MaxValue) 22 | throw new InvalidOperationException("Stream is too large to fit in a PoolableDataBlock."); 23 | 24 | // Create a new PoolableDataBlock 25 | var block = new PoolableMemoryBlock((int)stream.Length); 26 | var data = block.Get(); 27 | 28 | // Read the stream into the data block 29 | var totalRead = 0; 30 | while (totalRead < data.Length) 31 | { 32 | var bytesRead = stream.Read(data, totalRead, data.Length - totalRead); 33 | if (bytesRead == 0) 34 | break; 35 | totalRead += bytesRead; 36 | } 37 | 38 | // Note: It's typical for streams to not read the full length requested and will not throw. 39 | return block; 40 | } 41 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/GameState.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Shop; 2 | using UnityEngine.SceneManagement; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | public static class GameState 7 | { 8 | // Map State 9 | public static MapType SelectedMapType => IsInFreeplay 10 | ? (MapType)AmongUsClient.Instance.TutorialMapId 11 | : (MapType)GameOptionsManager.Instance.CurrentGameOptions.MapId; 12 | 13 | public static bool IsCustomMapSelected => SelectedMapType == MapType.LevelImposter; 14 | public static bool IsCustomMapLoaded => MapLoader.CurrentMap != null; 15 | public static bool IsInCustomMap => LIShipStatus.GetInstanceOrNull() != null; 16 | public static bool IsFallbackMap => MapLoader.IsFallback; 17 | 18 | public static string MapName => MapLoader.CurrentMap?.name ?? LIConstants.MAP_NAME; 19 | 20 | // Scenes 21 | public static bool IsInFreeplay => AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay; 22 | public static bool IsInLobby => LobbyBehaviour.Instance != null; 23 | public static bool IsInMainMenu => SceneManager.GetActiveScene().name == "MainMenu"; 24 | public static bool IsInShop => ShopManager.Instance != null; 25 | public static bool IsInMeeting => MeetingHud.Instance != null; 26 | public static bool IsPlayerLoaded => PlayerControl.LocalPlayer != null; 27 | 28 | // Network 29 | public static bool IsHost => AmongUsClient.Instance?.AmHost ?? false; 30 | 31 | // Player State 32 | public static bool IsLocalPlayerImpostor => PlayerControl.LocalPlayer?.Data?.Role.TeamType == RoleTeamTypes.Impostor; 33 | public static bool IsLocalPlayerDead => PlayerControl.LocalPlayer?.Data?.IsDead ?? true; 34 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/ModCompatibility.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Unity.IL2CPP; 2 | 3 | namespace LevelImposter.Core; 4 | 5 | public static class ModCompatibility 6 | { 7 | public const string REACTOR_ID = "gg.reactor.api"; 8 | public const string TOR_GUID = "me.eisbison.theotherroles"; 9 | public const string TOU_GUID = "com.slushiegoose.townofus"; 10 | public const string REW_GUID = "me.alchlcdvl.reworked"; 11 | public const string SUBMERGED_GUID = "Submerged"; 12 | 13 | public static bool IsTOREnabled { get; private set; } 14 | 15 | public static bool IsTOUEnabled { get; private set; } 16 | 17 | public static bool IsSubmergedEnabled { get; private set; } 18 | 19 | public static bool IsReworkedEnabled { get; private set; } 20 | 21 | public static void Init() 22 | { 23 | IsTOREnabled = IsPlugin(TOR_GUID); 24 | IsTOUEnabled = IsPlugin(TOU_GUID); 25 | IsSubmergedEnabled = IsPlugin(SUBMERGED_GUID); 26 | IsReworkedEnabled = IsPlugin(REW_GUID); 27 | 28 | if (IsTOREnabled) 29 | LILogger.Info("LevelImposter detected TOR installed, compatibility enabled"); 30 | if (IsTOUEnabled) 31 | LILogger.Info("LevelImposter detected TOU installed, compatibility enabled"); 32 | if (IsReworkedEnabled) 33 | LILogger.Info("LevelImposter detected Reworked installed, compatibility enabled"); 34 | if (IsSubmergedEnabled) 35 | LILogger.Info("LevelImposter detected Submerged installed, currently unsupported"); 36 | } 37 | 38 | private static bool IsPlugin(string guid) 39 | { 40 | return IL2CPPChainloader.Instance.Plugins.TryGetValue(guid, out _); 41 | } 42 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Builders/LobbyConsoleBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace LevelImposter.Shop; 7 | 8 | public static class LobbyConsoleBuilder 9 | { 10 | private static Sprite? _consoleSprite; 11 | 12 | public static void Build() 13 | { 14 | // Object 15 | var lobbyTransform = LobbyBehaviour.Instance.transform; 16 | var consolePrefab = lobbyTransform.FindChild("panel_Wardrobe").gameObject; 17 | var liConsoleObj = Object.Instantiate(consolePrefab, lobbyTransform); 18 | liConsoleObj.name = "panel_LevelImposter"; 19 | liConsoleObj.transform.localPosition = new Vector3(-1.41f, 1.84f, -9.998f); 20 | 21 | // Sprite 22 | var liRenderer = liConsoleObj.GetComponent(); 23 | liRenderer.sprite = GetSprite(); 24 | 25 | // Console 26 | var consoleObj = liConsoleObj.transform.GetChild(0).gameObject; 27 | Object.Destroy(consoleObj.GetComponent()); 28 | var liConsole = consoleObj.AddComponent(); 29 | liConsole.SetRenderer(liRenderer); 30 | 31 | // Collider 32 | var liCollider = liConsoleObj.GetComponentInChildren(); 33 | liCollider.size = new Vector2(0.01f, 0.01f); 34 | } 35 | 36 | private static Sprite GetSprite() 37 | { 38 | if (_consoleSprite == null) 39 | _consoleSprite = MapUtils.LoadSpriteResource("LobbyConsole.png"); 40 | if (_consoleSprite == null) 41 | throw new Exception("The \"LobbyConsole.png\" resource was not found in assembly"); 42 | return _consoleSprite; 43 | } 44 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LITriggerArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Trigger; 3 | 4 | namespace LevelImposter.Core; 5 | 6 | /// 7 | /// Object that fires a trigger when the player enters/exits it's range 8 | /// 9 | public class LITriggerArea(IntPtr intPtr) : PlayerArea(intPtr) 10 | { 11 | private const string ENTER_TRIGGER_ID = "onEnter"; 12 | private const string EXIT_TRIGGER_ID = "onExit"; 13 | 14 | private bool _isClientSide; 15 | 16 | /// 17 | /// Sets whether or not the Trigger Area is client sided 18 | /// 19 | /// TRUE if the trigger is client sided 20 | public void SetClientSide(bool isClientSide) 21 | { 22 | _isClientSide = isClientSide; 23 | } 24 | 25 | public override void OnPlayerEnter(PlayerControl player) 26 | { 27 | var triggerServerSided = CurrentPlayersIDs?.Count <= 1 && !_isClientSide; 28 | var triggerClientSided = player.AmOwner && _isClientSide; 29 | if (triggerClientSided || triggerServerSided) 30 | { 31 | TriggerSignal signal = new(gameObject, ENTER_TRIGGER_ID, player); 32 | TriggerSystem.GetInstance().FireTrigger(signal); 33 | } 34 | } 35 | 36 | public override void OnPlayerExit(PlayerControl player) 37 | { 38 | var triggerServerSided = CurrentPlayersIDs?.Count <= 0 && !_isClientSide; 39 | var triggerClientSided = player.AmOwner && _isClientSide; 40 | if (triggerClientSided || triggerServerSided) 41 | { 42 | TriggerSignal signal = new(gameObject, EXIT_TRIGGER_ID, player); 43 | TriggerSystem.GetInstance().FireTrigger(signal); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /LevelImposter/LevelImposter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | LevelImposter 4 | 0.21.2 5 | Custom Among Us Mapping Studio 6 | DigiWorm 7 | 8 | net6.0 9 | latest 10 | embedded 11 | enable 12 | 13 | 14 | 15 | C:\Program Files (x86)\Steam\steamapps\common\Among Us 16 | 2025.11.18 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/EjectHandBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LevelImposter.Core; 4 | using LevelImposter.DB; 5 | using UnityEngine; 6 | 7 | namespace LevelImposter.Builders; 8 | 9 | public class EjectHandBuilder : IElemBuilder 10 | { 11 | public EjectHandBuilder() 12 | { 13 | AllHands.Clear(); 14 | } 15 | 16 | public static List AllHands { get; } = new(); 17 | 18 | public void OnBuild(LIElement elem, GameObject obj) 19 | { 20 | if (elem.type != "util-ejecthand" && 21 | elem.type != "util-ejectthumb") 22 | return; 23 | 24 | // Get Eject Controller Prefab 25 | var polusPrefab = AssetDB.GetObject("ss-polus"); 26 | var polusShipStatus = polusPrefab?.GetComponent(); 27 | var polusEjectController = polusShipStatus?.ExileCutscenePrefab?.TryCast(); 28 | if (!polusEjectController) 29 | throw new Exception("Failed to get Eject Controller from Polus's ShipStatus"); 30 | 31 | // Get Hand Prefab 32 | var handPrefab = polusEjectController?.HandSlot; 33 | if (!handPrefab) 34 | throw new Exception("Failed to get Player Prefab from Skeld's Eject Controller"); 35 | 36 | // Clone Sprite to Object 37 | var hand = MapUtils.CloneSprite(obj, handPrefab?.gameObject); 38 | 39 | // Update Sprite (Thumb or Hand) 40 | var isThumb = elem.type == "util-ejectthumb"; 41 | hand.sprite = isThumb ? polusEjectController?.GoodHand : polusEjectController?.BadHand; 42 | 43 | // Add to Hands 44 | AllHands.Add(hand); 45 | 46 | // Hide Object By Default 47 | obj.SetActive(false); 48 | } 49 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerSignal.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LevelImposter.Trigger; 4 | 5 | /// 6 | /// Trigger signal 7 | /// 8 | public class TriggerSignal 9 | { 10 | /// 11 | /// Constructs a trigger signal from a player 12 | /// 13 | /// The object to trigger 14 | /// The trigger ID 15 | /// The player that triggered the object 16 | public TriggerSignal(GameObject targetObject, string triggerID, PlayerControl? sourcePlayer) 17 | { 18 | TargetObject = targetObject; 19 | TriggerID = triggerID; 20 | StackSize = 1; 21 | SourcePlayer = sourcePlayer; 22 | } 23 | 24 | /// 25 | /// Constructs a trigger signal that propogated from another trigger 26 | /// 27 | /// The object to trigger 28 | /// The trigger ID 29 | /// The original trigger that ran this trigger 30 | public TriggerSignal(GameObject targetObject, string triggerID, TriggerSignal? sourceTrigger) 31 | { 32 | TargetObject = targetObject; 33 | TriggerID = triggerID; 34 | StackSize = (sourceTrigger?.StackSize ?? 0) + 1; 35 | SourcePlayer = sourceTrigger?.SourcePlayer; 36 | SourceTrigger = sourceTrigger; 37 | } 38 | 39 | public GameObject TargetObject { get; private set; } 40 | public string TriggerID { get; private set; } 41 | public int StackSize { get; } 42 | 43 | // Options 44 | public PlayerControl? SourcePlayer { get; } 45 | public TriggerSignal? SourceTrigger { get; private set; } 46 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Sub/TaskDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.Json.Serialization; 4 | using Il2CppInterop.Runtime.InteropTypes.Arrays; 5 | using LevelImposter.Core; 6 | 7 | namespace LevelImposter.DB; 8 | 9 | /// 10 | /// Database of PlayerTask objects 11 | /// 12 | public class TaskDB(SerializedAssetDB serializedDB) : SubDB(serializedDB) 13 | { 14 | public override void LoadShip(ShipStatus shipStatus, MapType mapType) 15 | { 16 | DB.TaskDB.ForEach(elem => 17 | { 18 | if (elem.MapType != mapType) 19 | return; 20 | 21 | // Task Type 22 | var taskArr = elem.TaskType switch 23 | { 24 | TaskLength.Common => shipStatus.CommonTasks.Cast>(), 25 | TaskLength.Long => shipStatus.LongTasks.Cast>(), 26 | TaskLength.Short => shipStatus.ShortTasks.Cast>(), 27 | _ => shipStatus.SpecialTasks 28 | }; 29 | 30 | // Task 31 | var task = taskArr.FirstOrDefault(e => { return e.name == elem.Name; }); 32 | if (task == null) 33 | { 34 | LILogger.Warn($"TaskDB could not find {elem.ID} in {shipStatus.name}"); 35 | return; 36 | } 37 | 38 | Add(elem.ID, task); 39 | }); 40 | } 41 | 42 | [Serializable] 43 | public class DBElement 44 | { 45 | public string ID { get; set; } 46 | public string Name { get; set; } 47 | public int Map { get; set; } 48 | public int Type { get; set; } 49 | 50 | [JsonIgnore] public MapType MapType => (MapType)Map; 51 | [JsonIgnore] public TaskLength TaskType => (TaskLength)Type; 52 | } 53 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Generic/MapPropertiesBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LevelImposter.Core; 3 | using LevelImposter.DB; 4 | using LevelImposter.Shop; 5 | using UnityEngine; 6 | 7 | namespace LevelImposter.Builders; 8 | 9 | /// 10 | /// Configures the map properties 11 | /// 12 | public class MapPropertiesBuilder : IElemBuilder 13 | { 14 | public static readonly Dictionary EXILE_IDS = new() 15 | { 16 | { "Skeld", "ss-skeld" }, 17 | { "MiraHQ", "ss-mira" }, 18 | { "Polus", "ss-polus" }, 19 | { "Airship", "ss-airship" }, 20 | { "Fungle", "ss-fungle" } 21 | }; 22 | 23 | public MapPropertiesBuilder() 24 | { 25 | // Get Ship Status 26 | var shipStatus = LIShipStatus.GetShip(); 27 | if (shipStatus == null) 28 | return; 29 | 30 | // Get Map 31 | var map = MapLoader.CurrentMap; 32 | 33 | // Set Map Name 34 | shipStatus.name = map.name; 35 | 36 | // Set Background Color 37 | if (!string.IsNullOrEmpty(map.properties.bgColor)) 38 | if (ColorUtility.TryParseHtmlString(map.properties.bgColor, out var bgColor)) 39 | Camera.main.backgroundColor = bgColor; 40 | 41 | // Set Exile Animation 42 | if (string.IsNullOrEmpty(map.properties.exileID)) 43 | return; 44 | 45 | if (!EXILE_IDS.ContainsKey(map.properties.exileID)) 46 | { 47 | LILogger.Warn($"Unknown exile ID: {map.properties.exileID}"); 48 | return; 49 | } 50 | 51 | var prefabShip = AssetDB.GetObject(EXILE_IDS[map.properties.exileID]); 52 | var prefabShipStatus = prefabShip?.GetComponent(); 53 | if (prefabShipStatus == null) 54 | return; 55 | shipStatus.ExileCutscenePrefab = prefabShipStatus.ExileCutscenePrefab; 56 | } 57 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Ship/SporePatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Hazel; 3 | using LevelImposter.Builders; 4 | using LevelImposter.Trigger; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | /// 9 | /// Normally, spores are handled by 10 | /// FungleShipStatus. This bypasses that 11 | /// dependency by supplying it's own 12 | /// spore listings. 13 | /// 14 | [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] 15 | public static class SporePatchHandle 16 | { 17 | public static bool Prefix( 18 | [HarmonyArgument(0)] byte callId, 19 | [HarmonyArgument(1)] MessageReader reader, 20 | PlayerControl __instance) 21 | { 22 | if (!LIShipStatus.IsInstance()) 23 | return true; 24 | if (callId != (byte)RpcCalls.TriggerSpores && callId != (byte)RpcCalls.CheckSpore) 25 | return true; 26 | 27 | // Find spores 28 | var mushroomId = reader.ReadPackedInt32(); 29 | if (mushroomId < 0 || mushroomId >= SporeBuilder.Mushrooms.Count) 30 | { 31 | LILogger.Warn($"[RPC] Could not find a mushroom with ID {mushroomId}"); 32 | return true; 33 | } 34 | 35 | // Run method 36 | if (callId == (byte)RpcCalls.TriggerSpores) 37 | SporeBuilder.Mushrooms[mushroomId].TriggerSpores(); 38 | else 39 | __instance.CheckSporeTrigger(SporeBuilder.Mushrooms[mushroomId]); 40 | 41 | return false; 42 | } 43 | } 44 | 45 | [HarmonyPatch(typeof(Mushroom), nameof(Mushroom.TriggerSpores))] 46 | public static class SporeTriggerHandle 47 | { 48 | public static void Postfix(Mushroom __instance) 49 | { 50 | if (!LIShipStatus.IsInstance()) 51 | return; 52 | 53 | TriggerSignal signal = new(__instance.gameObject, "onActivate", PlayerControl.LocalPlayer); 54 | TriggerSystem.GetInstance().FireTrigger(signal); 55 | } 56 | } -------------------------------------------------------------------------------- /LevelImposter/Trigger/TriggerHandles/TimerTriggerHandle.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using Il2CppInterop.Runtime.Attributes; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Trigger; 7 | 8 | public class TimerTriggerHandle : ITriggerHandle 9 | { 10 | private readonly GameObjectCoroutineManager _timerManager = new(); 11 | 12 | public void OnTrigger(TriggerSignal signal) 13 | { 14 | if (signal.TriggerID != "startTimer" && 15 | signal.TriggerID != "stopTimer") 16 | return; 17 | 18 | // Start timer 19 | if (signal.TriggerID == "startTimer") 20 | _timerManager.Start(signal.TargetObject, CoTimerTrigger(signal)); 21 | 22 | // Stop timer 23 | else if (signal.TriggerID == "stopTimer") _timerManager.Stop(signal.TargetObject); 24 | } 25 | 26 | 27 | /// 28 | /// Coroutine to run timer trigger. Fires onStart on the start and onFinish on completion. 29 | /// 30 | /// Duration of the timer in seconds 31 | [HideFromIl2Cpp] 32 | private IEnumerator CoTimerTrigger(TriggerSignal signal) 33 | { 34 | // Get the object data 35 | var objectData = signal.TargetObject.GetLIData(); 36 | 37 | // Get timer properties 38 | var duration = objectData.Properties.triggerTime ?? 1; 39 | var isLoop = objectData.Properties.triggerLoop ?? false; 40 | 41 | // Create Triggers 42 | var startTrigger = new TriggerSignal(signal.TargetObject, "onStart", signal); 43 | var endTrigger = new TriggerSignal(signal.TargetObject, "onFinish", signal); 44 | 45 | // Loop Timer 46 | do 47 | { 48 | TriggerSystem.GetInstance().FireTrigger(startTrigger); 49 | yield return new WaitForSeconds(duration); 50 | TriggerSystem.GetInstance().FireTrigger(endTrigger); 51 | } while (isLoop); 52 | } 53 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LagLimiter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Limits the amount of time a coroutine can divert the main thread 9 | /// 10 | public class LagLimiter(IntPtr intPtr) : MonoBehaviour(intPtr) 11 | { 12 | private Stopwatch? _frameTimer; // Basic FPS counter 13 | private bool _hasContinuedThisFrame; // Continue at least once per frame 14 | 15 | public static LagLimiter? Instance { get; private set; } 16 | 17 | public void Awake() 18 | { 19 | if (Instance == null) 20 | { 21 | Instance = this; 22 | DontDestroyOnLoad(gameObject); 23 | } 24 | else 25 | { 26 | Destroy(gameObject); 27 | } 28 | } 29 | 30 | public void Update() 31 | { 32 | _frameTimer?.Restart(); 33 | _hasContinuedThisFrame = false; 34 | } 35 | 36 | public void OnEnable() 37 | { 38 | _frameTimer = new Stopwatch(); 39 | _frameTimer?.Start(); 40 | } 41 | 42 | public void OnDisable() 43 | { 44 | _frameTimer?.Stop(); 45 | } 46 | 47 | /// 48 | /// Returns true if the coroutine should continue 49 | /// 50 | /// Minimum FPS to maintain 51 | /// true if the coroutine should continue, false otherwise 52 | private bool CheckShouldContinue(float minFPS) 53 | { 54 | float elapsedMilliseconds = _frameTimer?.ElapsedMilliseconds ?? 0; 55 | var currentFPS = 1000.0f / elapsedMilliseconds; 56 | var shouldContinue = currentFPS > minFPS || !_hasContinuedThisFrame; 57 | _hasContinuedThisFrame = true; 58 | return shouldContinue; 59 | } 60 | 61 | public static bool ShouldContinue(float minFPS) 62 | { 63 | return Instance?.CheckShouldContinue(minFPS) ?? true; 64 | } 65 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LIStar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Il2CppInterop.Runtime.Attributes; 3 | using UnityEngine; 4 | using Random = UnityEngine.Random; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | /// 9 | /// Object that flies across screen and respawns when it hits the edge 10 | /// 11 | public class LIStar(IntPtr intPtr) : MonoBehaviour(intPtr) 12 | { 13 | private float _currentSpeed; 14 | 15 | private float _height = 10; 16 | private float _length = 10; 17 | private float _maxSpeed = 2; 18 | private float _minSpeed = 2; 19 | 20 | public void Start() 21 | { 22 | Respawn(true); 23 | } 24 | 25 | public void Update() 26 | { 27 | transform.localPosition -= new Vector3( 28 | _currentSpeed * Time.deltaTime, 29 | 0, 30 | 0 31 | ); 32 | if (transform.localPosition.x < -_length) 33 | Respawn(false); 34 | } 35 | 36 | /// 37 | /// Initializes a star from util-starfield 38 | /// 39 | /// LIElement to extract props from 40 | [HideFromIl2Cpp] 41 | public void Init(LIElement elem) 42 | { 43 | _height = elem.properties.starfieldHeight ?? _height; 44 | _length = elem.properties.starfieldLength ?? _length; 45 | _minSpeed = elem.properties.starfieldMinSpeed ?? _minSpeed; 46 | _maxSpeed = elem.properties.starfieldMaxSpeed ?? _maxSpeed; 47 | } 48 | 49 | /// 50 | /// Respawns the Star in the Star Field 51 | /// 52 | /// TRUE will also randomize the X position 53 | private void Respawn(bool isInitial) 54 | { 55 | _currentSpeed = Random.Range(_minSpeed, _maxSpeed); 56 | transform.localPosition = new Vector3( 57 | isInitial ? Random.Range(-_length, 0) : 0, 58 | Random.Range(-_height / 2, _height / 2), 59 | 0 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/EjectBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Core; 3 | using LevelImposter.DB; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace LevelImposter.Builders; 8 | 9 | public class EjectBuilder : IElemBuilder 10 | { 11 | public static LIExileController? EjectController { get; private set; } 12 | 13 | public void OnBuild(LIElement elem, GameObject obj) 14 | { 15 | if (elem.type != "util-eject") 16 | return; 17 | 18 | // Create container object (This will be a prefab) 19 | var container = new GameObject("EjectContainer"); 20 | container.transform.SetParent(obj.transform.parent); 21 | container.SetActive(false); 22 | obj.transform.SetParent(container.transform); 23 | 24 | // Get Eject Controller Prefab 25 | var skeldPrefab = AssetDB.GetObject("ss-skeld"); 26 | var skeldShipStatus = skeldPrefab?.GetComponent(); 27 | var skeldEjectController = skeldShipStatus?.ExileCutscenePrefab; 28 | if (!skeldEjectController) 29 | throw new Exception("Failed to get Eject Controller from Skeld's ShipStatus"); 30 | 31 | // Copy Components from Skeld's Prefab 32 | var impostorText = Object.Instantiate(skeldEjectController?.ImpostorText, obj.transform); 33 | var text = Object.Instantiate(skeldEjectController?.Text, obj.transform); 34 | var player = Object.Instantiate(skeldEjectController?.Player, obj.transform); 35 | 36 | // TODO: Hide Player 37 | 38 | // Create Eject Controller 39 | EjectController = obj.AddComponent(); 40 | EjectController.ImpostorText = impostorText; 41 | EjectController.Text = text; 42 | EjectController.Player = player; 43 | EjectController.TextSound = skeldEjectController?.TextSound; 44 | 45 | // Add to ShipStatus 46 | var shipStatus = LIShipStatus.GetShip(); 47 | shipStatus.ExileCutscenePrefab = EjectController; 48 | } 49 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/StepSoundBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.AssetLoader; 2 | using LevelImposter.Core; 3 | using LevelImposter.DB; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Builders; 7 | 8 | internal class StepSoundBuilder : IElemBuilder 9 | { 10 | public void OnBuild(LIElement elem, GameObject obj) 11 | { 12 | if (elem.type != "util-sound2") 13 | return; 14 | 15 | // Colliders 16 | Collider2D[] colliders = obj.GetComponentsInChildren(); 17 | foreach (var collider in colliders) 18 | collider.isTrigger = true; 19 | if (colliders.Length < 1) 20 | { 21 | LILogger.Warn($"{elem.name} missing cooresponding collision"); 22 | return; 23 | } 24 | 25 | // AudioClip 26 | if (elem.properties.sounds == null) 27 | { 28 | LILogger.Warn($"{elem.name} missing audio listing"); 29 | return; 30 | } 31 | 32 | // Sound Group 33 | var soundGroup = ScriptableObject.CreateInstance(); 34 | soundGroup.Clips = new AudioClip[elem.properties.sounds.Length]; 35 | for (var i = 0; i < elem.properties.sounds.Length; i++) 36 | { 37 | // Sound Data 38 | var sound = elem.properties.sounds[i]; 39 | if (sound == null) 40 | { 41 | LILogger.Warn($"{elem.name} missing audio data"); 42 | continue; 43 | } 44 | 45 | // Preset 46 | if (sound.isPreset) 47 | soundGroup.Clips[i] = AssetDB.GetSound(sound.presetID ?? ""); 48 | // WAVLoader 49 | else 50 | soundGroup.Clips[i] = WAVLoader.Load(sound); 51 | } 52 | 53 | // Sound Player 54 | var stepPlayer = obj.AddComponent(); 55 | stepPlayer.Area = colliders[0]; 56 | stepPlayer.Sounds = soundGroup; 57 | stepPlayer.priority = elem.properties.soundPriority ?? 0; 58 | } 59 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/FilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using LevelImposter.DB; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | internal class FilterBuilder : IElemBuilder 8 | { 9 | public void OnBuild(LIElement elem, GameObject obj) 10 | { 11 | if (elem.type != "util-filter") 12 | return; 13 | 14 | // Prefab 15 | var sporePrefab = AssetDB.GetObject("util-spore"); 16 | if (sporePrefab == null) 17 | return; 18 | var maskPrefab = sporePrefab.transform.FindChild("SporeScreenMask").gameObject; 19 | var maskPrefabRenderer = maskPrefab.GetComponent(); 20 | 21 | // Create Sprite 22 | var spriteRenderer = obj.GetComponent(); 23 | if (spriteRenderer == null) 24 | { 25 | spriteRenderer = obj.AddComponent(); 26 | spriteRenderer.sprite = MapUtils.GetDefaultSquare(); 27 | spriteRenderer.color = elem.properties.color?.ToUnity() ?? Color.white; 28 | } 29 | 30 | // Create Mask 31 | var maskObj = Object.Instantiate(obj, obj.transform); 32 | maskObj.name = "Mask"; 33 | maskObj.transform.localScale = Vector3.one; 34 | maskObj.transform.position = new Vector3( 35 | obj.transform.position.x, 36 | obj.transform.position.y, 37 | 10.0f 38 | ); 39 | var maskRenderer = maskObj.GetComponent(); 40 | maskRenderer.material = maskPrefabRenderer.material; 41 | 42 | // Update Mask on Sprite Load 43 | SpriteBuilder.OnSpriteLoad += (loadedElem, _) => 44 | { 45 | if (loadedElem.id != elem.id || maskRenderer == null) 46 | return; 47 | maskRenderer.sprite = spriteRenderer.sprite; 48 | maskRenderer.color = spriteRenderer.color; 49 | }; 50 | 51 | // Set Layer 52 | obj.layer = (int)Layer.Ship; 53 | maskObj.layer = (int)Layer.Ship; 54 | } 55 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Util/ThumbnailCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Il2CppInterop.Runtime.Attributes; 3 | using LevelImposter.AssetLoader; 4 | using LevelImposter.Core; 5 | using UnityEngine; 6 | 7 | namespace LevelImposter.Shop; 8 | 9 | /// 10 | /// API to manage thumbnail files in the local filesystem 11 | /// 12 | public static class ThumbnailCache 13 | { 14 | /// 15 | /// Checks if a thumbnail exists in the local cache 16 | /// 17 | /// ID of the map thumbnail to check 18 | /// true if the map thumbnail exists in the cache, false otherwise 19 | public static bool Exists(string mapID) 20 | { 21 | return FileCache.Exists($"{mapID}.png"); 22 | } 23 | 24 | /// 25 | /// Saves a thumbnail to the local cache 26 | /// 27 | /// ID of the map thumbnail to save 28 | /// Raw image bytes to write to disk 29 | public static void Save(string mapID, byte[] thumbnailBytes) 30 | { 31 | FileCache.Save($"{mapID}.png", thumbnailBytes); 32 | } 33 | 34 | /// 35 | /// Reads and parses a thumbnail file into a Texture2D. 36 | /// 37 | /// Map ID for the thumbnail 38 | /// Callback on success 39 | [HideFromIl2Cpp] 40 | public static void Get(string mapID, Action callback) 41 | { 42 | if (!Exists(mapID)) 43 | { 44 | LILogger.Warn($"Could not find [{mapID}] thumbnail in filesystem"); 45 | return; 46 | } 47 | 48 | // Read thumbnail from filesystem 49 | LILogger.Info($"Loading thumbnail [{mapID}] from filesystem"); 50 | 51 | // Load thumbnail into sprite 52 | SpriteLoader.LoadAsync( 53 | $"{mapID}_thumb", 54 | new FileStore(FileCache.GetPath($"{mapID}.png")), 55 | callback 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Task/TaskBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using LevelImposter.DB; 3 | using UnityEngine; 4 | using UnityEngine.Events; 5 | 6 | namespace LevelImposter.Builders; 7 | 8 | public class TaskBuilder : IElemBuilder 9 | { 10 | private readonly TaskConsoleBuilder _consoleBuilder = new(); 11 | private readonly ShipTaskBuilder _shipBuilder = new(); 12 | 13 | public void OnBuild(LIElement elem, GameObject obj) 14 | { 15 | if (!elem.type.StartsWith("task-")) 16 | return; 17 | 18 | // Prefab 19 | var prefab = AssetDB.GetObject(elem.type); 20 | if (prefab == null) 21 | return; 22 | 23 | // Sprite 24 | MapUtils.CloneSprite(obj, prefab); 25 | 26 | // Console 27 | var console = _consoleBuilder.Build(elem, obj, prefab); 28 | _shipBuilder.Build(elem, console); 29 | 30 | // Button 31 | var prefabBtn = prefab.GetComponentInChildren(); 32 | var collider = MapUtils.CreateDefaultColliders(obj, prefab); 33 | if (prefabBtn != null) 34 | { 35 | var btn = obj.AddComponent(); 36 | btn.ClickMask = collider; 37 | btn.OnMouseOver = new UnityEvent(); 38 | btn.OnMouseOut = new UnityEvent(); 39 | var action = console.Use; 40 | btn.OnClick.AddListener(action); 41 | } 42 | 43 | // Medscan 44 | var isMedscan = elem.type == "task-medscan"; 45 | if (isMedscan) 46 | { 47 | // ShipStatus 48 | var shipStatus = LIShipStatus.GetInstance().ShipStatus; 49 | 50 | // MedScanner 51 | if (shipStatus.MedScanner != null) 52 | LILogger.Warn("Only 1 med scanner can be used per map"); 53 | var medscan = obj.AddComponent(); 54 | shipStatus.MedScanner = medscan; 55 | } 56 | } 57 | 58 | public void OnCleanup() 59 | { 60 | _consoleBuilder.OnCleanup(); 61 | _shipBuilder.OnCleanup(); 62 | } 63 | } -------------------------------------------------------------------------------- /LevelImposter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LevelImposter", "LevelImposter\LevelImposter.csproj", "{11FBC798-BAF5-4EE5-9511-BE6DB0592F99}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FF409D3C-A75E-4023-A0C9-12F2C366BA41}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | BuildMod|Any CPU = BuildMod|Any CPU 16 | BuildMod|x86 = BuildMod|x86 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x86 = Release|x86 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.BuildMod|Any CPU.ActiveCfg = Release|Any CPU 24 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.BuildMod|Any CPU.Build.0 = Release|Any CPU 25 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.BuildMod|x86.ActiveCfg = Release|Any CPU 26 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.BuildMod|x86.Build.0 = Release|Any CPU 27 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {11FBC798-BAF5-4EE5-9511-BE6DB0592F99}.Release|x86.ActiveCfg = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(SolutionProperties) = preSolution 35 | HideSolutionNode = FALSE 36 | EndGlobalSection 37 | GlobalSection(ExtensibilityGlobals) = postSolution 38 | SolutionGuid = {900EDCA2-9009-41C6-AD8B-05F615316F09} 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Triggers/ConsolePatch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using HarmonyLib; 4 | using LevelImposter.Trigger; 5 | using UnityEngine; 6 | 7 | namespace LevelImposter.Core; 8 | 9 | /// 10 | /// Fires "onUse" triggers on a variety of console objects 11 | /// 12 | [HarmonyPatch] 13 | public class ConsolePatch 14 | { 15 | public static IEnumerable TargetMethods() 16 | { 17 | yield return AccessTools.Method(typeof(Console), nameof(Console.Use)); 18 | yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.Use)); 19 | yield return AccessTools.Method(typeof(DoorConsole), nameof(DoorConsole.Use)); 20 | yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.Use)); 21 | } 22 | 23 | public static bool Prefix(MonoBehaviour __instance) 24 | { 25 | if (!LIShipStatus.IsInstance()) 26 | return true; 27 | 28 | // Get IUsable 29 | var usable = __instance.TryCast(); 30 | if (usable == null) 31 | return true; 32 | 33 | // Check if the player can use the console 34 | usable.CanUse(PlayerControl.LocalPlayer.Data, out var canUse, out _); 35 | if (!canUse) 36 | return true; 37 | 38 | // Update Last Console 39 | MinigamePatch.LastConsole = __instance.gameObject; 40 | 41 | // Get Object Data 42 | var objectData = __instance.gameObject.GetComponent(); 43 | if (objectData == null) 44 | return true; 45 | 46 | // Create Trigger 47 | var isClientSide = objectData.Properties.triggerClientSide ?? true; 48 | TriggerSignal signal = new(__instance.gameObject, "onUse", PlayerControl.LocalPlayer); 49 | 50 | // Fire Trigger 51 | if (isClientSide) 52 | TriggerSystem.GetInstance().FireTrigger(signal); 53 | else 54 | TriggerSystem.GetInstance().FireTriggerRPC(signal); 55 | 56 | // TODO: Check if should continue? 57 | return true; 58 | } 59 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/GameObjectCoroutineManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using BepInEx.Unity.IL2CPP.Utils.Collections; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | /// 9 | /// Manages a singular coroutine across a set of GameObjects 10 | /// 11 | public class GameObjectCoroutineManager 12 | { 13 | private readonly Dictionary _activeCoroutines = new(); 14 | 15 | private int GetObjectID(GameObject gameObject) 16 | { 17 | return gameObject.GetInstanceID(); 18 | } 19 | 20 | /// 21 | /// Starts a coroutine on a game object. 22 | /// Automatically stops the existing coroutine if one exists. 23 | /// 24 | /// GameObject to run coroutine on 25 | /// The coroutine to run 26 | public void Start(GameObject gameObject, IEnumerator coroutine) 27 | { 28 | Stop(gameObject); 29 | var objectID = GetObjectID(gameObject); 30 | var newCoroutine = LIShipStatus.GetInstance().StartCoroutine( 31 | CoRunCoroutine(objectID, coroutine).WrapToIl2Cpp() 32 | ); 33 | _activeCoroutines[objectID] = newCoroutine; 34 | } 35 | 36 | /// 37 | /// Stops the coroutine on a game object if one exists 38 | /// 39 | /// GameObject to stop coroutines from 40 | public void Stop(GameObject gameObject) 41 | { 42 | var objectID = GetObjectID(gameObject); 43 | if (_activeCoroutines.ContainsKey(objectID)) 44 | { 45 | LIShipStatus.GetInstance().StopCoroutine(_activeCoroutines[objectID]); 46 | _activeCoroutines.Remove(objectID); 47 | } 48 | } 49 | 50 | /// 51 | /// Coroutine to remove the coroutine from the active list on completion 52 | /// 53 | private IEnumerator CoRunCoroutine(int objectID, IEnumerator coroutine) 54 | { 55 | yield return coroutine; 56 | _activeCoroutines.Remove(objectID); 57 | } 58 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/Loaders/WAVLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using LevelImposter.Core; 3 | using LevelImposter.Shop; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.AssetLoader; 7 | 8 | public static class WAVLoader 9 | { 10 | /// 11 | /// Loads a WAV from a sound data object. 12 | /// 13 | /// Sound Data to load 14 | /// Sound data in the form of a Unity AudioClip 15 | public static AudioClip? Load(LISound? soundData) 16 | { 17 | // Get Sound Data 18 | if (soundData == null) 19 | return null; 20 | 21 | // Get Sound Stream 22 | var soundDBElem = MapLoader.CurrentMap?.mapAssetDB?.Get(soundData.dataID); 23 | if (soundDBElem == null) 24 | return null; 25 | 26 | // Get Sound Data 27 | using var stream = soundDBElem.OpenStream(); 28 | return Load(stream, soundData.id.ToString()); 29 | } 30 | 31 | /// 32 | /// Loads a WAV from a data store. 33 | /// 34 | /// Data store containing the WAV data 35 | /// Name of the resulting object 36 | /// Sound data in the form of a Unity AudioClip 37 | public static AudioClip Load(IDataStore dataStore, string name) 38 | { 39 | using var stream = dataStore.OpenStream(); 40 | return Load(stream, name); 41 | } 42 | 43 | /// 44 | /// Loads a WAV from a raw stream. 45 | /// 46 | /// Raw WAV file stream 47 | /// Name of the resulting object 48 | /// Sound data in the form of a Unity AudioClip 49 | public static AudioClip Load(Stream wavStream, string name) 50 | { 51 | // Create a new WAV file 52 | var wavFile = new WAVFile(name); 53 | GCHandler.Register(wavFile); 54 | 55 | // Load the WAV file from the stream 56 | wavFile.Load(wavStream); 57 | 58 | // Return the WAV file 59 | return wavFile.GetClip(); 60 | } 61 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LITriggerSpawnable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using BepInEx.Unity.IL2CPP.Utils.Collections; 4 | using Il2CppInterop.Runtime.Attributes; 5 | using LevelImposter.Trigger; 6 | using UnityEngine; 7 | 8 | namespace LevelImposter.Core; 9 | 10 | /// 11 | /// Fires a trigger on creation. Used for spawnable prefabs. 12 | /// 13 | public class LITriggerSpawnable(IntPtr intPtr) : MonoBehaviour(intPtr) 14 | { 15 | private string _triggerID = ""; 16 | 17 | private GameObject? _triggerTarget; 18 | 19 | public void Start() 20 | { 21 | if (_triggerTarget == null || _triggerID == "") 22 | LILogger.Warn("A Spawnable Trigger enabled without a target"); 23 | StartCoroutine(CoFireTrigger().WrapToIl2Cpp()); 24 | } 25 | 26 | public void OnDestroy() 27 | { 28 | _triggerID = ""; 29 | _triggerTarget = null; 30 | } 31 | 32 | /// 33 | /// Sets spawnable trigger properties 34 | /// 35 | /// GameObject to trigger 36 | /// ID of the trigger 37 | public void SetTrigger(GameObject triggerTarget, string triggerID) 38 | { 39 | _triggerTarget = triggerTarget; 40 | _triggerID = triggerID; 41 | gameObject.SetActive(false); 42 | } 43 | 44 | /// 45 | /// Coroutine that fires the trigger once the LocalPlayer is spawned in 46 | /// 47 | [HideFromIl2Cpp] 48 | private IEnumerator CoFireTrigger() 49 | { 50 | while (PlayerControl.LocalPlayer == null 51 | || LIShipStatus.GetInstanceOrNull()?.IsReady != true 52 | || (!GameManager.Instance.GameHasStarted && GameManager.Instance.ShouldCheckForGameEnd) 53 | || !LagLimiter.ShouldContinue(30)) 54 | yield return null; 55 | 56 | if (_triggerTarget != null) 57 | { 58 | TriggerSignal signal = new(_triggerTarget, _triggerID, PlayerControl.LocalPlayer); 59 | TriggerSystem.GetInstance().FireTrigger(signal); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/ModCompatibility/VentPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HarmonyLib; 3 | using UnityEngine.Events; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /* 8 | * Fixes instantiated vents 9 | * (TOU miner / TOR JackInTheBox) 10 | * to append UnityAction 11 | */ 12 | [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.FixedUpdate))] 13 | public static class VentPatch 14 | { 15 | private static int _ventTotal = -1; 16 | 17 | public static void Postfix() 18 | { 19 | if (!LIShipStatus.IsInstance()) 20 | return; 21 | if (!ModCompatibility.IsTOUEnabled && !ModCompatibility.IsTOREnabled && !ModCompatibility.IsReworkedEnabled) 22 | return; 23 | if (_ventTotal == ShipStatus.Instance.AllVents.Count) 24 | return; 25 | _ventTotal = ShipStatus.Instance.AllVents.Count; 26 | 27 | // Iterate Vents 28 | for (var i = 0; i < ShipStatus.Instance.AllVents.Count; i++) 29 | { 30 | var vent = ShipStatus.Instance.AllVents[i]; 31 | 32 | // Filter Vents 33 | var fixedVentTag = $"(V{i})"; 34 | var isFixed = vent.name.Contains(fixedVentTag); 35 | var isLIVent = vent.transform.childCount == 1; 36 | if (isFixed || !isLIVent) 37 | continue; 38 | 39 | // Update Button Actions 40 | vent.name = $"{vent.name}{fixedVentTag}"; 41 | for (var b = 0; b < vent.Buttons.Length; b++) 42 | { 43 | var ventButton = vent.Buttons[b]; 44 | ventButton.OnMouseOver = new UnityEvent(); 45 | ventButton.OnMouseOut = new UnityEvent(); 46 | Action action = b switch 47 | { 48 | 0 => vent.ClickRight, 49 | 1 => vent.ClickLeft, 50 | 2 => vent.ClickCenter, 51 | _ => HandleError 52 | }; 53 | ventButton.OnClick.AddListener(action); 54 | } 55 | } 56 | } 57 | 58 | private static void HandleError() 59 | { 60 | LILogger.Warn("Vent object has an extra button assigned"); 61 | } 62 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/DownloadManagerPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using InnerNet; 3 | 4 | namespace LevelImposter.Shop; 5 | 6 | /* 7 | * Remove player from DownloadManager 8 | * if the player disconnects 9 | */ 10 | [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))] 11 | public static class DisconnectPatch 12 | { 13 | public static void Postfix([HarmonyArgument(0)] ClientData data) 14 | { 15 | if (data.Character != null) 16 | DownloadManager.RemovePlayer(data.Character); 17 | } 18 | } 19 | 20 | /* 21 | * Updates the lobby menu 22 | * text w/ DownloadManager 23 | */ 24 | [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Update))] 25 | public static class LobbyTextPatch 26 | { 27 | private static bool _isDownloadTextDisplayed; 28 | 29 | public static void Postfix(GameStartManager __instance) 30 | { 31 | if (!DownloadManager.CanStart) 32 | { 33 | // Disable Button 34 | __instance.StartButton.SetButtonEnableState(false); 35 | 36 | // Set Text 37 | var buttonText = DownloadManager.GetStartText(); 38 | __instance.GameStartTextClient.text = buttonText; // Client 39 | __instance.StartButton.ChangeButtonText(buttonText); // Host 40 | 41 | // Set flag 42 | _isDownloadTextDisplayed = true; 43 | } 44 | 45 | // Check flag to prevent unnecessary updates 46 | else if (_isDownloadTextDisplayed) 47 | { 48 | // Force GameStartManager.Update to update the button state 49 | __instance.LastPlayerCount = -1; 50 | 51 | // Clear Text 52 | __instance.GameStartTextClient.text = string.Empty; 53 | 54 | // Clear flag 55 | _isDownloadTextDisplayed = false; 56 | } 57 | } 58 | } 59 | 60 | /* 61 | * Disables the Start Button 62 | * when the map isn't downloaded 63 | */ 64 | [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.BeginGame))] 65 | public static class LobbyStartPatch 66 | { 67 | public static bool Prefix() 68 | { 69 | return DownloadManager.CanStart; 70 | } 71 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Minimap/RoomNameBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using TMPro; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | public class RoomNameBuilder : IElemBuilder 8 | { 9 | private int _nameCount; 10 | 11 | public void OnBuild(LIElement elem, GameObject obj) 12 | { 13 | if (elem.type != "util-room") 14 | return; 15 | 16 | // Check Visibility 17 | var isMinimapVisible = elem.properties.isRoomNameVisible ?? true; 18 | if (!isMinimapVisible) 19 | return; 20 | 21 | // ShipStatus 22 | var shipStatus = LIShipStatus.GetShip(); 23 | 24 | // Minimap 25 | var mapBehaviour = MinimapBuilder.GetMinimap(); 26 | 27 | // Clone 28 | var roomNames = mapBehaviour.transform.GetChild(mapBehaviour.transform.childCount - 1); 29 | var roomNameClone = roomNames.GetChild(0).gameObject; 30 | 31 | // Object 32 | var mapScale = shipStatus.MapScale; 33 | var roomName = Object.Instantiate(roomNameClone, roomNames); 34 | roomName.name = elem.name; 35 | roomName.layer = (int)Layer.UI; 36 | roomName.transform.localPosition = new Vector3( 37 | elem.x / mapScale, 38 | elem.y / mapScale, 39 | -1 40 | ); 41 | 42 | // Text 43 | Object.Destroy(roomName.GetComponent()); 44 | var roomText = roomName.GetComponent(); 45 | roomText.text = elem.name.Replace("\\n", "\n"); 46 | roomText.fontSizeMin = roomText.fontSizeMax; 47 | roomText.alignment = TextAlignmentOptions.Bottom; 48 | roomText.enabled = true; 49 | _nameCount++; 50 | 51 | // Transform 52 | var rectTransform = roomName.GetComponent(); 53 | rectTransform.sizeDelta = new Vector2(10, 0); 54 | } 55 | 56 | public void OnCleanup() 57 | { 58 | var mapBehaviour = MinimapBuilder.GetMinimap(); 59 | var roomNames = mapBehaviour.transform.GetChild(mapBehaviour.transform.childCount - 1); 60 | 61 | while (roomNames.childCount > _nameCount) 62 | Object.DestroyImmediate(roomNames.GetChild(0).gameObject); 63 | } 64 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/PhysicsObjectBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using UnityEngine; 3 | 4 | namespace LevelImposter.Builders; 5 | 6 | internal class PhysicsObjectBuilder : IElemBuilder 7 | { 8 | private bool _isCameraFixed; 9 | 10 | public void OnBuild(LIElement elem, GameObject obj) 11 | { 12 | if (elem.type != "util-physics") 13 | return; 14 | 15 | // Add Rigidbody2D 16 | var rb = obj.AddComponent(); 17 | rb.mass = elem.properties.physicsMass ?? 10.0f; 18 | rb.drag = elem.properties.physicsDrag ?? 100.0f; 19 | rb.angularDrag = elem.properties.physicsAngularDrag ?? 100.0f; 20 | rb.freezeRotation = elem.properties.physicsFreezeRotation ?? false; 21 | rb.gravityScale = 0; 22 | 23 | // Add Constraints 24 | var constraints = RigidbodyConstraints2D.None; 25 | if (elem.properties.physicsFreezeX ?? false) 26 | constraints |= RigidbodyConstraints2D.FreezePositionX; 27 | if (elem.properties.physicsFreezeY ?? false) 28 | constraints |= RigidbodyConstraints2D.FreezePositionY; 29 | rb.constraints = constraints; 30 | 31 | // Create Physics Material 32 | var physicsMaterial = new PhysicsMaterial2D 33 | { 34 | bounciness = elem.properties.physicsBounciness ?? 0.6f, 35 | friction = elem.properties.physicsFriction ?? 0.6f 36 | }; 37 | rb.sharedMaterial = physicsMaterial; 38 | 39 | // Set Layer 40 | obj.layer = (int)Layer.Physics; 41 | 42 | // Add Physics Object Component 43 | obj.AddComponent(); 44 | 45 | // Fix Camera 46 | if (_isCameraFixed) 47 | return; 48 | 49 | // Fix Camera to render physics objects 50 | var camera = Camera.main; 51 | if (camera != null) 52 | camera.cullingMask |= 1 << (int)Layer.Physics; 53 | 54 | // Fix Shadow camera to render physics objects 55 | var shadowCamera = camera?.transform.Find("ShadowCamera")?.GetComponent(); 56 | if (shadowCamera != null) 57 | shadowCamera.cullingMask |= 1 << (int)Layer.Physics; 58 | 59 | _isCameraFixed = true; 60 | } 61 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/IO/LISerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using LevelImposter.Core; 7 | 8 | namespace LevelImposter.Shop; 9 | 10 | public static class LISerializer 11 | { 12 | private static JsonSerializerOptions? _options; 13 | 14 | /// 15 | /// Serializes a map into a string 16 | /// 17 | /// Map Data to serialize 18 | /// Raw LIM2 file data 19 | public static MemoryStream SerializeMap(LIMap mapData) 20 | { 21 | try 22 | { 23 | // Create Options 24 | if (_options == null) 25 | { 26 | _options = new JsonSerializerOptions(); 27 | _options.DefaultIgnoreCondition = 28 | JsonIgnoreCondition.WhenWritingNull; 29 | } 30 | 31 | // Open Stream 32 | MemoryStream stream = new(); 33 | 34 | // Update Legacy Format 35 | if (mapData.isLegacy) 36 | LegacyConverter.UpdateMap(mapData); 37 | 38 | // Map Data 39 | var mapJsonBytes = JsonSerializer.SerializeToUtf8Bytes(mapData, _options); 40 | stream.Write(BitConverter.GetBytes(mapJsonBytes.Length)); 41 | stream.Write(mapJsonBytes); 42 | 43 | // SpriteDB 44 | if (mapData.mapAssetDB != null) 45 | foreach (var spriteAsset in mapData.mapAssetDB.DB) 46 | { 47 | var data = spriteAsset.Value.LoadToMemory().Get(); 48 | var idBytes = Encoding.UTF8.GetBytes(spriteAsset.Key.ToString()); 49 | 50 | // Write Element 51 | stream.Write(idBytes); 52 | stream.Write(BitConverter.GetBytes(data.Length)); // <-- TODO: Improve memory usage here 53 | stream.Write(data); 54 | } 55 | 56 | // Return 57 | stream.Position = 0; 58 | return stream; 59 | } 60 | catch (Exception ex) 61 | { 62 | LILogger.Error(ex); 63 | return new MemoryStream(); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/GCHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using LevelImposter.AssetLoader; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace LevelImposter.Core; 8 | 9 | /// 10 | /// Handles garbage collection of disposable objects 11 | /// 12 | public static class GCHandler 13 | { 14 | private static readonly Stack Disposables = new(); 15 | 16 | /// 17 | /// Registers a new disposable object to be cleaned. Cleaning happens when a map is unloaded. 18 | /// 19 | /// Object to be cleaned 20 | public static void Register(IDisposable disposable) 21 | { 22 | Disposables.Push(disposable); 23 | } 24 | 25 | /// 26 | /// Creates and registers a new disposable object to be cleaned. Cleaning happens when a map is unloaded. 27 | /// 28 | /// UnityEngine Object to be cleaned 29 | public static void Register(Object obj) 30 | { 31 | Register(new DisposableUnityObject(obj)); 32 | } 33 | 34 | /// 35 | /// Cleans all registered disposables. Ran on map change. 36 | /// 37 | public static void Clean() 38 | { 39 | // Disposables 40 | LILogger.Info($"Disposing of {Disposables.Count} objects"); 41 | while (Disposables.Count > 0) 42 | Disposables.Pop().Dispose(); 43 | 44 | // Asset Loaders 45 | LILogger.Info($"{TextureLoader.Instance.CacheSize} cached textures"); 46 | LILogger.Info($"{SpriteLoader.Instance.CacheSize} cached sprites"); 47 | LILogger.Info($"{AudioLoader.Instance.CacheSize} cached audio clips"); 48 | TextureLoader.Instance.Clear(); 49 | SpriteLoader.Instance.Clear(); 50 | AudioLoader.Instance.Clear(); 51 | 52 | // GC 53 | GC.Collect(); 54 | } 55 | 56 | /// 57 | /// A thin wrapper around UnityEngine.Object.Destroy 58 | /// 59 | private class DisposableUnityObject(Object obj) : IDisposable 60 | { 61 | public void Dispose() 62 | { 63 | Object.Destroy(obj); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Minimap/AdminMapBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using LevelImposter.Core; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | public class AdminMapBuilder : IElemBuilder 8 | { 9 | public const float ICON_OFFSET = -0.25f; 10 | 11 | private readonly List _counterAreaDB = new(); 12 | private PoolableBehavior? _poolPrefab; 13 | 14 | public void OnBuild(LIElement elem, GameObject obj) 15 | { 16 | if (elem.type != "util-room") 17 | return; 18 | 19 | // Check Admin 20 | var isAdminVisible = elem.properties.isRoomAdminVisible ?? true; 21 | if (!isAdminVisible) 22 | return; 23 | 24 | // ShipStatus 25 | var shipStatus = LIShipStatus.GetShip(); 26 | 27 | var mapBehaviour = MinimapBuilder.GetMinimap(); 28 | var mapCountOverlay = mapBehaviour.countOverlay; 29 | 30 | // Prefab 31 | if (_poolPrefab == null) 32 | _poolPrefab = mapCountOverlay.CountAreas[0].pool.Prefab; 33 | 34 | // System 35 | var systemType = RoomBuilder.GetSystem(elem.id); 36 | 37 | // Map Room 38 | var overlayScale = mapCountOverlay.transform.localScale.x * shipStatus.MapScale; 39 | GameObject roomObj = new(elem.name); 40 | roomObj.transform.SetParent(mapCountOverlay.transform); 41 | roomObj.transform.localPosition = new Vector3( 42 | elem.x * (1 / overlayScale), 43 | elem.y * (1 / overlayScale) + ICON_OFFSET, 44 | -25.0f 45 | ); 46 | 47 | var counterArea = roomObj.AddComponent(); 48 | counterArea.RoomType = systemType; 49 | counterArea.pool = roomObj.AddComponent(); 50 | counterArea.pool.Prefab = _poolPrefab; 51 | 52 | _counterAreaDB.Add(counterArea); 53 | 54 | mapCountOverlay.CountAreas = _counterAreaDB.ToArray(); 55 | } 56 | 57 | public void OnCleanup() 58 | { 59 | var mapBehaviour = MinimapBuilder.GetMinimap(); 60 | var mapCountOverlay = mapBehaviour.countOverlay; 61 | 62 | while (mapCountOverlay.transform.childCount > _counterAreaDB.Count) 63 | Object.DestroyImmediate(mapCountOverlay.transform.GetChild(0).gameObject); 64 | } 65 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Animations/DoorPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Trigger; 3 | using PowerTools; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | //// 9 | /// Toggles a GIF animation alongside the 10 | /// regular animation components on doors. 11 | /// 12 | [HarmonyPatch(typeof(PlainDoor), nameof(PlainDoor.SetDoorway))] 13 | public static class DoorPatch 14 | { 15 | private static bool _hasStateChanged; 16 | 17 | public static void Prefix([HarmonyArgument(0)] bool open, PlainDoor __instance) 18 | { 19 | _hasStateChanged = open != __instance.Open; 20 | } 21 | 22 | public static void Postfix([HarmonyArgument(0)] bool open, PlainDoor __instance) 23 | { 24 | if (!LIShipStatus.IsInstance()) 25 | return; 26 | 27 | // Colliders 28 | Collider2D[] colliders = __instance.gameObject.GetComponentsInChildren(); 29 | foreach (var collider in colliders) 30 | collider.enabled = !open; 31 | 32 | // Dummy Collider 33 | var dummyCollider = __instance.GetComponent(); 34 | dummyCollider.enabled = false; 35 | 36 | // Sprite Renderer 37 | var animClip = open ? __instance.OpenDoorAnim : __instance.CloseDoorAnim; 38 | var spriteAnim = __instance.GetComponent(); 39 | var animator = __instance.GetComponent(); 40 | var spriteRenderer = __instance.GetComponent(); 41 | if (spriteAnim != null && animClip != null) 42 | { 43 | // SpriteAnim 44 | } 45 | else if (animator != null) 46 | { 47 | // GIFAnimator 48 | if (_hasStateChanged) 49 | animator.PlayType(open ? "openDoor" : "closeDoor"); 50 | spriteRenderer.enabled = true; 51 | } 52 | else 53 | { 54 | // SpriteRenderer 55 | spriteRenderer.enabled = !open; 56 | } 57 | 58 | // Triggers 59 | if (_hasStateChanged) 60 | { 61 | var triggerID = open ? "onOpen" : "onClose"; 62 | TriggerSignal signal = new(__instance.gameObject, triggerID, PlayerControl.LocalPlayer); 63 | TriggerSystem.GetInstance().FireTrigger(signal); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/AmbientSoundBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.AssetLoader; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Builders; 7 | 8 | internal class AmbientSoundBuilder : IElemBuilder 9 | { 10 | private const string AMBIENT_SOUND_TYPE = "util-sound1"; 11 | private const string TRIGGER_SOUND_TYPE = "util-triggersound"; 12 | 13 | public void OnBuild(LIElement elem, GameObject obj) 14 | { 15 | var isAmbient = elem.type == AMBIENT_SOUND_TYPE; 16 | var isTrigger = elem.type == TRIGGER_SOUND_TYPE; 17 | if (!isAmbient && !isTrigger) 18 | return; 19 | 20 | // Colliders 21 | Collider2D[] colliders = obj.GetComponentsInChildren(); 22 | foreach (var collider in colliders) 23 | collider.isTrigger = true; 24 | 25 | // AudioClip 26 | if (elem.properties.sounds == null) 27 | { 28 | LILogger.Warn($"{elem.name} missing audio listing"); 29 | return; 30 | } 31 | 32 | if (elem.properties.sounds.Length <= 0) 33 | { 34 | LILogger.Warn($"{elem.name} missing audio elements"); 35 | return; 36 | } 37 | 38 | // Sound Data 39 | if (elem.properties.sounds.Length == 0) 40 | { 41 | LILogger.Warn($"{elem.name} missing audio data"); 42 | return; 43 | } 44 | 45 | var soundData = elem.properties.sounds[0]; 46 | if (soundData.dataID == null) 47 | { 48 | LILogger.Warn($"{elem.name} missing audio data ID"); 49 | return; 50 | } 51 | 52 | // Sound Player 53 | if (isAmbient) 54 | { 55 | var ambientPlayer = obj.AddComponent(); 56 | ambientPlayer.HitAreas = colliders; 57 | ambientPlayer.MaxVolume = soundData?.volume ?? 1f; 58 | 59 | // Load asynchronously 60 | AudioLoader.LoadAsync( 61 | soundData?.dataID ?? Guid.Empty, 62 | clip => ambientPlayer.AmbientSound = clip); 63 | } 64 | else if (isTrigger) 65 | { 66 | var triggerPlayer = obj.AddComponent(); 67 | triggerPlayer.Init(soundData, colliders); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /LevelImposter/Shop/Patches/VersionPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HarmonyLib; 3 | using LevelImposter.Core; 4 | using TMPro; 5 | using UnityEngine; 6 | 7 | namespace LevelImposter.Shop; 8 | 9 | /* 10 | * I swear to god if I find that 11 | * any of you patch over my logo 12 | * I will swallow your entire house whole. 13 | */ 14 | [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] 15 | public static class VersionPatch 16 | { 17 | private static Sprite? _logoSprite; 18 | 19 | public static GameObject? VersionObject { get; private set; } 20 | 21 | public static void Postfix() 22 | { 23 | if (!GameState.IsInMainMenu) 24 | return; 25 | 26 | var antiPiracy = Guid.NewGuid().ToString(); 27 | 28 | VersionObject = new GameObject("LevelImposterVersion " + antiPiracy); 29 | VersionObject.transform.localScale = new Vector3(0.55f, 0.55f, 1.0f); 30 | VersionObject.layer = (int)Layer.UI; 31 | 32 | var logoPosition = VersionObject.AddComponent(); 33 | logoPosition.Alignment = AspectPosition.EdgeAlignments.Right; 34 | logoPosition.DistanceFromEdge = new Vector3(1.8f, -2.3f, 4.5f); 35 | logoPosition.AdjustPosition(); 36 | 37 | var logoRenderer = VersionObject.AddComponent(); 38 | logoRenderer.sprite = GetLogoSprite(); 39 | 40 | GameObject logoTextObj = new("LevelImposterText " + antiPiracy); 41 | logoTextObj.transform.SetParent(VersionObject.transform); 42 | logoTextObj.transform.localPosition = new Vector3(3.2f, 0, 0); 43 | 44 | var logoTransform = logoTextObj.AddComponent(); 45 | logoTransform.sizeDelta = new Vector2(2, 0.19f); 46 | 47 | var logoText = logoTextObj.AddComponent(); 48 | logoText.fontSize = 1.5f; 49 | logoText.alignment = TextAlignmentOptions.BottomLeft; 50 | logoText.raycastTarget = false; 51 | logoText.SetText("v" + LevelImposter.DisplayVersion); 52 | } 53 | 54 | private static Sprite GetLogoSprite() 55 | { 56 | if (_logoSprite == null) 57 | _logoSprite = MapUtils.LoadSpriteResource("LevelImposterLogo.png"); 58 | if (_logoSprite == null) 59 | throw new Exception("The \"LevelImposterLogo.png\" resource was not found in assembly"); 60 | return _logoSprite; 61 | } 62 | } -------------------------------------------------------------------------------- /LevelImposter/AssetLoader/SpriteLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.AssetLoader; 7 | 8 | public class SpriteLoader : AsyncQueue 9 | { 10 | private SpriteLoader() 11 | { 12 | } 13 | 14 | public static SpriteLoader Instance { get; } = new(); 15 | 16 | /// 17 | /// Simplified shorthand to load a sprite asynchronously. 18 | /// 19 | /// ID of the sprite 20 | /// Data store to load from 21 | /// Callback when the sprite is loaded 22 | public static void LoadAsync(string id, IDataStore dataStore, Action onLoad) 23 | { 24 | var loadableTexture = new LoadableTexture(id, dataStore); 25 | var loadableSprite = new LoadableSprite(id, loadableTexture); 26 | Instance.AddToQueue(loadableSprite, loadedSprite => onLoad(loadedSprite)); 27 | } 28 | 29 | /// 30 | /// Simplified shorthand to load a sprite synchronously. 31 | /// 32 | /// Loadable sprite data 33 | /// Loaded sprite 34 | public static LoadedSprite LoadSync(LoadableSprite sprite) 35 | { 36 | return Instance.LoadImmediate(sprite); 37 | } 38 | 39 | protected override LoadedSprite Load(LoadableSprite loadable) 40 | { 41 | // Load the texture 42 | var loadedTexture = TextureLoader.LoadSync(loadable.Texture); 43 | var texture = loadedTexture.Texture; 44 | 45 | // Generate Sprite 46 | var options = loadable.Options; 47 | var sprite = Sprite.Create( 48 | texture, 49 | options.Frame ?? new Rect(0, 0, texture.width, texture.height), 50 | options.Pivot ?? new Vector2(0.5f, 0.5f), 51 | 100.0f, 52 | 0, 53 | SpriteMeshType.FullRect 54 | ); 55 | 56 | // Set Sprite Flags 57 | sprite.name = $"{loadable.ID}_sprite"; 58 | sprite.hideFlags = HideFlags.DontUnloadUnusedAsset; 59 | 60 | // Register in GC 61 | if (options.AddToGC) 62 | GCHandler.Register(sprite); 63 | 64 | // Return Loaded Sprite 65 | return new LoadedSprite(sprite, loadedTexture); 66 | } 67 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Utils/RandomizerSync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Reactor.Networking.Attributes; 3 | using Random = UnityEngine.Random; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// A randomizer class that is synced across all clients 9 | /// 10 | public static class RandomizerSync 11 | { 12 | private static int _randomSeed; 13 | 14 | /// 15 | /// Gets a random value based on an element GUID. 16 | /// Given the same GUID, weight, and seed will always return with the same value. 17 | /// Random seeds are synchronized it across all clients and 18 | /// regenerated with MapUtils.SyncRandomSeed() 19 | /// 20 | /// GUID identifier 21 | /// A weight value to generate new numbers with the same GUID 22 | /// A random float between 0.0 and 1.0 (inclusive) 23 | public static float GetRandom(Guid id, int weight = 0) 24 | { 25 | // Generate a new seed based on the GUID and weight 26 | var trueSeed = id.GetHashCode() + _randomSeed + weight; 27 | Random.InitState(trueSeed); 28 | 29 | // Generate a random value 30 | var randomValue = Random.value; 31 | 32 | // Reset the seed to a pseudo-random value to avoid predictability 33 | Random.InitState((int)DateTime.Now.Ticks); 34 | 35 | return randomValue; 36 | } 37 | 38 | /// 39 | /// Generates a new random seed and 40 | /// synchronizes it across all clients. 41 | /// 42 | public static void SyncRandomSeed() 43 | { 44 | var isConnected = AmongUsClient.Instance.AmConnected; 45 | var isHost = AmongUsClient.Instance.AmHost; 46 | if (isConnected && (!isHost || PlayerControl.LocalPlayer == null)) 47 | return; 48 | Random.InitState((int)DateTime.Now.Ticks); 49 | var newSeed = Random.RandomRange(int.MinValue, int.MaxValue); 50 | if (isConnected) 51 | RPCSyncRandomSeed(PlayerControl.LocalPlayer, newSeed); 52 | else 53 | _randomSeed = newSeed; 54 | } 55 | 56 | [MethodRpc((uint)LIRpc.SyncRandomSeed)] 57 | private static void RPCSyncRandomSeed(PlayerControl _, int randomSeed) 58 | { 59 | LILogger.Info($"[RPC] New random seed set: {randomSeed}"); 60 | _randomSeed = randomSeed; 61 | } 62 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Util/StarfieldBuilder.cs: -------------------------------------------------------------------------------- 1 | using LevelImposter.Core; 2 | using LevelImposter.DB; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Builders; 6 | 7 | internal class StarfieldBuilder : IElemBuilder 8 | { 9 | public void OnBuild(LIElement elem, GameObject obj) 10 | { 11 | if (elem.type != "util-starfield") 12 | return; 13 | 14 | // Prefab 15 | var prefab = AssetDB.GetObject("dec-rock4"); 16 | if (prefab == null) 17 | return; 18 | var prefabRenderer = prefab.GetComponent(); 19 | 20 | // Sprite 21 | var spriteRenderer = obj.GetComponent(); 22 | if (spriteRenderer == null) 23 | LILogger.Warn($"{elem.name} missing a sprite"); 24 | else 25 | spriteRenderer.material = prefabRenderer.material; 26 | 27 | // Star Prefab 28 | var starPrefab = Object.Instantiate(obj); 29 | starPrefab.transform.localScale = Vector3.one; 30 | starPrefab.transform.localRotation = Quaternion.identity; 31 | var prefabComp = starPrefab.AddComponent(); 32 | 33 | var count = elem.properties.starfieldCount ?? 20; 34 | var liStars = new LIStar[count]; 35 | for (var i = 0; i < count; i++) 36 | { 37 | var liStar = Object.Instantiate(prefabComp, obj.transform); 38 | liStar.Init(elem); 39 | liStars[i] = liStar; 40 | } 41 | 42 | Object.Destroy(starPrefab); 43 | 44 | // Load Sprite 45 | SpriteBuilder.OnSpriteLoad += (loadedElem, _) => 46 | { 47 | if (loadedElem.id != elem.id) 48 | return; 49 | 50 | foreach (var liStar in liStars) 51 | { 52 | var starRenderer = liStar.GetComponent(); 53 | starRenderer.sprite = spriteRenderer?.sprite; 54 | starRenderer.color = spriteRenderer?.color ?? starRenderer.color; 55 | } 56 | }; 57 | 58 | // Disable SpriteRenderers 59 | SpriteRenderer[] spriteRenderers = obj.GetComponents(); 60 | foreach (var renderer in spriteRenderers) 61 | renderer.enabled = false; 62 | 63 | // Disable Colliders 64 | Collider2D[] colliders = obj.GetComponents(); 65 | foreach (var collider in colliders) 66 | collider.enabled = false; 67 | } 68 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/PlayerArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Il2CppInterop.Runtime.Attributes; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Core; 7 | 8 | /// 9 | /// Object that tracks the players that are inside it's range 10 | /// 11 | public class PlayerArea(IntPtr intPtr) : MonoBehaviour(intPtr) 12 | { 13 | [HideFromIl2Cpp] public List? CurrentPlayersIDs { get; private set; } = new(); 14 | 15 | public bool IsLocalPlayerInside { get; private set; } 16 | 17 | public void OnDestroy() 18 | { 19 | CurrentPlayersIDs = null; 20 | } 21 | 22 | public void OnTriggerEnter2D(Collider2D collider) 23 | { 24 | var player = collider.GetComponent(); 25 | if (player == null) 26 | return; 27 | 28 | CurrentPlayersIDs?.Add(player.PlayerId); 29 | if (player.AmOwner) 30 | IsLocalPlayerInside = true; 31 | 32 | if (enabled) 33 | OnPlayerEnter(player); 34 | } 35 | 36 | public void OnTriggerExit2D(Collider2D collider) 37 | { 38 | var player = collider.GetComponent(); 39 | if (player == null) 40 | return; 41 | 42 | CurrentPlayersIDs?.RemoveAll(id => id == player.PlayerId); 43 | if (player.AmOwner) 44 | IsLocalPlayerInside = false; 45 | 46 | if (enabled) 47 | OnPlayerExit(player); 48 | } 49 | 50 | /// 51 | /// Gets a player by it's ID 52 | /// 53 | /// Player ID to search 54 | /// The cooresponding PlayerControl or null if it can't be found 55 | public PlayerControl? GetPlayer(byte playerID) 56 | { 57 | foreach (var player in PlayerControl.AllPlayerControls) 58 | if (player.PlayerId == playerID) 59 | return player; 60 | return null; 61 | } 62 | 63 | /// 64 | /// Called when a player enters the collider 65 | /// 66 | /// Player that entered the collider 67 | public virtual void OnPlayerEnter(PlayerControl player) 68 | { 69 | } 70 | 71 | /// 72 | /// Called when a player exits the collider 73 | /// 74 | /// Player that exited the collider 75 | public virtual void OnPlayerExit(PlayerControl player) 76 | { 77 | } 78 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Components/LIScroll.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LevelImposter.Builders; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Object that scrolls in a repeating pattern 9 | /// 10 | public class LIScroll(IntPtr intPtr) : MonoBehaviour(intPtr) 11 | { 12 | private static readonly int MainTex = Shader.PropertyToID("_MainTex"); 13 | 14 | private Material? _mat; 15 | private float _xSpeed = 1.0f; 16 | private float _ySpeed; 17 | 18 | public void Awake() 19 | { 20 | // Get Object Data 21 | var objData = gameObject.GetLIData(); 22 | if (objData == null) 23 | throw new Exception("Missing MapObjectData"); 24 | 25 | // Get Scrolling Speeds 26 | _xSpeed = objData.Element.properties.scrollingXSpeed ?? _xSpeed; 27 | _ySpeed = objData.Element.properties.scrollingYSpeed ?? _ySpeed; 28 | 29 | // Set Layer 30 | //gameObject.layer = (int)Layer.Ship; 31 | 32 | 33 | // Replace SpriteRenderer with MeshRenderer because 34 | // Unity doesn't support scrolling textures on sprites 35 | SpriteBuilder.OnSpriteLoad += (loadedElem, _) => 36 | { 37 | if (loadedElem.id != objData.ID) 38 | return; 39 | ReplaceRenderer(); 40 | }; 41 | } 42 | 43 | public void Update() 44 | { 45 | var t = Time.time; 46 | _mat?.SetTextureOffset(MainTex, new Vector2( 47 | t * _xSpeed, 48 | t * -_ySpeed 49 | )); 50 | } 51 | 52 | public void OnDestroy() 53 | { 54 | _mat = null; 55 | } 56 | 57 | /// 58 | /// Replaces the SpriteRenderer with a MeshRenderer 59 | /// to support scrolling textures 60 | /// 61 | private void ReplaceRenderer() 62 | { 63 | // Delete Sprite Renderer 64 | var spriteRenderer = GetComponent(); 65 | var tex = spriteRenderer.sprite.texture; 66 | 67 | _mat = spriteRenderer.material; 68 | 69 | // Texture Wrapping 70 | tex.wrapMode = TextureWrapMode.Repeat; 71 | 72 | // Create Material 73 | _mat = new Material(Shader.Find("Unlit/Transparent")) 74 | { 75 | name = $"{gameObject.name}_scrollmat", 76 | mainTexture = tex, 77 | hideFlags = HideFlags.HideAndDontSave 78 | }; 79 | spriteRenderer.material = _mat; 80 | GCHandler.Register(_mat); 81 | } 82 | } -------------------------------------------------------------------------------- /LevelImposter/Builders/Generic/ColliderBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Il2CppSystem.Collections.Generic; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.Builders; 7 | 8 | /// 9 | /// Configures the Collider2D on the GameObject 10 | /// 11 | public class ColliderBuilder : IElemBuilder 12 | { 13 | private static readonly string[] SHADOW_ONLY_TYPES = 14 | { 15 | "util-onewaycollider" 16 | }; 17 | 18 | public void OnPreBuild(LIElement elem, GameObject obj) 19 | { 20 | if (elem.properties.colliders == null) 21 | return; 22 | 23 | // Iterate through colliders 24 | foreach (var colliderData in elem.properties.colliders) 25 | { 26 | // Shadow Object 27 | if (colliderData.blocksLight) 28 | { 29 | GameObject shadowObj = new("Shadow " + colliderData.id); 30 | shadowObj.transform.SetParent(obj.transform); 31 | shadowObj.transform.localPosition = Vector3.zero; 32 | shadowObj.transform.localRotation = Quaternion.Euler(Vector3.zero); 33 | shadowObj.transform.localScale = Vector3.one; 34 | shadowObj.layer = (int)Layer.Shadow; 35 | 36 | var shadowCollider = shadowObj.AddComponent(); 37 | shadowCollider.SetPoints(GetPoints(colliderData, colliderData.isSolid)); 38 | } 39 | 40 | // Shadow Only 41 | if (SHADOW_ONLY_TYPES.Contains(elem.type)) 42 | continue; 43 | 44 | // PolygonCollider2D 45 | if (colliderData.isSolid) 46 | { 47 | var collider = obj.AddComponent(); 48 | collider.pathCount = 1; 49 | collider.SetPath(0, GetPoints(colliderData)); 50 | } 51 | // EdgeCollider2D 52 | else 53 | { 54 | var collider = obj.AddComponent(); 55 | collider.SetPoints(GetPoints(colliderData)); 56 | } 57 | } 58 | } 59 | 60 | private List GetPoints(LICollider collider, bool wrap = false) 61 | { 62 | var list = new List(collider.points.Length); 63 | foreach (var point in collider.points) 64 | list.Add(new Vector2(point.x, -point.y)); 65 | if (wrap && list.Count > 0) 66 | list.Add(list[0]); 67 | return list; 68 | } 69 | } -------------------------------------------------------------------------------- /LevelImposter/DB/Sub/SoundDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using LevelImposter.Core; 4 | using UnityEngine; 5 | 6 | namespace LevelImposter.DB; 7 | 8 | /// 9 | /// Database of Among Us AudioClips 10 | /// 11 | public class SoundDB(SerializedAssetDB serializedDB) : SubDB(serializedDB) 12 | { 13 | public override void LoadShip(ShipStatus shipStatus, MapType mapType) 14 | { 15 | DB.SoundDB.ForEach(elem => 16 | { 17 | if (elem.MapType != mapType) 18 | return; 19 | 20 | // Transform 21 | var transform = shipStatus.transform.Find(elem.Path); 22 | if (transform == null) 23 | { 24 | LILogger.Warn($"SoundDB could not find {elem.ID} in {shipStatus.name}"); 25 | return; 26 | } 27 | 28 | // Components 29 | var shipRoom = transform.gameObject.GetComponent(); 30 | var stepWatcher = transform.gameObject.GetComponent(); 31 | AudioClip? audioClip = null; 32 | if (shipRoom != null) 33 | { 34 | audioClip = SearchSoundGroup(shipRoom.FootStepSounds, elem.Name); 35 | } 36 | else if (stepWatcher != null) 37 | { 38 | audioClip = SearchSoundGroup(stepWatcher.Sounds, elem.Name); 39 | } 40 | else 41 | { 42 | LILogger.Warn($"SoundDB could not find component for {elem.ID}"); 43 | return; 44 | } 45 | 46 | // AudioClip 47 | if (audioClip == null) 48 | { 49 | LILogger.Warn($"SoundDB could not find AudioClip for {elem.ID}"); 50 | return; 51 | } 52 | 53 | Add(elem.ID, audioClip); 54 | }); 55 | } 56 | 57 | private AudioClip? SearchSoundGroup(SoundGroup? soundGroup, string name) 58 | { 59 | if (soundGroup == null) 60 | return null; 61 | foreach (var clip in soundGroup.Clips) 62 | if (clip.name == name) 63 | return clip; 64 | 65 | return null; 66 | } 67 | 68 | [Serializable] 69 | public class DBElement 70 | { 71 | public string ID { get; set; } 72 | public string Path { get; set; } 73 | public string Name { get; set; } 74 | public int Map { get; set; } 75 | 76 | [JsonIgnore] public MapType MapType => (MapType)Map; 77 | } 78 | } -------------------------------------------------------------------------------- /LevelImposter/Core/Patches/Ship/BinocularsPatch.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using LevelImposter.Builders; 3 | using UnityEngine; 4 | 5 | namespace LevelImposter.Core; 6 | 7 | /// 8 | /// Normally, binoculars (util-cams4) are handled by 9 | /// FungleShipStatus. This bypasses that 10 | /// dependency by supplying it's own data. 11 | /// 12 | [HarmonyPatch(typeof(FungleSurveillanceMinigame), nameof(FungleSurveillanceMinigame.Begin))] 13 | public static class BinocularsPatch 14 | { 15 | public static void Prefix(FungleSurveillanceMinigame __instance) 16 | { 17 | if (!LIShipStatus.IsInstance()) 18 | return; 19 | 20 | // Create a temporary room to prevent System.InvalidOperationException 21 | var tempRoom = __instance.gameObject.AddComponent(); 22 | tempRoom.RoomId = SystemTypes.MeetingRoom; 23 | ShipStatus.Instance.AllRooms = MapUtils.AddToArr(ShipStatus.Instance.AllRooms, tempRoom); 24 | } 25 | 26 | public static void Postfix(FungleSurveillanceMinigame __instance) 27 | { 28 | if (!LIShipStatus.IsInstance()) 29 | return; 30 | 31 | // Remove the temporary room 32 | var arr = ShipStatus.Instance.AllRooms; 33 | ShipStatus.Instance.AllRooms = MapUtils.RemoveFromArr(arr, arr.Count - 1); 34 | 35 | // Fix Security Camera Position 36 | var lastPos = BinocularsBuilder.LastBinocularsPos; 37 | if (lastPos != Vector2.zero) 38 | __instance.securityCamera.transform.localPosition = new Vector3(lastPos.x, lastPos.y, 50f); 39 | else 40 | __instance.securityCamera.transform.position = 41 | BinocularsBuilder.CameraOffset + __instance.transform.position; 42 | 43 | // Fix Camera Properties 44 | var camera = __instance.securityCamera.cam; 45 | camera.clearFlags = CameraClearFlags.SolidColor; 46 | camera.backgroundColor = Camera.main.backgroundColor; 47 | camera.orthographicSize = BinocularsBuilder.OrthographicSize; 48 | } 49 | } 50 | 51 | [HarmonyPatch(typeof(FungleSurveillanceMinigame), nameof(FungleSurveillanceMinigame.Close))] 52 | public static class BinocularsClosePatch 53 | { 54 | public static void Postfix(FungleSurveillanceMinigame __instance) 55 | { 56 | if (!LIShipStatus.IsInstance()) 57 | return; 58 | 59 | // Set Last Camera Position 60 | BinocularsBuilder.LastBinocularsPos = new Vector2( 61 | __instance.securityCamera.transform.localPosition.x, 62 | __instance.securityCamera.transform.localPosition.y 63 | ); 64 | } 65 | } --------------------------------------------------------------------------------