diff --git a/.vscode/settings.json b/.vscode/settings.json index cddac42..28c1f22 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "editor.codeLens": false + "editor.codeLens": false, + "dotnet.defaultSolution": "LevelImposter.sln" } \ No newline at end of file diff --git a/LevelImposter/Assets/SerializedAssetDB.json b/LevelImposter/Assets/SerializedAssetDB.json index 371a6f5..e5abf4d 100644 --- a/LevelImposter/Assets/SerializedAssetDB.json +++ b/LevelImposter/Assets/SerializedAssetDB.json @@ -25,6 +25,11 @@ "Map": 2, "Path": "Office/panel_vitals" }, + { + "ID": "util-spore", + "Map": 5, + "Path": "Outside/OutsideJungle/Mushrooms/Mushroom (1)" + }, { "ID": "util-computer", "Map": 2, @@ -40,6 +45,11 @@ "Map": 2, "Path": "Office/OfficeVent" }, + { + "ID": "util-vent3", + "Map": 5, + "Path": "Rooms/Kitchen/Vents/KitchenVent" + }, { "ID": "util-cam", "Map": 2, @@ -60,6 +70,11 @@ "Map": 4, "Path": "Security/task_cams" }, + { + "ID": "util-cams4", + "Map": 5, + "Path": "lookout_binoculars" + }, { "ID": "util-ladder1", "Map": 4, @@ -755,6 +770,106 @@ "Map": 4, "Path": "Electrical/task_calibrate" }, + { + "ID": "task-marshmallow1", + "Map": 5, + "Path": "Outside/OutsideBeach/TaskConsoles/RoastMarshmallow Stick (1)/RoastMarshmallowStickConsole" + }, + { + "ID": "task-marshmallow2", + "Map": 5, + "Path": "Outside/OutsideBeach/TaskConsoles/RoastMarshmallow Fire/RoastMarshmallowFireConsole" + }, + { + "ID": "task-sandcastle", + "Map": 5, + "Path": "Rooms/RecRoom/TaskConsoles/BuildSandcastle/BuildSandcastleConsole" + }, + { + "ID": "task-weights", + "Map": 5, + "Path": "Rooms/RecRoom/TaskConsoles/LiftWeights/LiftWeightsConsole" + }, + { + "ID": "task-frisbee", + "Map": 5, + "Path": "Rooms/RecRoom/TaskConsoles/TestFrisbee/TestFrisbeeConsole" + }, + { + "ID": "task-generator", + "Map": 5, + "Path": "Rooms/Kitchen/TaskConsoles/CrankGenerator (1a)/CrankGeneratorConsole" + }, + { + "ID": "task-fish1", + "Map": 5, + "Path": "Rooms/Dock/TaskConsoles/CatchFish (1)/CatchFishConsole" + }, + { + "ID": "task-fish2", + "Map": 5, + "Path": "Rooms/Kitchen/TaskConsoles/CatchFish (2)/CatchFishConsole" + }, + { + "ID": "task-seashells", + "Map": 5, + "Path": "Outside/OutsideBeach/TaskConsoles/CollectShells (1)/CollectShellsConsole" + }, + { + "ID": "task-egg1", + "Map": 5, + "Path": "Outside/OutsideJungle/TaskConsoles/HelpCritter (1b)/HelpCritterConsole" + }, + { + "ID": "task-egg2", + "Map": 5, + "Path": "Rooms/Laboratory/TaskConsoles/HelpCritter (2)/HelpCritterConsole" + }, + { + "ID": "task-samples1", + "Map": 5, + "Path": "Outside/OutsideJungle/TaskConsoles/CollectSamples (1a)/CollectSamplesConsole" + }, + { + "ID": "task-samples2", + "Map": 5, + "Path": "Rooms/Laboratory/TaskConsoles/CollectSamples (2)/CollectSamplesConsole" + }, + { + "ID": "task-vegetables1", + "Map": 5, + "Path": "Rooms/Greenhouse/TaskConsoles/CollectVegetables (1)/CollectVegetablesConsole" + }, + { + "ID": "task-vegetables2", + "Map": 5, + "Path": "Rooms/Kitchen/TaskConsoles/CollectVegetables (2)/CollectVegetablesConsole" + }, + { + "ID": "task-radio", + "Map": 5, + "Path": "Rooms/Communications/TaskConsoles/TuneRadio/TuneRadioConsole" + }, + { + "ID": "task-mineores", + "Map": 5, + "Path": "Rooms/MiningPit/TaskConsoles/MineOres/MineOresConsole" + }, + { + "ID": "task-replaceparts1", + "Map": 5, + "Path": "Rooms/UpperEngine/TaskConsoles/ReplaceParts (1b)/ReplacePartsConsole" + }, + { + "ID": "task-replaceparts2", + "Map": 5, + "Path": "Rooms/DropshipRuins/TaskConsoles/ReplaceParts (2b)/ReplacePartsConsole" + }, + { + "ID": "task-hoist", + "Map": 5, + "Path": "Outside/OutsideJungle/TaskConsoles/HoistSupplies (1)/HoistSuppliesConsole" + }, { "ID": "ss-skeld", "Map": 0, @@ -775,6 +890,11 @@ "Map": 4, "Path": "" }, + { + "ID": "ss-fungle", + "Map": 5, + "Path": "" + }, { "ID": "dec-barrier", "Map": 2, @@ -1413,6 +1533,90 @@ "Type": 2, "Name": "CalibrateDistributorTask" }, + { + "ID": "task-marshmallow1", + "Map": 5, + "Type": 0, + "Name": "RoastMarshmallowTask" + }, + { + "ID": "task-sandcastle", + "Map": 5, + "Type": 1, + "Name": "BuildSandcastleTask" + }, + { + "ID": "task-weights", + "Map": 5, + "Type": 1, + "Name": "LiftWeightsTask" + }, + { + "ID": "task-frisbee", + "Map": 5, + "Type": 1, + "Name": "TestFrisbeeTask" + }, + { + "ID": "task-generator", + "Map": 5, + "Type": 1, + "Name": "CrankGeneratorTask" + }, + { + "ID": "task-fish1", + "Map": 5, + "Type": 2, + "Name": "CatchFishTask" + }, + { + "ID": "task-seashells", + "Map": 5, + "Type": 1, + "Name": "CollectShellsTask" + }, + { + "ID": "task-egg1", + "Map": 5, + "Type": 2, + "Name": "HelpCritterTask" + }, + { + "ID": "task-samples1", + "Map": 5, + "Type": 0, + "Name": "CollectSamplesTask" + }, + { + "ID": "task-vegetables1", + "Map": 5, + "Type": 2, + "Name": "CollectVegetablesTask" + }, + { + "ID": "task-radio", + "Map": 5, + "Type": 1, + "Name": "TuneRadioTask" + }, + { + "ID": "task-mineores", + "Map": 5, + "Type": 2, + "Name": "MineOresTask" + }, + { + "ID": "task-replaceparts1", + "Map": 5, + "Type": 0, + "Name": "ReplacePartsTask" + }, + { + "ID": "task-hoist", + "Map": 5, + "Type": 2, + "Name": "HoistSuppliesTask" + }, { "ID": "sab-electric", "Map": 2, @@ -1448,6 +1652,12 @@ "Map": 0, "Type": 3, "Name": "NoOxyTask" + }, + { + "ID": "sab-btnmixup", + "Map": 5, + "Type": 3, + "Name": "MushroomMixupSabotageTask" } ], "SoundDB": [ @@ -2352,6 +2562,14 @@ "ID": "task-id_walletmask", "Paths": [ "admin_sliderBottom/admin_Wallet/admin_walletFront" ] }, + { + "ID": "task-id_greenlight", + "Paths": [ "admin_sliderTop/admin_lightGreen" ] + }, + { + "ID": "task-id_redlight", + "Paths": [ "admin_sliderTop/admin_lightRed" ] + }, { "ID": "task-keys_keychain", "Paths": [ "panel_key_keychain" ] @@ -2493,6 +2711,22 @@ "ID": "task-pass_scanner", "Paths": [ "Panel_scanner" ] }, + { + "ID": "task-pass_pull", + "Paths": [ "Panel_boardingpull" ] + }, + { + "ID": "task-pass_flip", + "Paths": [ "panel_boardingFront/panel_boardingFlip" ] + }, + { + "ID": "task-pass_idwindow", + "Paths": [ "panel_boardingFront/panel_boardingpicBG/panel_boardingpicwindow" ] + }, + { + "ID": "task-pass_idfront", + "Paths": [ "panel_boardingFront" ] + }, { "ID": "task-photos_bg", "Paths": [ "photos_bucket" ] diff --git a/LevelImposter/Builders/BuildRouter.cs b/LevelImposter/Builders/BuildRouter.cs index 4b73568..48736ea 100644 --- a/LevelImposter/Builders/BuildRouter.cs +++ b/LevelImposter/Builders/BuildRouter.cs @@ -30,8 +30,10 @@ public class BuildRouter new SabotageOptionsBuilder(), new OneWayColliderBuilder(), new DecontaminationBuilder(), + new SporeBuilder(), new SabBuilder(), + new SabMixupBuilder(), new SabConsoleBuilder(), new SabMapBuilder(), new SabDoorBuilder(), diff --git a/LevelImposter/Builders/Generic/SpriteBuilder.cs b/LevelImposter/Builders/Generic/SpriteBuilder.cs index 19cbe8f..7c4a3b4 100644 --- a/LevelImposter/Builders/Generic/SpriteBuilder.cs +++ b/LevelImposter/Builders/Generic/SpriteBuilder.cs @@ -11,7 +11,7 @@ public class SpriteBuilder : IElemBuilder { public void Build(LIElement elem, GameObject obj) { - if (elem.properties.spriteData == null) + if (elem.properties.spriteID == null) return; if (SpriteLoader.Instance == null) throw new Exception("SpriteLoader has not initialized"); diff --git a/LevelImposter/Builders/Sab/SabBuilder.cs b/LevelImposter/Builders/Sab/SabBuilder.cs index 0fbba50..5498abe 100644 --- a/LevelImposter/Builders/Sab/SabBuilder.cs +++ b/LevelImposter/Builders/Sab/SabBuilder.cs @@ -1,7 +1,7 @@ +using LevelImposter.Core; +using LevelImposter.DB; using System.Collections.Generic; using UnityEngine; -using LevelImposter.DB; -using LevelImposter.Core; namespace LevelImposter.Builders { @@ -27,7 +27,9 @@ public SabBuilder() public void Build(LIElement elem, GameObject obj) { - if (!elem.type.StartsWith("sab-") || elem.type.StartsWith("sab-btn") || elem.type.StartsWith("sab-door")) + if (!elem.type.StartsWith("sab-") || + elem.type.StartsWith("sab-btn") || + elem.type.StartsWith("sab-door")) return; // ShipStatus @@ -42,7 +44,7 @@ public void Build(LIElement elem, GameObject obj) _sabContainer.transform.SetParent(shipStatus.transform); _sabContainer.SetActive(false); } - + // Prefab var prefabTask = AssetDB.GetTask(elem.type); if (prefabTask == null) @@ -82,7 +84,7 @@ public void Build(LIElement elem, GameObject obj) bool hasSabSystem = SAB_SYSTEMS.TryGetValue(elem.type, out SystemTypes sabSystemType); if (!hasSabSystem) return; - + // Remove Old System var oldSystem = shipStatus.Systems[sabSystemType].Cast(); var sabSystem = shipStatus.Systems[SystemTypes.Sabotage].Cast(); diff --git a/LevelImposter/Builders/Sab/SabDoorBuilder.cs b/LevelImposter/Builders/Sab/SabDoorBuilder.cs index 494740b..aadf2fa 100644 --- a/LevelImposter/Builders/Sab/SabDoorBuilder.cs +++ b/LevelImposter/Builders/Sab/SabDoorBuilder.cs @@ -1,9 +1,9 @@ -using UnityEngine; +using LevelImposter.Core; using LevelImposter.DB; using PowerTools; -using LevelImposter.Core; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using UnityEngine; namespace LevelImposter.Builders { @@ -59,7 +59,7 @@ public void Build(LIElement elem, GameObject obj) SpriteRenderer spriteRenderer = obj.GetComponent(); Animator animator = obj.AddComponent(); SpriteAnim spriteAnim = obj.AddComponent(); - obj.layer = (int)Layer.Ship; + obj.layer = (int)Layer.ShortObjects; // <-- Required for Decontamination Doors bool isSpriteAnim = false; if (!spriteRenderer) { @@ -113,11 +113,11 @@ public void Build(LIElement elem, GameObject obj) // Sound LISound? openSound = MapUtils.FindSound(elem.properties.sounds, OPEN_SOUND_NAME); if (openSound != null) - doorComponent.OpenSound = WAVFile.Load(openSound.data); + doorComponent.OpenSound = WAVFile.LoadSound(openSound); LISound? closeSound = MapUtils.FindSound(elem.properties.sounds, CLOSE_SOUND_NAME); if (closeSound != null) - doorComponent.CloseSound = WAVFile.Load(closeSound.data); + doorComponent.CloseSound = WAVFile.LoadSound(closeSound); // SpriteAnim if (isSpriteAnim) @@ -150,7 +150,7 @@ public void Build(LIElement elem, GameObject obj) } } - public void PostBuild() {} + public void PostBuild() { } /// /// Gets a door component by its element ID. diff --git a/LevelImposter/Builders/Sab/SabMapBuilder.cs b/LevelImposter/Builders/Sab/SabMapBuilder.cs index 3b822e7..1ae35a0 100644 --- a/LevelImposter/Builders/Sab/SabMapBuilder.cs +++ b/LevelImposter/Builders/Sab/SabMapBuilder.cs @@ -1,8 +1,8 @@ +using LevelImposter.Core; +using LevelImposter.DB; using System; using System.Collections.Generic; using UnityEngine; -using LevelImposter.DB; -using LevelImposter.Core; namespace LevelImposter.Builders { @@ -15,6 +15,7 @@ public class SabMapBuilder : IElemBuilder private Sprite? _oxygenBtnSprite = null; private Sprite? _doorsBtnSprite = null; private Sprite? _lightsBtnSprite = null; + private Sprite? _mixupBtnSprite = null; private Material? _btnMat = null; private bool _hasSabConsoles = false; @@ -51,7 +52,8 @@ public void Build(LIElement elem, GameObject obj) _doorsBtnSprite == null || _oxygenBtnSprite == null || _reactorBtnSprite == null || - _commsBtnSprite == null) + _commsBtnSprite == null || + _mixupBtnSprite == null) { LILogger.Warn("1 or more sabotage map sprites were not found"); return; @@ -107,7 +109,8 @@ public void Build(LIElement elem, GameObject obj) ButtonBehavior button = sabButton.AddComponent(); Action btnAction; Sprite btnSprite; - switch (elem.type) { + switch (elem.type) + { case "sab-btnreactor": btnSprite = _reactorBtnSprite; btnAction = mapRoom.SabotageReactor; @@ -128,6 +131,11 @@ public void Build(LIElement elem, GameObject obj) btnAction = mapRoom.SabotageLights; mapRoom.special = btnRenderer; break; + case "sab-btnmixup": + btnSprite = _mixupBtnSprite; + btnAction = mapRoom.SabotageMushroomMixup; + mapRoom.special = btnRenderer; + break; case "sab-btndoors": btnSprite = _doorsBtnSprite; btnAction = mapRoom.SabotageDoors; @@ -182,25 +190,38 @@ private void GetAllAssets() // Polus var polusShip = AssetDB.GetObject("ss-polus"); - var polusShipStatus = polusShip?.GetComponent(); - var polusOverlay = polusShipStatus?.MapPrefab.infectedOverlay; - if (polusOverlay == null) - return; + { + var polusShipStatus = polusShip?.GetComponent(); + var polusOverlay = polusShipStatus?.MapPrefab.infectedOverlay; + if (polusOverlay == null) + return; - _commsBtnSprite = GetSprite(polusOverlay, "Comms", "bomb"); // um...BOMB!? - _reactorBtnSprite = GetSprite(polusOverlay, "Laboratory", "meltdown"); - _doorsBtnSprite = GetSprite(polusOverlay, "Office", "Doors"); - _lightsBtnSprite = GetSprite(polusOverlay, "Electrical", "lightsOut"); - _btnMat = polusOverlay.transform.GetChild(0).GetChild(0).GetComponent().material; + _commsBtnSprite = GetSprite(polusOverlay, "Comms", "bomb"); // um...BOMB!? + _reactorBtnSprite = GetSprite(polusOverlay, "Laboratory", "meltdown"); + _doorsBtnSprite = GetSprite(polusOverlay, "Office", "Doors"); + _lightsBtnSprite = GetSprite(polusOverlay, "Electrical", "lightsOut"); + _btnMat = polusOverlay.transform.GetChild(0).GetChild(0).GetComponent().material; + } // Mira var miraShip = AssetDB.GetObject("ss-mira"); - var miraShipStatus = miraShip?.GetComponent(); - var miraOverlay = miraShipStatus?.MapPrefab.infectedOverlay; - if (miraOverlay == null) - return; + { + var miraShipStatus = miraShip?.GetComponent(); + var miraOverlay = miraShipStatus?.MapPrefab.infectedOverlay; + if (miraOverlay == null) + return; + _oxygenBtnSprite = GetSprite(miraOverlay, "LifeSupp", "bomb"); // Another bomb? + } - _oxygenBtnSprite = GetSprite(miraOverlay, "LifeSupp", "bomb"); // Another bomb? + // Fungle + var fungleShip = AssetDB.GetObject("ss-fungle"); + { + var fungleShipStatus = fungleShip?.GetComponent(); + var fungleOverlay = fungleShipStatus?.MapPrefab.infectedOverlay; + if (fungleOverlay == null) + return; + _mixupBtnSprite = GetSprite(fungleOverlay, "Jungle", "mushroomMixup"); + } } /// diff --git a/LevelImposter/Builders/Sab/SabMixupBuilder.cs b/LevelImposter/Builders/Sab/SabMixupBuilder.cs new file mode 100644 index 0000000..915a206 --- /dev/null +++ b/LevelImposter/Builders/Sab/SabMixupBuilder.cs @@ -0,0 +1,120 @@ +using LevelImposter.Core; +using LevelImposter.DB; +using UnityEngine; + +namespace LevelImposter.Builders +{ + public class SabMixupBuilder : IElemBuilder + { + private const SystemTypes MIXUP_TYPE = SystemTypes.MushroomMixupSabotage; + + private static MushroomMixupSabotageSystem? _sabotageSystem = null; + public static MushroomMixupSabotageSystem? SabotageSystem => _sabotageSystem; + + public void Build(LIElement elem, GameObject obj) + { + if (elem.type != "sab-btnmixup") + return; + + // Check Already Exists + if (_sabotageSystem != null) + { + LILogger.Warn("Only 1 mushroom mixup sabotage can exist at a time"); + return; + } + + // ShipStatus + var shipStatus = LIShipStatus.Instance?.ShipStatus; + if (shipStatus == null) + throw new MissingShipException(); + + // ShipStatus Prefab + var fungleShipStatus = AssetDB.GetObject("ss-fungle")?.GetComponent(); + if (fungleShipStatus == null) + return; + var prefabSystem = fungleShipStatus.specialSabotage; + + // Containers + var taskContainer = new GameObject("Mixup Sabotage Task"); + taskContainer.transform.SetParent(shipStatus.transform); + taskContainer.SetActive(false); + + var systemContainer = new GameObject("Mixup Sabotage System"); + systemContainer.transform.SetParent(shipStatus.transform); + + // Prefab + var prefabTask = AssetDB.GetTask(elem.type); + if (prefabTask == null) + return; + + // Create Task + LILogger.Info($" + Adding sabotage for {elem}..."); + MushroomMixupSabotageTask task = taskContainer.AddComponent(); + { + // Properties + task.StartAt = prefabTask.StartAt; + task.TaskType = prefabTask.TaskType; + task.MinigamePrefab = prefabTask.MinigamePrefab; + + // Rename Task + if (!string.IsNullOrEmpty(elem.properties.description)) + LIShipStatus.Instance?.Renames.Add(StringNames.MushroomMixupSabotage, elem.properties.description); + + // Add Task + shipStatus.SpecialTasks = MapUtils.AddToArr(shipStatus.SpecialTasks, task); + } + + // Screen Tint + GameObject screenTintObj = new GameObject("Screen Tint"); + { + screenTintObj.transform.SetParent(systemContainer.transform); + + // Mesh Filter + var tintFilter = screenTintObj.AddComponent(); + tintFilter.mesh = prefabSystem.screenTint.GetComponent().sharedMesh; + + // Mesh Renderer + var tintRenderer = screenTintObj.AddComponent(); + tintRenderer.sharedMaterial = prefabSystem.screenTint.GetComponent().sharedMaterial; + tintRenderer.enabled = false; + + // Full Screen Scaler + screenTintObj.AddComponent(); + + // Screen Tint + var screenTint = screenTintObj.AddComponent(); + screenTint.meshRenderer = tintRenderer; + screenTint.maxOpacity = prefabSystem.screenTint.maxOpacity; + + // Fix Null Reference Exception + screenTint.Awake(); + screenTint.enabled = true; + } + + // Create New System + _sabotageSystem = systemContainer.AddComponent(); + { + _sabotageSystem.skinEmptyChance = prefabSystem.skinEmptyChance; + _sabotageSystem.skinIds = prefabSystem.skinIds; + _sabotageSystem.hatEmptyChance = prefabSystem.hatEmptyChance; + _sabotageSystem.hatIds = prefabSystem.hatIds; + _sabotageSystem.visorEmptyChance = prefabSystem.visorEmptyChance; + _sabotageSystem.visorIds = prefabSystem.visorIds; + _sabotageSystem.petEmptyChance = prefabSystem.petEmptyChance; + _sabotageSystem.petIds = prefabSystem.petIds; + _sabotageSystem.secondsForAutoHeal = elem.properties.sabDuration ?? 10; + _sabotageSystem.screenTint = screenTintObj.GetComponent(); + _sabotageSystem.playerAnimationPrefab = prefabSystem.playerAnimationPrefab; + _sabotageSystem.activateSfx = prefabSystem.activateSfx; + _sabotageSystem.deactivateSfx = prefabSystem.deactivateSfx; + } + + // Add New System + var sabSystem = shipStatus.Systems[SystemTypes.Sabotage].Cast(); + shipStatus.Systems.Add(MIXUP_TYPE, _sabotageSystem.Cast()); + sabSystem.specials.Add(_sabotageSystem.Cast()); + } + + public void PostBuild() { } + } +} diff --git a/LevelImposter/Builders/Task/ShipTaskBuilder.cs b/LevelImposter/Builders/Task/ShipTaskBuilder.cs index 443684c..def79bf 100644 --- a/LevelImposter/Builders/Task/ShipTaskBuilder.cs +++ b/LevelImposter/Builders/Task/ShipTaskBuilder.cs @@ -1,8 +1,8 @@ +using LevelImposter.Core; +using LevelImposter.DB; using System; using System.Collections.Generic; using UnityEngine; -using LevelImposter.DB; -using LevelImposter.Core; namespace LevelImposter.Builders { @@ -15,11 +15,6 @@ public class ShipTaskBuilder { "Common", TaskLength.Common } }; - private readonly List WHITELIST_TYPES = new() - { - "task-node" - }; - private List _builtTypes = new(); private GameObject _taskParent = null; private NormalPlayerTask? _wiresTask = null; @@ -189,7 +184,7 @@ private void AddTaskToShip( if (taskLength == TaskLength.Common) shipStatus.CommonTasks = MapUtils.AddToArr(shipStatus.CommonTasks, task); if (taskLength == TaskLength.Short) - shipStatus.NormalTasks = MapUtils.AddToArr(shipStatus.NormalTasks, task); + shipStatus.ShortTasks = MapUtils.AddToArr(shipStatus.ShortTasks, task); if (taskLength == TaskLength.Long) shipStatus.LongTasks = MapUtils.AddToArr(shipStatus.LongTasks, task); } diff --git a/LevelImposter/Builders/Task/TaskConsoleBuilder.cs b/LevelImposter/Builders/Task/TaskConsoleBuilder.cs index 694f29b..17c887c 100644 --- a/LevelImposter/Builders/Task/TaskConsoleBuilder.cs +++ b/LevelImposter/Builders/Task/TaskConsoleBuilder.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; -using UnityEngine; using Il2CppInterop.Runtime.InteropTypes.Arrays; using LevelImposter.Core; +using System.Collections.Generic; +using UnityEngine; namespace LevelImposter.Builders { @@ -18,7 +18,14 @@ public class TaskConsoleBuilder { "task-fans2", 1 }, { "task-records1", 0 }, { "task-pistols1", 1 }, - { "task-pistols2", 1 } + { "task-pistols2", 1 }, + { "task-marshmallow2", 0 }, + { "task-vegetables1", 0 }, + { "task-vegetables2", 2 }, + { "task-egg1", 1 }, + { "task-fish1", 0 }, + { "task-fish2", 1 }, + { "task-replaceparts1", 0 } }; private static readonly Dictionary CONSOLE_ID_INCREMENTS = new() { @@ -30,7 +37,12 @@ public class TaskConsoleBuilder { "task-fuel2", 0 }, { "task-align1", 0 }, { "task-records2", 1 }, - { "task-wires", 0 } + { "task-wires", 0 }, + { "task-marshmallow1", 1 }, + { "task-egg1", 0 }, + { "task-samples1", 0 }, + { "task-replaceparts2", 1 }, + { "task-hoist", 0 } }; private Dictionary _consoleIDIncrements = new(CONSOLE_ID_INCREMENTS); @@ -104,7 +116,7 @@ public Console Build(LIElement elem, GameObject obj, GameObject prefab) console.Image = obj.GetComponent(); console.ConsoleId = GetConsoleID(elem.type); console.Room = RoomBuilder.GetParentOrDefault(elem); - console.TaskTypes = prefabConsole.TaskTypes; + console.TaskTypes = prefabConsole.TaskTypes; console.ValidTasks = GetConsoleTasks(elem.type, console.ConsoleId) ?? prefabConsole.ValidTasks; console.AllowImpostor = false; console.checkWalls = elem.properties.checkCollision ?? false; @@ -123,7 +135,7 @@ public Console Build(LIElement elem, GameObject obj, GameObject prefab) private int GetConsoleID(string type) { bool isTowels = type.StartsWith("task-towels"); - + if (CONSOLE_ID_PAIRS.ContainsKey(type)) { return CONSOLE_ID_PAIRS[type]; @@ -153,6 +165,7 @@ private int GetConsoleID(string type) bool isWaterJug = type == "task-waterjug2"; bool isFuel = type == "task-fuel1"; bool isFuelOutput = type == "task-fuel2"; + bool isHoist = type == "task-hoist"; if (isWaterJug) { @@ -186,6 +199,15 @@ private int GetConsoleID(string type) }; return new(new TaskSet[] { taskSet }); } + else if (isHoist) + { + TaskSet taskSet = new() + { + taskType = TaskTypes.HoistSupplies, + taskStep = new(consoleID, consoleID) + }; + return new(new TaskSet[] { taskSet }); + } return null; } diff --git a/LevelImposter/Builders/Trigger/TriggerAreaBuilder.cs b/LevelImposter/Builders/Trigger/TriggerAreaBuilder.cs index e052d20..0e2563e 100644 --- a/LevelImposter/Builders/Trigger/TriggerAreaBuilder.cs +++ b/LevelImposter/Builders/Trigger/TriggerAreaBuilder.cs @@ -15,6 +15,11 @@ public void Build(LIElement elem, GameObject obj) foreach (Collider2D collider in colliders) collider.isTrigger = true; + // Ghost + if (elem.properties.isGhostEnabled ?? false) + obj.layer = (int)Layer.Default; + + // Trigger Area LITriggerArea triggerArea = obj.AddComponent(); triggerArea.SetClientSide(elem.properties.triggerClientSide != false); } diff --git a/LevelImposter/Builders/Util/AmbientSoundBuilder.cs b/LevelImposter/Builders/Util/AmbientSoundBuilder.cs index dd05aa9..0078ffb 100644 --- a/LevelImposter/Builders/Util/AmbientSoundBuilder.cs +++ b/LevelImposter/Builders/Util/AmbientSoundBuilder.cs @@ -34,7 +34,7 @@ public void Build(LIElement elem, GameObject obj) // Sound Data LISound soundData = elem.properties.sounds[0]; - if (soundData.data == null) + if (soundData == null) { LILogger.Warn($"{elem.name} missing audio data"); return; @@ -46,7 +46,7 @@ public void Build(LIElement elem, GameObject obj) AmbientSoundPlayer ambientPlayer = obj.AddComponent(); ambientPlayer.HitAreas = colliders; ambientPlayer.MaxVolume = soundData.volume; - ambientPlayer.AmbientSound = WAVFile.Load(soundData?.data); + ambientPlayer.AmbientSound = WAVFile.LoadSound(soundData); } else if (isTrigger) { diff --git a/LevelImposter/Builders/Util/DecontaminationBuilder.cs b/LevelImposter/Builders/Util/DecontaminationBuilder.cs index b4752a5..399a76b 100644 --- a/LevelImposter/Builders/Util/DecontaminationBuilder.cs +++ b/LevelImposter/Builders/Util/DecontaminationBuilder.cs @@ -40,7 +40,7 @@ public void Build(LIElement elem, GameObject obj) // Sound var deconSound = MapUtils.FindSound(elem.properties.sounds, DECONTAM_SOUND_NAME); if (deconSound != null) - deconSystem.SpraySound = WAVFile.Load(deconSound.data); + deconSystem.SpraySound = WAVFile.LoadSound(deconSound); _deconSystemDB.Add(elem.id, deconSystem); _deconElemDB.Add(elem.id, elem); diff --git a/LevelImposter/Builders/Util/MeetingOptionsBuilder.cs b/LevelImposter/Builders/Util/MeetingOptionsBuilder.cs index f5c3d16..c019d20 100644 --- a/LevelImposter/Builders/Util/MeetingOptionsBuilder.cs +++ b/LevelImposter/Builders/Util/MeetingOptionsBuilder.cs @@ -1,5 +1,5 @@ -using UnityEngine; using LevelImposter.Core; +using UnityEngine; namespace LevelImposter.Builders { @@ -33,13 +33,14 @@ public void Build(LIElement elem, GameObject obj) return; } TriggerObject = obj; - + // Meeting Background if (!string.IsNullOrEmpty(elem.properties.meetingBackground)) { SpriteLoader.Instance?.LoadSpriteAsync( elem.properties.meetingBackground, - (spriteData) => { + (spriteData) => + { LoadMeetingBackground(elem, spriteData); }, elem.id.ToString(), @@ -56,7 +57,7 @@ public void Build(LIElement elem, GameObject obj) LISound? buttonSound = MapUtils.FindSound(elem.properties.sounds, BUTTON_SOUND_NAME); if (buttonSound != null) { - meetingOverlay.Stinger = WAVFile.Load(buttonSound?.data) ?? meetingOverlay.Stinger; + meetingOverlay.Stinger = WAVFile.LoadSound(buttonSound) ?? meetingOverlay.Stinger; meetingOverlay.StingerVolume = buttonSound?.volume ?? 1; } @@ -65,16 +66,17 @@ public void Build(LIElement elem, GameObject obj) MeetingCalledAnimation reportOverlay = Object.Instantiate(shipStatus.ReportOverlay, shipStatus.transform); reportOverlay.gameObject.SetActive(false); shipStatus.ReportOverlay = reportOverlay; - + LISound? reportSound = MapUtils.FindSound(elem.properties.sounds, REPORT_SOUND_NAME); if (reportSound != null) { - reportOverlay.Stinger = WAVFile.Load(reportSound?.data) ?? reportOverlay.Stinger; + reportOverlay.Stinger = WAVFile.LoadSound(reportSound) ?? reportOverlay.Stinger; reportOverlay.StingerVolume = reportSound?.volume ?? 1; } } - private void LoadMeetingBackground(LIElement elem, SpriteLoader.SpriteData? spriteData) { + private void LoadMeetingBackground(LIElement elem, SpriteLoader.SpriteData? spriteData) + { // Handle Error if (spriteData == null) diff --git a/LevelImposter/Builders/Util/PlatformBuilder.cs b/LevelImposter/Builders/Util/PlatformBuilder.cs index f1aad28..a8b075a 100644 --- a/LevelImposter/Builders/Util/PlatformBuilder.cs +++ b/LevelImposter/Builders/Util/PlatformBuilder.cs @@ -76,7 +76,7 @@ public void Build(LIElement elem, GameObject obj) // Sound LISound? moveSound = MapUtils.FindSound(elem.properties.sounds, MOVE_SOUND_NAME); if (moveSound != null) - movingPlatform.MovingSound = WAVFile.Load(moveSound?.data); + movingPlatform.MovingSound = WAVFile.LoadSound(moveSound); // Consoles GameObject leftObj = new("Left Console"); diff --git a/LevelImposter/Builders/Util/SabotageOptionsBuilder.cs b/LevelImposter/Builders/Util/SabotageOptionsBuilder.cs index 4687a54..b3a8c74 100644 --- a/LevelImposter/Builders/Util/SabotageOptionsBuilder.cs +++ b/LevelImposter/Builders/Util/SabotageOptionsBuilder.cs @@ -1,5 +1,5 @@ -using UnityEngine; using LevelImposter.Core; +using UnityEngine; namespace LevelImposter.Builders { @@ -32,11 +32,11 @@ public void Build(LIElement elem, GameObject obj) return; } TriggerObject = obj; - + // Sabotage Sound LISound? sabotageSound = MapUtils.FindSound(elem.properties.sounds, SABOTAGE_SOUND_NAME); if (sabotageSound != null) - shipStatus.SabotageSound = WAVFile.Load(sabotageSound?.data) ?? shipStatus.SabotageSound; + shipStatus.SabotageSound = WAVFile.LoadSound(sabotageSound) ?? shipStatus.SabotageSound; } public void PostBuild() { } diff --git a/LevelImposter/Builders/Util/SporeBuilder.cs b/LevelImposter/Builders/Util/SporeBuilder.cs new file mode 100644 index 0000000..641973c --- /dev/null +++ b/LevelImposter/Builders/Util/SporeBuilder.cs @@ -0,0 +1,79 @@ +using Il2CppSystem.Collections.Generic; +using LevelImposter.Core; +using LevelImposter.DB; +using UnityEngine; + +namespace LevelImposter.Builders +{ + public class SporeBuilder : IElemBuilder + { + private static List _mushrooms = new(); + public static List Mushrooms => _mushrooms; + + public void Build(LIElement elem, GameObject obj) + { + if (elem.type != "util-spore") + return; + + // Prefab + var prefab = AssetDB.GetObject(elem.type); + if (prefab == null) + return; + var prefabSpore = prefab.GetComponent(); + + // Sprite + var spriteRenderer = MapUtils.CloneSprite(obj, prefab, true); + obj.layer = (int)Layer.Ship; + + // Screen Mask + var sporeRange = (elem.properties.sporeRange ?? 3.7f) * 0.65f; + var screenMaskPrefab = prefab.transform.FindChild("SporeScreenMask").gameObject; + var screenMaskObj = new GameObject("ScreenMask"); + screenMaskObj.transform.parent = obj.transform; + screenMaskObj.transform.localPosition = new Vector3(0, 0, 4.3f); + screenMaskObj.transform.localScale = new Vector3(sporeRange, sporeRange, 1.2f); + var screenMaskRenderer = MapUtils.CloneSprite(screenMaskObj, screenMaskPrefab, true); + + // Screen Graphic + var screenGraphicPrefab = prefab.transform.FindChild("SporeScreenGraphic").gameObject; + var screenGraphicObj = new GameObject("ScreenGraphic"); + screenGraphicObj.transform.parent = obj.transform; + screenGraphicObj.transform.localPosition = new Vector3(0, 0, -10.0f); + screenGraphicObj.transform.localScale = new Vector3(sporeRange, sporeRange, 1.2f); + var sceenGraphicRenderer = MapUtils.CloneSprite(screenGraphicObj, screenGraphicPrefab, true); + + // Set Color + sceenGraphicRenderer.color = elem.properties.gasColor?.ToUnity() ?? screenGraphicPrefab.GetComponent().color; + + // Collider + CircleCollider2D collider = obj.AddComponent(); + collider.radius = elem.properties.range ?? 0.25f; + collider.isTrigger = true; + + // Mushroom + Mushroom mushroom = obj.AddComponent(); + mushroom.id = _mushrooms.Count; + mushroom.mushroomCollider = collider; + mushroom.mushroom = spriteRenderer; + mushroom.mushroomAnimator = spriteRenderer.GetComponent(); + mushroom.sporeMask = screenMaskObj; + mushroom.sporeCloudMaskAnimator = screenMaskRenderer.GetComponent(); + mushroom.spores = screenMaskRenderer; + mushroom.sporeCloudAnimator = screenGraphicObj.GetComponent(); + mushroom.mushroomIdle = prefabSpore.mushroomIdle; + mushroom.mushroomAppear = prefabSpore.mushroomAppear; + mushroom.mushroomSteppedOn = prefabSpore.mushroomSteppedOn; + mushroom.sporeCloudIdle = prefabSpore.sporeCloudIdle; + mushroom.sporeCloudAppear = prefabSpore.sporeCloudAppear; + mushroom.sporeCloudDisappear = prefabSpore.sporeCloudDisappear; + mushroom.spawnSound = prefabSpore.spawnSound; + mushroom.activateSporeSound = prefabSpore.activateSporeSound; + + mushroom.ResetState(); + mushroom.enabled = true; + _mushrooms.Add(mushroom); + } + + public void PostBuild() { } + } +} diff --git a/LevelImposter/Builders/Util/StepSoundBuilder.cs b/LevelImposter/Builders/Util/StepSoundBuilder.cs index 58e61f9..ce97a00 100644 --- a/LevelImposter/Builders/Util/StepSoundBuilder.cs +++ b/LevelImposter/Builders/Util/StepSoundBuilder.cs @@ -1,6 +1,6 @@ +using LevelImposter.Core; using LevelImposter.DB; using UnityEngine; -using LevelImposter.Core; namespace LevelImposter.Builders { @@ -35,7 +35,7 @@ public void Build(LIElement elem, GameObject obj) { // Sound Data LISound sound = elem.properties.sounds[i]; - if (sound.data == null) + if (sound == null) { LILogger.Warn($"{elem.name} missing audio data"); continue; @@ -44,12 +44,12 @@ public void Build(LIElement elem, GameObject obj) // Preset if (sound.isPreset) { - soundGroup.Clips[i] = AssetDB.GetSound(sound.data); + soundGroup.Clips[i] = AssetDB.GetSound(sound.presetID ?? ""); } // WAVLoader else { - soundGroup.Clips[i] = WAVFile.Load(sound.data); + soundGroup.Clips[i] = WAVFile.LoadSound(sound); } } diff --git a/LevelImposter/Builders/Util/VentBuilder.cs b/LevelImposter/Builders/Util/VentBuilder.cs index b1b3524..d1b683e 100644 --- a/LevelImposter/Builders/Util/VentBuilder.cs +++ b/LevelImposter/Builders/Util/VentBuilder.cs @@ -1,10 +1,10 @@ +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using LevelImposter.Core; using LevelImposter.DB; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; -using Il2CppInterop.Runtime.InteropTypes.Arrays; -using LevelImposter.Core; namespace LevelImposter.Builders { @@ -36,22 +36,21 @@ public void Build(LIElement elem, GameObject obj) var prefabVent = prefab.GetComponent(); var prefabArrow = prefab.transform.FindChild("Arrow").gameObject; - // Skeld ShipStatus - var skeldShip = AssetDB.GetObject("ss-skeld"); - var skeldShipStatus = skeldShip?.GetComponent(); - // Default Sprite - bool isAnim = elem.type == "util-vent1"; + bool isAnim = elem.type == "util-vent1" || elem.type == "util-vent3"; SpriteRenderer spriteRenderer = MapUtils.CloneSprite(obj, prefab, isAnim); // Console - VentCleaningConsole console = obj.AddComponent(); - console.Image = spriteRenderer; - console.ImpostorDiscoveredSound = prefabConsole.ImpostorDiscoveredSound; - console.TaskTypes = prefabConsole.TaskTypes; - console.ValidTasks = prefabConsole.ValidTasks; - if (elem.properties.range != null) - console.usableDistance = (float)elem.properties.range; + if (prefabConsole != null) + { + VentCleaningConsole console = obj.AddComponent(); + console.Image = spriteRenderer; + console.ImpostorDiscoveredSound = prefabConsole.ImpostorDiscoveredSound; + console.TaskTypes = prefabConsole.TaskTypes; + console.ValidTasks = prefabConsole.ValidTasks; + if (elem.properties.range != null) + console.usableDistance = (float)elem.properties.range; + } // Vent Vent vent = obj.AddComponent(); @@ -78,12 +77,12 @@ public void Build(LIElement elem, GameObject obj) LISound? openSound = MapUtils.FindSound(elem.properties.sounds, OPEN_SOUND_NAME); if (openSound != null) - shipStatus.VentEnterSound = WAVFile.Load(openSound?.data); + shipStatus.VentEnterSound = WAVFile.LoadSound(openSound); LISound? moveSound = MapUtils.FindSound(elem.properties.sounds, MOVE_SOUND_NAME); if (moveSound != null) shipStatus.VentMoveSounds = new Il2CppReferenceArray(new AudioClip[] { - WAVFile.Load(moveSound?.data) + WAVFile.LoadSound(moveSound) }); } diff --git a/LevelImposter/Core/Components/LIShipStatus.cs b/LevelImposter/Core/Components/LIShipStatus.cs index e8549ea..622bb14 100644 --- a/LevelImposter/Core/Components/LIShipStatus.cs +++ b/LevelImposter/Core/Components/LIShipStatus.cs @@ -1,16 +1,16 @@ +using BepInEx.Unity.IL2CPP.Utils.Collections; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using LevelImposter.Builders; +using LevelImposter.DB; +using LevelImposter.Shop; +using Reactor.Networking.Attributes; using System; -using System.Collections.Generic; using System.Collections; -using UnityEngine; -using LevelImposter.Shop; -using LevelImposter.DB; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; -using Il2CppInterop.Runtime.Attributes; -using Il2CppInterop.Runtime.InteropTypes.Arrays; -using BepInEx.Unity.IL2CPP.Utils.Collections; -using Reactor.Networking.Attributes; -using LevelImposter.Builders; +using UnityEngine; namespace LevelImposter.Core { @@ -36,7 +36,8 @@ public LIShipStatus(IntPtr intPtr) : base(intPtr) { "Skeld", "ss-skeld" }, { "MiraHQ", "ss-mira" }, { "Polus", "ss-polus" }, - { "Airship", "ss-airship" } + { "Airship", "ss-airship" }, + { "Fungle", "ss-fungle" } }; public static readonly KeyCode[] RESPAWN_SEQ = new KeyCode[] { KeyCode.R, @@ -69,13 +70,14 @@ public LIShipStatus(IntPtr intPtr) : base(intPtr) public ShipStatus? ShipStatus => _shipStatus; public bool IsReady { - get { + get + { return SpriteLoader.Instance?.RenderCount <= 0 && !MapSync.IsDownloadingMap && _isReady; } } - + /// /// Resets the map to a blank slate. Ran before any map elements are applied. /// @@ -92,12 +94,12 @@ public void ResetMap() camera.shakeAmount = 0; camera.shakePeriod = 0; - ShipStatus.AllDoors = new Il2CppReferenceArray(0); + ShipStatus.AllDoors = new Il2CppReferenceArray(0); ShipStatus.DummyLocations = new Il2CppReferenceArray(0); ShipStatus.SpecialTasks = new Il2CppReferenceArray(0); ShipStatus.CommonTasks = new Il2CppReferenceArray(0); ShipStatus.LongTasks = new Il2CppReferenceArray(0); - ShipStatus.NormalTasks = new Il2CppReferenceArray(0); + ShipStatus.ShortTasks = new Il2CppReferenceArray(0); ShipStatus.SystemNames = new Il2CppStructArray(0); ShipStatus.Systems = new Il2CppSystem.Collections.Generic.Dictionary(); ShipStatus.MedScanner = null; @@ -147,9 +149,6 @@ public void LoadMap(LIMap map) if (!AssetDB.IsInit) LILogger.Warn("Asset DB is not initialized yet!"); - // Sprite Loader - SpriteLoader.Instance?.SearchForDuplicateSprites(map); - // Priority First foreach (string type in PRIORITY_TYPES) foreach (LIElement elem in map.elements) @@ -367,17 +366,20 @@ public void Start() DestroyableSingleton.Instance.ShadowQuad.material.SetInt("_Mask", 7); // Respawn the player on key combo - StartCoroutine(CoHandleKeyCombo(RESPAWN_SEQ, () =>{ + StartCoroutine(CoHandleKeyCombo(RESPAWN_SEQ, () => + { RespawnPlayer(PlayerControl.LocalPlayer); }).WrapToIl2Cpp()); // Set CPU affinity on key combo - StartCoroutine(CoHandleKeyCombo(CPU_SEQ, () => { + StartCoroutine(CoHandleKeyCombo(CPU_SEQ, () => + { SetCPUAffinity(); }).WrapToIl2Cpp()); // Run Debug Tests - StartCoroutine(CoHandleKeyCombo(DEBUG_SEQ, () => { + StartCoroutine(CoHandleKeyCombo(DEBUG_SEQ, () => + { RunDebugTests(); }).WrapToIl2Cpp()); } @@ -387,7 +389,7 @@ public void OnDestroy() _renames = null; _currentMap = null; Instance = null; - + // Wipe Cache (Freeplay Only) if (GameState.IsInFreeplay && LIConstants.FREEPLAY_FLUSH_CACHE) GCHandler.Clean(); diff --git a/LevelImposter/Core/Components/LITeleporter.cs b/LevelImposter/Core/Components/LITeleporter.cs index e02b0ac..eb82817 100644 --- a/LevelImposter/Core/Components/LITeleporter.cs +++ b/LevelImposter/Core/Components/LITeleporter.cs @@ -1,8 +1,8 @@ +using Il2CppInterop.Runtime.Attributes; +using Reactor.Networking.Attributes; using System; using System.Collections.Generic; using UnityEngine; -using Reactor.Networking.Attributes; -using Il2CppInterop.Runtime.Attributes; namespace LevelImposter.Core { @@ -47,14 +47,7 @@ public void SetElement(LIElement elem) public static void RPCTeleport(PlayerControl player, float x, float y) { LILogger.Info($"Teleported {player.name} to ({x},{y})"); - player.transform.position = new Vector3( - x, - y, - player.transform.position.z - ); - - player.NetTransform.targetSyncPosition = player.transform.position; - player.NetTransform.prevPosSent = player.transform.position; + player.NetTransform.SnapTo(player.transform.position); } public void Awake() diff --git a/LevelImposter/Core/Components/LITriggerable.cs b/LevelImposter/Core/Components/LITriggerable.cs index e3c0379..d21a704 100644 --- a/LevelImposter/Core/Components/LITriggerable.cs +++ b/LevelImposter/Core/Components/LITriggerable.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using BepInEx.Unity.IL2CPP.Utils.Collections; +using BepInEx.Unity.IL2CPP.Utils.Collections; using Il2CppInterop.Runtime.Attributes; using Reactor.Networking.Attributes; -using System.Linq; -using UnityEngine.Rendering.VirtualTexturing; -using Hazel; using Reactor.Networking.Rpc; -using static UnityEngine.ParticleSystem.PlaybackState; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; namespace LevelImposter.Core { @@ -239,33 +236,44 @@ private void OnTrigger(PlayerControl? orgin, int stackSize = 0) // Sabotage case "startOxygen": - ShipStatus.Instance.RpcRepairSystem(SystemTypes.LifeSupp, 128); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.LifeSupp, 128); break; case "startLights": byte switchBits = 4; for (int i = 0; i < 5; i++) if (BoolRange.Next(0.5f)) switchBits |= (byte)(1 << i); - ShipStatus.Instance.RpcRepairSystem(SystemTypes.Electrical, switchBits | 128); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)(switchBits | 128)); break; case "startReactor": - ShipStatus.Instance.RpcRepairSystem(SystemTypes.Reactor, 128); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Reactor, 128); break; case "startComms": - ShipStatus.Instance.RpcRepairSystem(SystemTypes.Comms, 128); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, 128); + break; + case "startMixup": + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.MushroomMixupSabotage, 1); break; case "endOxygen": - ShipStatus.Instance.RpcRepairSystem(SystemTypes.LifeSupp, 16); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.LifeSupp, 16); break; case "endLights": var lights = ShipStatus.Instance.Systems[SystemTypes.Electrical].Cast(); - ShipStatus.Instance.RpcRepairSystem(SystemTypes.Electrical, (lights.ExpectedSwitches ^ lights.ActualSwitches) | 128); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Electrical, (byte)((lights.ExpectedSwitches ^ lights.ActualSwitches) | 128)); break; case "endReactor": - ShipStatus.Instance.RpcRepairSystem(SystemTypes.Reactor, 16); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Reactor, 16); break; case "endComms": - ShipStatus.Instance.RpcRepairSystem(SystemTypes.Comms, 0); + ShipStatus.Instance.RpcUpdateSystem(SystemTypes.Comms, 0); + break; + case "endMixup": + if (!ShipStatus.Instance.Systems.ContainsKey(SystemTypes.MushroomMixupSabotage)) + return; + var mixup = ShipStatus.Instance.Systems[SystemTypes.MushroomMixupSabotage].Cast(); + mixup.currentSecondsUntilHeal = 0.01f; + mixup.IsDirty = true; + // TODO: Transmit to other clients break; } } @@ -291,7 +299,7 @@ private void StopComponents() if (triggerSound != null) triggerSound.Stop(); } - + /// /// Starts any components attatched to object /// diff --git a/LevelImposter/Core/Components/MinigameSprites.cs b/LevelImposter/Core/Components/MinigameSprites.cs index 052a540..a1cccd9 100644 --- a/LevelImposter/Core/Components/MinigameSprites.cs +++ b/LevelImposter/Core/Components/MinigameSprites.cs @@ -1,9 +1,11 @@ -using System; -using UnityEngine; +using Il2CppInterop.Runtime.Attributes; using LevelImposter.DB; -using Il2CppInterop.Runtime.Attributes; -using System.Collections.Generic; using PowerTools; +using QRCoder; +using QRCoder.Unity; +using System; +using System.Collections.Generic; +using UnityEngine; namespace LevelImposter.Core { @@ -71,15 +73,25 @@ public void LoadMinigame(Minigame minigame) return; foreach (LIMinigameSprite minigameData in _minigameDataArr) { + // Get Pivot Vector2? pivot = PIVOTS.ContainsKey(minigameData.type) ? PIVOTS[minigameData.type] : null; - SpriteLoader.Instance?.LoadSpriteAsync( - minigameData.spriteData, - (spriteData) => { - LoadMinigameSprite(minigame, minigameData.type, spriteData?.Sprite); - }, - minigameData.id.ToString(), - pivot - ); + + // Get Sprite Stream + var mapAssetDB = LIShipStatus.Instance?.CurrentMap?.mapAssetDB; + Guid? guid = minigameData.spriteID; + var mapAsset = mapAssetDB?.Get(guid); + + // Load Sprite + if (mapAsset != null) + { + SpriteLoader.Instance?.LoadSpriteAsync( + mapAsset.OpenStream(), + (spriteData) => LoadMinigameSprite(minigame, minigameData.type, spriteData?.Sprite), + minigameData.spriteID?.ToString(), + pivot + ); + } + } } catch (Exception e) @@ -141,6 +153,54 @@ private void LoadMinigameProps(Minigame minigame) var starfield = minigame.transform.Find("BlackBg/starfield"); if (starfield != null) starfield.gameObject.SetActive(_minigameProps?.isStarfieldEnabled ?? true); + LILogger.Info("Applied Telescope Props"); + } + + // Weapons Task + bool isWeapons = _minigameProps?.weaponsColor != null; + if (isWeapons) + { + var weaponsLine = minigame.transform.Find("TargetLines").GetComponent(); + var weaponsColor = _minigameProps?.weaponsColor?.ToUnity(); + weaponsLine.startColor = weaponsColor ?? weaponsLine.startColor; + weaponsLine.endColor = weaponsColor ?? weaponsLine.endColor; + weaponsLine.sharedMaterial?.SetColor("_Color", weaponsColor ?? weaponsLine.startColor); + LILogger.Info("Applied Weapon Props"); + } + + // Boarding Pass + bool isBoardingPass = !string.IsNullOrEmpty(_minigameProps?.qrCodeText); + if (isBoardingPass) + { + // Wait for 2 frames for Start() to finish + MapUtils.WaitForFrames(2, () => + { + // Generate a QR Code + var qrCodeGenerator = new QRCodeGenerator(); + var qrCode = qrCodeGenerator.CreateQrCode( + _minigameProps?.qrCodeText ?? "", + QRCodeGenerator.ECCLevel.M, + false, + false, + QRCodeGenerator.EciMode.Default, + -1 + ); + // Create Texture + var texture = new UnityQRCode(qrCode).GetGraphic(1); + GCHandler.Register(texture); + + // Create Sprite + var sprite = Sprite.Create( + texture, + new Rect(0f, 0f, texture.width, texture.height), + new Vector2(0.5f, 0.5f) + ); + GCHandler.Register(sprite); + + // Apply Sprite + minigame.Cast().renderer.sprite = sprite; + LILogger.Info("Applied Boarding Pass Props: " + _minigameProps?.qrCodeText); + }); } } @@ -329,6 +389,9 @@ private bool LoadMinigameFieldSprite(Minigame minigame, string type, Sprite? spr case "task-pass_scanningb": minigame.Cast().ScannerScanning = sprite; return false; + case "task-pass_idface": + minigame.Cast().Image.sprite = sprite; + return false; /* task-telescope */ case "task-telescope_bg": @@ -353,7 +416,7 @@ private bool LoadMinigameFieldSprite(Minigame minigame, string type, Sprite? spr { var button = minigame.transform.Find(path); var rolloverComponent = button.GetComponent(); - + if (isDown) rolloverComponent.DownSprite = sprite; else diff --git a/LevelImposter/Core/Components/SpriteLoader.cs b/LevelImposter/Core/Components/SpriteLoader.cs index aa64422..dd2ac28 100644 --- a/LevelImposter/Core/Components/SpriteLoader.cs +++ b/LevelImposter/Core/Components/SpriteLoader.cs @@ -1,7 +1,6 @@ using BepInEx.Unity.IL2CPP.Utils.Collections; using Il2CppInterop.Runtime.Attributes; using Il2CppInterop.Runtime.InteropTypes.Arrays; -using PowerTools; using System; using System.Collections; using System.Collections.Generic; @@ -29,7 +28,6 @@ public SpriteLoader(IntPtr intPtr) : base(intPtr) private Stack? _spriteCache = new(); private int _renderCount = 0; - private Dictionary? _duplicateSpriteDB = null; public int RenderCount => _renderCount; @@ -51,10 +49,6 @@ public SpriteLoader(IntPtr intPtr) : base(intPtr) return spriteData; } } - if (_duplicateSpriteDB?.ContainsKey(spriteID) == true) - { - return GetSpriteFromCache(_duplicateSpriteDB[spriteID]); - } return null; } @@ -65,7 +59,6 @@ public void Clean() { OnLoad = null; _spriteCache?.Clear(); - _duplicateSpriteDB = null; } /// @@ -105,8 +98,17 @@ public void LoadSpriteAsync(LIElement element, GameObject obj) LILogger.Info($"Loading sprite for {element}"); Stopwatch stopwatch = Stopwatch.StartNew(); - string b64 = element.properties.spriteData ?? ""; - LoadSpriteAsync(b64, (nullableSpriteData) => + // Get Sprite Data + var spriteDB = LIShipStatus.Instance?.CurrentMap?.mapAssetDB; + Guid? spriteID = element.properties.spriteID; + var spriteStream = spriteDB?.Get(spriteID)?.OpenStream(); + if (spriteStream == null) + { + LILogger.Warn($"Could not find sprite for {element}"); + return; + } + + LoadSpriteAsync(spriteStream, (nullableSpriteData) => { // Abort on Exit if (obj == null) @@ -119,9 +121,6 @@ public void LoadSpriteAsync(LIElement element, GameObject obj) return; } - // Sprite is in cache, we can reduce memory usage - element.properties.spriteData = ""; - // Load Components var spriteData = nullableSpriteData; if (spriteData.IsGIF) // Animated GIF @@ -129,7 +128,7 @@ public void LoadSpriteAsync(LIElement element, GameObject obj) GIFAnimator gifAnimator = obj.AddComponent(); gifAnimator.Init(element, spriteData.GIFData); stopwatch.Stop(); - LILogger.Info($"Done loading {spriteData.GIFData.Width}x{spriteData.GIFData.Height} sprite for {element} ({RenderCount} Left) [{stopwatch.ElapsedMilliseconds}ms]"); + LILogger.Info($"Done loading {spriteData.GIFData?.Width}x{spriteData.GIFData?.Height} sprite for {element} ({RenderCount} Left) [{stopwatch.ElapsedMilliseconds}ms]"); } else // Still Image { @@ -142,7 +141,7 @@ public void LoadSpriteAsync(LIElement element, GameObject obj) if (OnLoad != null) OnLoad.Invoke(element); - }, element.id.ToString(), null); + }, spriteID.ToString(), null); } /// @@ -154,8 +153,8 @@ public void LoadSpriteAsync(LIElement element, GameObject obj) public void LoadSpriteAsync(string b64Image, Action onLoad, string? spriteID, Vector2? pivot) { var imgData = string.IsNullOrEmpty(b64Image) ? new(0) : MapUtils.ParseBase64(b64Image); - bool isGIF = b64Image.StartsWith("data:image/gif"); - LoadSpriteAsync(imgData, isGIF, (spriteList) => + var imgStream = new MemoryStream(imgData); + LoadSpriteAsync(imgStream, (spriteList) => { onLoad(spriteList); spriteList = null; @@ -169,9 +168,9 @@ public void LoadSpriteAsync(string b64Image, Action onLoad, string? /// Image File Data /// Callback on success [HideFromIl2Cpp] - public void LoadSpriteAsync(Il2CppStructArray imgData, bool isGIF, Action onLoad, string? spriteID, Vector2? pivot) + public void LoadSpriteAsync(Stream? imgStream, Action onLoad, string? spriteID, Vector2? pivot) { - StartCoroutine(CoLoadSpriteAsync(imgData, isGIF, onLoad, spriteID, pivot).WrapToIl2Cpp()); + StartCoroutine(CoLoadSpriteAsync(imgStream, onLoad, spriteID, pivot).WrapToIl2Cpp()); } /// @@ -180,7 +179,7 @@ public void LoadSpriteAsync(Il2CppStructArray imgData, bool isGIF, Action< /// Image File Data /// Callback on success [HideFromIl2Cpp] - private IEnumerator CoLoadSpriteAsync(Il2CppStructArray imgData, bool isGIF, Action? onLoad, string? spriteID, Vector2? pivot) + private IEnumerator CoLoadSpriteAsync(Stream? imgStream, Action? onLoad, string? spriteID, Vector2? pivot) { { _renderCount++; @@ -195,33 +194,45 @@ private IEnumerator CoLoadSpriteAsync(Il2CppStructArray imgData, bool isGI { // Using sprite data from cache } - else if (isGIF) + else if (imgStream == null) + { + LILogger.Warn($"Could not load sprite {spriteID} from null stream"); + spriteData = null; + } + else if (GIFFile.IsGIF(imgStream)) { - using (MemoryStream ms = new(imgData)) + // Generate GIF Data + var gifFile = new GIFFile(spriteID ?? "LISprite"); + gifFile.SetPivot(pivot); + gifFile.Load(imgStream); + + // Create Texture + spriteData = new() { - var gifFile = new GIFFile(spriteID ?? "LISprite"); - gifFile.SetPivot(pivot); - gifFile.Load(ms); - - spriteData = new() - { - ID = spriteID ?? "", - Sprite = gifFile.GetFrameSprite(0), - GIFData = gifFile - }; - AddSpriteToCache(spriteData); - GCHandler.Register(spriteData); - } + ID = spriteID ?? "", + Sprite = gifFile.GetFrameSprite(0), + GIFData = gifFile + }; + AddSpriteToCache(spriteData); + GCHandler.Register(spriteData); } else { + // Get All Data + byte[] imageDataBuffer = new byte[imgStream.Length]; + imgStream.Read(imageDataBuffer, 0, imageDataBuffer.Length); + + // Create Texture spriteData = new() { ID = spriteID ?? "", - Sprite = RawImageToSprite(imgData, pivot) + Sprite = RawImageToSprite(imageDataBuffer, pivot) }; spriteData.Sprite.hideFlags = HideFlags.DontUnloadUnusedAsset; AddSpriteToCache(spriteData); + + // Cleanup + imageDataBuffer = null; GCHandler.Register(spriteData); } @@ -231,7 +242,8 @@ private IEnumerator CoLoadSpriteAsync(Il2CppStructArray imgData, bool isGI onLoad.Invoke(spriteData); onLoad = null; - imgData = null; + imgStream?.Dispose(); + imgStream = null; } } @@ -283,45 +295,6 @@ public Sprite LoadSprite(Il2CppStructArray imgData, string? spriteID) return sprite; } - /// - /// Searches the current map for duplicate sprite entries. Optional, improves performance. - /// TODO: Optimize me! ( O(n^2) ) - /// - [HideFromIl2Cpp] - public void SearchForDuplicateSprites(LIMap map) - { - // Already Loaded - if (_duplicateSpriteDB != null) - return; - - // Debug Start - var elems = map.elements; - Stopwatch sw = Stopwatch.StartNew(); - LILogger.Info($"Searching {elems.Length} elements for duplicate sprites"); - - // Iterate through map elements - _duplicateSpriteDB = new(); - for (int a = 0; a < elems.Length - 1; a++) - { - for (int b = a + 1; b < elems.Length; b++) - { - var spriteA = elems[a].properties.spriteData; - var spriteB = elems[b].properties.spriteData; - - if (_duplicateSpriteDB?.ContainsKey(elems[a].id.ToString()) == false - && !string.IsNullOrEmpty(spriteA) - && spriteA == spriteB) - { - _duplicateSpriteDB?.Add(elems[b].id.ToString(), elems[a].id.ToString()); - } - } - } - - // Debug End - sw.Stop(); - LILogger.Info($"Found {_duplicateSpriteDB?.Count} duplicate sprites in {sw.ElapsedMilliseconds}ms"); - } - public void Awake() { if (Instance == null) @@ -352,7 +325,10 @@ public void Dispose() if (GIFData != null) GIFData.Dispose(); if (Sprite != null) + { Destroy(Sprite.texture); + Destroy(Sprite); + } Sprite = null; GIFData = null; } diff --git a/LevelImposter/Core/Components/TriggerSoundPlayer.cs b/LevelImposter/Core/Components/TriggerSoundPlayer.cs index 706f3e2..1b94cba 100644 --- a/LevelImposter/Core/Components/TriggerSoundPlayer.cs +++ b/LevelImposter/Core/Components/TriggerSoundPlayer.cs @@ -1,9 +1,8 @@ -using System; -using UnityEngine; -using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.Attributes; +using System; using System.Linq; +using UnityEngine; using UnityEngine.Audio; -using Il2CppInterop.Runtime.InteropTypes.Fields; namespace LevelImposter.Core { @@ -31,7 +30,7 @@ public TriggerSoundPlayer(IntPtr intPtr) : base(intPtr) [HideFromIl2Cpp] public void Init(LISound soundData, Collider2D[] colliders) { - _clip = WAVFile.Load(soundData?.data); + _clip = WAVFile.LoadSound(soundData); _volume = soundData?.volume ?? 1.0f; _colliders = colliders; _channel = soundData?.channel switch diff --git a/LevelImposter/Core/Models/LIMap.cs b/LevelImposter/Core/Models/LIMap.cs index 630bc9d..b6d74b4 100644 --- a/LevelImposter/Core/Models/LIMap.cs +++ b/LevelImposter/Core/Models/LIMap.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace LevelImposter.Core { @@ -7,5 +8,17 @@ public class LIMap : LIMetadata { public LIElement[] elements { get; set; } public LIMapProperties properties { get; set; } + + // LIM2 + [JsonIgnore] + public const int LIM_VERSION = 2; + [JsonIgnore] + public bool isLegacy + { + get => v < LIM_VERSION; + set => v = value ? 1 : LIM_VERSION; + } + [JsonIgnore] + public MapAssetDB? mapAssetDB { get; set; } } } diff --git a/LevelImposter/Core/Models/LIMinigameProps.cs b/LevelImposter/Core/Models/LIMinigameProps.cs index c49c6df..94da363 100644 --- a/LevelImposter/Core/Models/LIMinigameProps.cs +++ b/LevelImposter/Core/Models/LIMinigameProps.cs @@ -11,6 +11,8 @@ public class LIMinigameProps public LIColor? lightsColorOff { get; set; } public LIColor? fuelColor { get; set; } public LIColor? fuelBgColor { get; set; } + public LIColor? weaponsColor { get; set; } + public string? qrCodeText { get; set; } public bool? isStarfieldEnabled { get; set; } } } diff --git a/LevelImposter/Core/Models/LIMinigameSprite.cs b/LevelImposter/Core/Models/LIMinigameSprite.cs index 6594f34..38e8baa 100644 --- a/LevelImposter/Core/Models/LIMinigameSprite.cs +++ b/LevelImposter/Core/Models/LIMinigameSprite.cs @@ -7,6 +7,10 @@ public class LIMinigameSprite { public Guid id { get; set; } public string type { get; set; } - public string spriteData { get; set; } + public Guid? spriteID { get; set; } + + // Legacy + [Obsolete("Use spriteID instead")] + public string? spriteData { get; set; } } } diff --git a/LevelImposter/Core/Models/LIProperties.cs b/LevelImposter/Core/Models/LIProperties.cs index 220ffb1..ea2e647 100644 --- a/LevelImposter/Core/Models/LIProperties.cs +++ b/LevelImposter/Core/Models/LIProperties.cs @@ -18,18 +18,25 @@ public class LIProperties public int? triggerCount { get; set; } // Sprite - public string? spriteData { get; set; } + public Guid? spriteID { get; set; } public bool? noShadows { get; set; } public bool? noShadowsBehaviour { get; set; } public LIColor? color { get; set; } public bool? loopGIF { get; set; } + // Legacy + [Obsolete("Use spriteID instead")] + public string? spriteData { get; set; } + // One-Way Colliders public bool? isImposterIgnored { get; set; } // Towels public float? towelPickupCount { get; set; } + // Spores + public LIColor? gasColor { get; set; } + // Decontamination public Guid? doorA { get; set; } public Guid? doorB { get; set; } @@ -74,10 +81,11 @@ public class LIProperties public bool? onlyFromBelow { get; set; } public bool? checkCollision { get; set; } public float? range { get; set; } + public float? sporeRange { get; set; } // Ladder public float? ladderHeight { get; set; } - + // Platform public float? platformXOffset { get; set; } public float? platformYOffset { get; set; } @@ -100,7 +108,7 @@ public class LIProperties // Tasks public string? description { get; set; } public string? taskLength { get; set; } - public float? sabDuration { get; set; } + public float? sabDuration { get; set; } // Room public bool? isRoomNameVisible { get; set; } diff --git a/LevelImposter/Core/Models/LIRpc.cs b/LevelImposter/Core/Models/LIRpc.cs index cbce443..6dde055 100644 --- a/LevelImposter/Core/Models/LIRpc.cs +++ b/LevelImposter/Core/Models/LIRpc.cs @@ -1,17 +1,17 @@ namespace LevelImposter.Core { - // Among Us 0 - 32 + // Among Us 0 - 61 // TOR 60 - 73, 100 - 149 // Las Monjas 60 - 69, 75 - 194 // StellaRoles 60 - 169 // ToU 100 - 210, 220 - 251 // Submerged 210 - 214 - // LI 50 - 59 (Guess I'll exist here...) + // LI 94 - 99 (Wow we really need to coordinate this better) public enum LIRpc { - FireTrigger = 50, // Fires a global trigger on an object + FireTrigger = 94, // Fires a global trigger on an object TeleportPlayer, // Uses a util-tele object SyncMapID, // Syncs the map ID in the lobby SyncRandomSeed, // Syncs a random seed for util-triggerrand diff --git a/LevelImposter/Core/Models/LISound.cs b/LevelImposter/Core/Models/LISound.cs index 0543afa..fe8cc5f 100644 --- a/LevelImposter/Core/Models/LISound.cs +++ b/LevelImposter/Core/Models/LISound.cs @@ -7,9 +7,14 @@ public class LISound { public Guid id { get; set; } public string? type { get; set; } - public string? data { get; set; } public float volume { get; set; } - public bool isPreset { get; set; } + public Guid? dataID { get; set; } public string? channel { get; set; } + public bool isPreset { get; set; } + public string? presetID { get; set; } + + // Legacy + [Obsolete("Use dataID instead")] + public string? data { get; set; } } } diff --git a/LevelImposter/Core/Models/Layer.cs b/LevelImposter/Core/Models/Layer.cs index 7f98860..30f4e56 100644 --- a/LevelImposter/Core/Models/Layer.cs +++ b/LevelImposter/Core/Models/Layer.cs @@ -15,11 +15,14 @@ enum Layer /// WARNING: Automatically hidden by util-display objects /// Objects, - + ShortObjects, IlluminatedBlocking, Ghost, UICollider, - DrawShadows + DrawShadows, + KeyMapper, + MusicTriggers, + Notifications } } diff --git a/LevelImposter/Core/Models/MapAssetDB.cs b/LevelImposter/Core/Models/MapAssetDB.cs new file mode 100644 index 0000000..d3946f1 --- /dev/null +++ b/LevelImposter/Core/Models/MapAssetDB.cs @@ -0,0 +1,64 @@ +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using System; +using System.Collections.Generic; +using System.IO; + +namespace LevelImposter.Core +{ + public class MapAssetDB + { + private Dictionary _db = new(); + public Dictionary DB => _db; + + public void Add(Guid id, byte[] rawData) + { + _db.Add(id, new DBElement { rawData = rawData }); + } + + public void Add(Guid id, FileChunk fileChunk) + { + _db.Add(id, new DBElement { fileChunk = fileChunk }); + } + + public DBElement? Get(Guid? id) + { + if (id == null) + return null; + _db.TryGetValue((Guid)id, out DBElement? result); + if (result == null) + LILogger.Warn($"No such map asset with id {id}"); + return result; + } + + public class DBElement + { + public Il2CppStructArray? rawData { get; set; } + public FileChunk? fileChunk { get; set; } + + public Stream OpenStream() + { + if (rawData != null) + return new MemoryStream(rawData); + else if (fileChunk != null) + return fileChunk.OpenStream(); + else + throw new Exception("No data to convert to stream"); + } + + public byte[] ToBytes() + { + if (rawData != null) + return rawData; + else if (fileChunk != null) + using (var stream = fileChunk.OpenStream()) + { + byte[] buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return buffer; + } + else + throw new Exception("No data to convert to bytes"); + } + } + } +} diff --git a/LevelImposter/Core/Models/MapType.cs b/LevelImposter/Core/Models/MapType.cs index 283abae..2ce1df0 100644 --- a/LevelImposter/Core/Models/MapType.cs +++ b/LevelImposter/Core/Models/MapType.cs @@ -7,6 +7,7 @@ public enum MapType : int Polus, Dleks, Airship, + Fungle, Submerged, // TODO: Add Submerged Support LevelImposter } diff --git a/LevelImposter/Core/Patches/LateInitPatch.cs b/LevelImposter/Core/Patches/LateInitPatch.cs index a4b8349..bee2b11 100644 --- a/LevelImposter/Core/Patches/LateInitPatch.cs +++ b/LevelImposter/Core/Patches/LateInitPatch.cs @@ -1,6 +1,6 @@ using HarmonyLib; -using UnityEngine; using LevelImposter.Shop; +using UnityEngine; namespace LevelImposter.Core { @@ -29,7 +29,6 @@ public static void Postfix() GameObject apiParent = new GameObject("LevelImposter"); apiParent.AddComponent(); apiParent.AddComponent(); - apiParent.AddComponent(); apiParent.AddComponent(); UnityEngine.Object.DontDestroyOnLoad(apiParent); } diff --git a/LevelImposter/Core/Patches/MixupPatch.cs b/LevelImposter/Core/Patches/MixupPatch.cs new file mode 100644 index 0000000..1d8edfb --- /dev/null +++ b/LevelImposter/Core/Patches/MixupPatch.cs @@ -0,0 +1,24 @@ +using HarmonyLib; +using LevelImposter.Builders; + +namespace LevelImposter.Core +{ + /* + * Normally, mushroom mixup is handled + * by FungleShipStatus. This bypasses that + * requirement by supplying it's own system. + */ + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.IsMushroomMixupActive))] + public static class MixupPatch + { + public static bool Prefix(PlayerControl __instance, ref bool __result) + { + if (LIShipStatus.Instance == null) + return true; + + __result = (SabMixupBuilder.SabotageSystem?.IsActive ?? false) || + __instance.CurrentOutfitType == PlayerOutfitType.MushroomMixup; + return false; + } + } +} diff --git a/LevelImposter/Core/Patches/ModCompatibility/VentPatch.cs b/LevelImposter/Core/Patches/ModCompatibility/VentPatch.cs index 3056574..e30ae89 100644 --- a/LevelImposter/Core/Patches/ModCompatibility/VentPatch.cs +++ b/LevelImposter/Core/Patches/ModCompatibility/VentPatch.cs @@ -18,7 +18,7 @@ public static void Postfix() { if (LIShipStatus.Instance == null) return; - if (!ModCompatibility.IsTOUEnabled && !ModCompatibility.IsTOREnabled) + if (!ModCompatibility.IsTOUEnabled && !ModCompatibility.IsTOREnabled && !ModCompatibility.IsReworkedEnabled) return; if (_ventTotal == ShipStatus.Instance.AllVents.Count) return; diff --git a/LevelImposter/Core/Patches/RenamePatch.cs b/LevelImposter/Core/Patches/RenamePatch.cs index b60a242..70c224c 100644 --- a/LevelImposter/Core/Patches/RenamePatch.cs +++ b/LevelImposter/Core/Patches/RenamePatch.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using ObjList = Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppReferenceArray; namespace LevelImposter.Core { @@ -30,4 +31,19 @@ public static bool Prefix([HarmonyArgument(0)] TaskTypes taskType, ref string __ return false; } } + [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), new System.Type[] { typeof(StringNames), typeof(ObjList) })] + public static class StringRenamePatch + { + public static bool Prefix([HarmonyArgument(0)] StringNames stringNames, + [HarmonyArgument(1)] ObjList _, // TODO: Format parameters into string + ref string __result) + { + + if (LIShipStatus.Instance == null || !LIShipStatus.Instance.Renames.Contains(stringNames)) + return true; + + __result = LIShipStatus.Instance.Renames.Get(stringNames); + return false; + } + } } \ No newline at end of file diff --git a/LevelImposter/Core/Patches/SabPatch.cs b/LevelImposter/Core/Patches/SabPatch.cs index 6d5db77..f5fa83f 100644 --- a/LevelImposter/Core/Patches/SabPatch.cs +++ b/LevelImposter/Core/Patches/SabPatch.cs @@ -1,6 +1,6 @@ using HarmonyLib; -using System.Collections.Generic; using LevelImposter.Builders; +using System.Collections.Generic; namespace LevelImposter.Core { @@ -18,13 +18,15 @@ public static class SabStartPatch { SystemTypes.Reactor, TaskTypes.ResetReactor }, { SystemTypes.LifeSupp, TaskTypes.RestoreOxy }, { SystemTypes.Comms, TaskTypes.FixComms }, + { SystemTypes.MushroomMixupSabotage, TaskTypes.MushroomMixupSabotage } }; private static Dictionary _systemTriggerPairs = new() { { SystemTypes.Electrical, "onLightsStart" }, { SystemTypes.Reactor, "onReactorStart" }, { SystemTypes.LifeSupp, "onOxygenStart" }, - { SystemTypes.Comms, "onCommsStart" } + { SystemTypes.Comms, "onCommsStart" }, + { SystemTypes.MushroomMixupSabotage, "onMixupStart" } }; public static bool Prefix([HarmonyArgument(0)] SystemTypes systemType) @@ -66,7 +68,8 @@ public static class SabEndPatch { TaskTypes.FixLights, "onLightsEnd" }, { TaskTypes.ResetReactor, "onReactorEnd" }, { TaskTypes.RestoreOxy, "onOxygenEnd" }, - { TaskTypes.FixComms, "onCommsEnd" } + { TaskTypes.FixComms, "onCommsEnd" }, + { TaskTypes.MushroomMixupSabotage, "onMixupEnd" } }; public static void Postfix([HarmonyArgument(0)] PlayerTask task) @@ -75,7 +78,7 @@ public static void Postfix([HarmonyArgument(0)] PlayerTask task) return; if (!_taskTriggerPairs.ContainsKey(task.TaskType)) return; - + // Fire Trigger string triggerName = _taskTriggerPairs[task.TaskType]; LITriggerable.Trigger(SabotageOptionsBuilder.TriggerObject, triggerName, null); diff --git a/LevelImposter/Core/Patches/SporePatch.cs b/LevelImposter/Core/Patches/SporePatch.cs new file mode 100644 index 0000000..7fcd9a1 --- /dev/null +++ b/LevelImposter/Core/Patches/SporePatch.cs @@ -0,0 +1,43 @@ +using HarmonyLib; +using Hazel; +using LevelImposter.Builders; + +namespace LevelImposter.Core +{ + /* + * Normally, spore are handled + * by FungleShipStatus. This bypasses that + * requirement by supplying it's own + * spore listings. + */ + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] + public static class SporePatchHandle + { + public static bool Prefix( + [HarmonyArgument(0)] byte callId, + [HarmonyArgument(1)] MessageReader reader, + PlayerControl __instance) + { + if (LIShipStatus.Instance == null) + return true; + if (callId != (byte)RpcCalls.TriggerSpores || callId != (byte)RpcCalls.CheckSpore) + return true; + + // Find spores + int mushroomId = reader.ReadPackedInt32(); + if (mushroomId < 0 || mushroomId >= SporeBuilder.Mushrooms.Count) + { + LILogger.Warn($"[RPC] Could not find a mushroom with ID {mushroomId}"); + return true; + } + + // Run method + if (callId == (byte)RpcCalls.TriggerSpores) + SporeBuilder.Mushrooms[mushroomId].TriggerSpores(); + else + __instance.CheckSporeTrigger(SporeBuilder.Mushrooms[mushroomId]); + + return false; + } + } +} diff --git a/LevelImposter/Core/Patches/TaskInitializePatch.cs b/LevelImposter/Core/Patches/TaskInitializePatch.cs index 235a914..fb3bdfb 100644 --- a/LevelImposter/Core/Patches/TaskInitializePatch.cs +++ b/LevelImposter/Core/Patches/TaskInitializePatch.cs @@ -1,6 +1,7 @@ -using HarmonyLib; -using UnityEngine; +using HarmonyLib; using LevelImposter.Builders; +using System.Linq; +using UnityEngine; namespace LevelImposter.Core { @@ -149,7 +150,7 @@ public static bool Prefix([HarmonyArgument(0)] SpriteRenderer folder, RecordsMin folder.gameObject.SetActive(false); __instance.MyNormTask.Data[folderIndex] = IntRange.NextByte(1, TaskConsoleBuilder.RecordsCount); - __instance.MyNormTask.UpdateArrow(); + __instance.MyNormTask.UpdateArrowAndLocation(); if (Constants.ShouldPlaySfx()) SoundManager.Instance.PlaySound(__instance.grabDocument, false, 1f); __instance.StartCoroutine(__instance.CoStartClose(0.75f)); @@ -157,4 +158,61 @@ public static bool Prefix([HarmonyArgument(0)] SpriteRenderer folder, RecordsMin return false; } } + + /* + * Fix hard-coded "Replace Parts" Updater + */ + [HarmonyPatch(typeof(NormalPlayerTask), nameof(NormalPlayerTask.UpdateArrowAndLocation))] + public static class PartsPatch + { + public static bool Prefix(NormalPlayerTask __instance) + { + if (LIShipStatus.Instance == null) + return true; + if (__instance.TaskType != TaskTypes.ReplaceParts) + return true; + if (!__instance.Arrow || !__instance.Owner.AmOwner || __instance.IsComplete) + return true; + + // Pick Next Console + var list = NormalPlayerTask.PickRandomConsoles(__instance.taskStep, TaskTypes.ReplaceParts); + if (list.Count <= 0) + { + LILogger.Warn("No consoles found of task-replaceparts2"); + return false; + } + + // Update Arrow + __instance.Arrow.target = list[0].transform.position; + __instance.StartAt = list[0].Room; + __instance.Data[0] = (byte)list[0].ConsoleId; + __instance.LocationDirty = true; + return false; + } + } + + + /* + * Fix a bug with the "Catch Fish" Console + */ + [HarmonyPatch(typeof(NormalPlayerTask), nameof(NormalPlayerTask.ValidConsole))] + public static class FishPatch + { + public static bool Prefix( + [HarmonyArgument(0)] Console console, + NormalPlayerTask __instance, + ref bool __result) + { + if (LIShipStatus.Instance == null) + return true; + if (__instance.TaskType != TaskTypes.CatchFish) + return true; + + // Replace Result + __result = console.Room == __instance.StartAt && + console.ValidTasks.Any((TaskSet set) => set.taskType == __instance.TaskType && set.taskStep.Contains(__instance.taskStep)) && + console.TaskTypes.Contains(__instance.TaskType); + return false; + } + } } \ No newline at end of file diff --git a/LevelImposter/Core/Utils/FileChunk.cs b/LevelImposter/Core/Utils/FileChunk.cs new file mode 100644 index 0000000..0b8aa70 --- /dev/null +++ b/LevelImposter/Core/Utils/FileChunk.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace LevelImposter.Core +{ + public class FileChunk + { + private string _filePath; + private long _offset = -1; + private long _length = -1; + + public FileChunk(string filePath, long offset, long length) + { + _filePath = filePath; + _offset = offset; + _length = length; + } + + /// + /// Opens a stream to the file chunk. + /// + /// A stream to the cooresponding file chunk + public FileChunkStream OpenStream() + { + FileStream fileStream = File.OpenRead(_filePath); + return new FileChunkStream(fileStream, _offset, _length); + } + } +} diff --git a/LevelImposter/Core/Utils/FileChunkStream.cs b/LevelImposter/Core/Utils/FileChunkStream.cs new file mode 100644 index 0000000..4e6d1e0 --- /dev/null +++ b/LevelImposter/Core/Utils/FileChunkStream.cs @@ -0,0 +1,90 @@ +using System.IO; + +namespace LevelImposter.Core +{ + public class FileChunkStream : Stream + { + private FileStream _fileStream; + private long _offset = -1; + private long _position = 0; + private long _length = -1; + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length => _length; + public override long Position + { + get => _position; + set => Seek(value, SeekOrigin.Begin); + } + + public FileChunkStream(FileStream fileStream, long offset, long length) + { + _fileStream = fileStream; + _offset = offset; + _length = length; + } + + public override void Flush() + { + // Nothing to flush + } + + public override int Read(byte[] buffer, int offset, int count) + { + _fileStream.Seek(_offset + _position, SeekOrigin.Begin); + int read = _fileStream.Read(buffer, offset, count); + _position += read; + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + _position = offset; + break; + case SeekOrigin.Current: + _position += offset; + break; + case SeekOrigin.End: + _position = _length - offset; + break; + } + return _position; + } + + public override void SetLength(long value) + { + // Length is read-only + throw new System.NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + // Write is not supported + throw new System.NotImplementedException(); + } + + public override string ToString() + { + byte[] buffer = new byte[_length]; + Read(buffer, 0, (int)_length); + return System.Text.Encoding.UTF8.GetString(buffer); + } + + public override void Close() + { + _fileStream.Close(); + base.Close(); + } + + protected override void Dispose(bool disposing) + { + _fileStream.Dispose(); + base.Dispose(disposing); + } + } +} diff --git a/LevelImposter/Core/Utils/GIFFile.cs b/LevelImposter/Core/Utils/GIFFile.cs index 95ed99d..225aa92 100644 --- a/LevelImposter/Core/Utils/GIFFile.cs +++ b/LevelImposter/Core/Utils/GIFFile.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; @@ -48,6 +47,38 @@ public GIFFile(string name) Frames = new(); } + /// + /// Checks if the given stream is a GIF file. Keeps the stream open. + /// + /// Stream of raw image data + /// True if the Stream is a GIF file. False otherwise + public static bool IsGIF(Stream dataStream) + { + using (var reader = new BinaryReader(dataStream, System.Text.Encoding.ASCII, true)) + { + try + { + // Read Header + var header = reader.ReadBytes(6); + if (header.Length != 6) + return false; + reader.BaseStream.Position = 0; + + // Check Header + return header[0] == 'G' && + header[1] == 'I' && + header[2] == 'F' && + header[3] == '8' && + (header[4] == '7' || header[4] == '9') && + header[5] == 'a'; + } + catch + { + return false; + } + } + } + /// /// Sets the pivot point for all frame sprites. /// @@ -436,6 +467,10 @@ private List DecodeLZW(List byteBuffer, byte minCodeSize, int expe while (indexStream.Count < expectedSize) indexStream.Add(0); + // Free Memory + for (int k = endOfInformationCode + 1; k < _codeTable.Length; k++) + _codeTable[k] = null; + return indexStream; } diff --git a/LevelImposter/Core/Utils/MapUtils.cs b/LevelImposter/Core/Utils/MapUtils.cs index 638339c..0394e3f 100644 --- a/LevelImposter/Core/Utils/MapUtils.cs +++ b/LevelImposter/Core/Utils/MapUtils.cs @@ -1,18 +1,18 @@ +using AmongUs.GameOptions; +using Il2CppInterop.Runtime; +using Il2CppInterop.Runtime.InteropTypes; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; using System; +using System.Collections; using System.Collections.Generic; -using System.Text; -using System.Text.Json; +using System.IO; using System.Linq; using System.Reflection; +using System.Text; +using System.Text.Json; using UnityEngine; -using System.IO; -using Il2CppInterop.Runtime.InteropTypes.Arrays; -using Il2CppInterop.Runtime.InteropTypes; -using Il2CppInterop.Runtime; -using AmongUs.GameOptions; -using Reactor.Utilities; -using System.Collections; -using Reactor.Utilities.Extensions; namespace LevelImposter.Core { @@ -176,6 +176,40 @@ public static Il2CppStructArray ParseBase64(string base64) return Convert.FromBase64String(sub64); } + /// + /// Parses a base64 encoded file chunk into a byte array + /// + /// Base64 File Chunk + /// True if the file chunk is a GIF. False otherwise + /// A byte array of the base64 data + public static Il2CppStructArray ParseBase64(FileChunk? chunk, out bool isGIF) + { + if (chunk == null) + { + isGIF = false; + return new Il2CppStructArray(0); + } + + using (var stream = chunk.OpenStream()) + using (var reader = new StreamReader(stream)) + { + // Read buffer to comma + string buffer = ""; + while (reader.Peek() != -1) + { + char c = (char)reader.Read(); + if (c == ',') + break; + buffer += c; + } + isGIF = buffer.ToLower() == "data:image/gif;base64"; + + // Read rest of stream + string sub64 = reader.ReadToEnd(); + return Convert.FromBase64String(sub64); + } + } + /// /// Checks if a GameObject is the local player /// @@ -234,7 +268,7 @@ public static Mesh Build2DMesh(float width, float height) return resourceData; } } - + private static Il2CppStructArray? GetResourceAsIl2Cpp(string name) { Assembly assembly = Assembly.GetExecutingAssembly(); @@ -243,7 +277,7 @@ public static Mesh Build2DMesh(float width, float height) if (resourceStream == null) return null; - var length = (int) resourceStream.Length; + var length = (int)resourceStream.Length; Il2CppStructArray resourceData = new Il2CppStructArray(length); resourceStream.AsIl2Cpp().Read(resourceData, 0, length); return resourceData; @@ -261,7 +295,7 @@ public static Mesh Build2DMesh(float width, float height) Assembly assembly = Assembly.GetExecutingAssembly(); string? resourceName = assembly.GetManifestResourceNames().FirstOrDefault(str => str.EndsWith(name)); using Stream? assetStream = assembly.GetManifestResourceStream(resourceName ?? ""); - + if (assetStream == null) return null; @@ -342,6 +376,25 @@ private static IEnumerator CoWaitForShip(float timeout, Action onFinish) } } + /// + /// Waits for a specific amount of frames, then calls Action + /// + /// Amount of frames to wait + /// Action to perform on completion + public static void WaitForFrames(int frames, Action onFinish) + { + Coroutines.Start(CoWaitForFrames(frames, onFinish)); + } + private static IEnumerator CoWaitForFrames(int frames, Action onFinish) + { + { + for (int i = 0; i < frames; i++) + yield return null; + onFinish.Invoke(); + onFinish = null; + } + } + /// /// Searches an array of LISounds /// for a specific sound by type @@ -369,8 +422,8 @@ private static IEnumerator CoWaitForShip(float timeout, Action onFinish) /// obj's SpriteRenderer public static SpriteRenderer CloneSprite(GameObject obj, GameObject prefab, bool isSpriteAnim = false) { - var prefabRenderer = prefab.GetComponent(); - var spriteRenderer = obj.GetComponent(); + var prefabRenderer = prefab.GetComponentInChildren(); + var spriteRenderer = obj.GetComponentInChildren(); if (!spriteRenderer) { spriteRenderer = obj.AddComponent(); @@ -378,7 +431,7 @@ public static SpriteRenderer CloneSprite(GameObject obj, GameObject prefab, bool if (isSpriteAnim) { - var prefabAnim = prefab.GetComponent(); + var prefabAnim = prefab.GetComponentInChildren(); var spriteAnim = obj.AddComponent(); spriteAnim.m_defaultAnim = prefabAnim.m_defaultAnim; spriteAnim.m_speed = prefabAnim.m_speed; diff --git a/LevelImposter/Core/Utils/ModCompatibility.cs b/LevelImposter/Core/Utils/ModCompatibility.cs index e6ff5bf..402dacf 100644 --- a/LevelImposter/Core/Utils/ModCompatibility.cs +++ b/LevelImposter/Core/Utils/ModCompatibility.cs @@ -8,34 +8,40 @@ public static class ModCompatibility public const string REACTOR_ID = "gg.reactor.api"; public const string TOR_GUID = "me.eisbison.theotherroles"; public const string TOU_GUID = "com.slushiegoose.townofus"; + public const string REW_GUID = "me.alchlcdvl.reworked"; public const string SUBMERGED_GUID = "Submerged"; public const ShipStatus.MapType SUBMERGED_MAP_TYPE = (ShipStatus.MapType)5; private static bool _isTOREnabled = false; private static bool _isTOUEnabled = false; private static bool _isSubmergedEnabled = false; + private static bool _isReworkedEnabled = false; public static bool IsTOREnabled => _isTOREnabled; public static bool IsTOUEnabled => _isTOUEnabled; public static bool IsSubmergedEnabled => _isSubmergedEnabled; + public static bool IsReworkedEnabled => _isReworkedEnabled; public static void Init() { _isTOREnabled = IsPlugin(TOR_GUID); _isTOUEnabled = IsPlugin(TOU_GUID); _isSubmergedEnabled = IsPlugin(SUBMERGED_GUID); + _isReworkedEnabled = IsPlugin(REW_GUID); if (_isTOREnabled) LILogger.Info("LevelImposter detected TOR installed, compatibility enabled"); if (_isTOUEnabled) LILogger.Info("LevelImposter detected TOU installed, compatibility enabled"); + if (_isReworkedEnabled) + LILogger.Info("LevelImposter detected Reworked installed, compatibility enabled"); if (_isSubmergedEnabled) LILogger.Info("LevelImposter detected Submerged installed, currently unsupported"); } private static bool IsPlugin(string guid) { - return IL2CPPChainloader.Instance.Plugins.TryGetValue(guid, out PluginInfo _); + return IL2CPPChainloader.Instance.Plugins.TryGetValue(guid, out _); } } } diff --git a/LevelImposter/Core/Utils/RenameHandler.cs b/LevelImposter/Core/Utils/RenameHandler.cs index d8b461a..2b4c35a 100644 --- a/LevelImposter/Core/Utils/RenameHandler.cs +++ b/LevelImposter/Core/Utils/RenameHandler.cs @@ -9,6 +9,7 @@ public class RenameHandler { private Dictionary _systemRenames = new(); private Dictionary _taskRenames = new(); + private Dictionary _stringRenames = new(); /// /// Renames a SystemType in the TranslationController @@ -30,6 +31,16 @@ public void Add(TaskTypes task, string name) _taskRenames[task] = name; } + /// + /// Renames a StringNames in the TranslationController + /// + /// String name to rename + /// String to rename to + public void Add(StringNames stringName, string name) + { + _stringRenames[stringName] = name; + } + /// /// Gets a SystemType to rename /// @@ -41,9 +52,17 @@ public void Add(TaskTypes task, string name) /// Gets a TaskType to rename /// /// Task to rename - /// String top replace task with + /// String to replace task with public string Get(TaskTypes task) => _taskRenames[task]; + /// + /// Gets a StringName to rename + /// + /// StringName to rename + /// String to replace text with + public string Get(StringNames stringNames) => _stringRenames[stringNames]; + + /// /// Checks if the system should be renamed /// @@ -54,10 +73,17 @@ public void Add(TaskTypes task, string name) /// /// Checks if the task should be renamed /// - /// Task to rename + /// Task to rename /// True iff the task should be renamed public bool Contains(TaskTypes task) => _taskRenames.ContainsKey(task); + /// + /// Checks if the task should be renamed + /// + /// StringNames to rename + /// True iff the task should be renamed + public bool Contains(StringNames stringName) => _stringRenames.ContainsKey(stringName); + /// /// Clears all renamed values /// @@ -65,6 +91,7 @@ public void Clear() { _systemRenames.Clear(); _taskRenames.Clear(); + _stringRenames.Clear(); } } } diff --git a/LevelImposter/Core/Utils/WAVFile.cs b/LevelImposter/Core/Utils/WAVFile.cs index eabc9cc..185a260 100644 --- a/LevelImposter/Core/Utils/WAVFile.cs +++ b/LevelImposter/Core/Utils/WAVFile.cs @@ -18,25 +18,39 @@ public class WAVFile : IDisposable private float[]? _data = null; private AudioClip? _clip = null; + public static AudioClip? LoadSound(LISound? soundData) + { + // Get Sound Data + if (soundData == null) + return null; + + // Get Asset DB + var mapAssetDB = LIShipStatus.Instance?.CurrentMap?.mapAssetDB; + if (mapAssetDB == null) + return null; + + // Get Sound Stream + var soundDBElem = mapAssetDB.Get(soundData.dataID); + if (soundDBElem == null) + return null; + + // Get Sound Data + using (var stream = soundDBElem.OpenStream()) + return LoadStream(stream); + } + /// /// Loads a WAV file from the given base64 string and adds to map's GC list /// /// Base64 string to load from /// A Unity AudioClip - public static AudioClip? Load(string? base64) + public static AudioClip? LoadStream(Stream dataStream) { try { - if (base64 == null) - throw new ArgumentNullException("Wave file base64 string cannot be null"); - if (!base64.StartsWith("data:audio/wav;base64,")) - throw new ArgumentException("Base64 string is not a WAV file"); - // Load File var wavFile = new WAVFile(); - var data = Convert.FromBase64String(base64.Substring(22)); - using (var stream = new MemoryStream(data)) - wavFile.Load(stream); + wavFile.Load(dataStream); // Add to GC list GCHandler.Register(wavFile); diff --git a/LevelImposter/DB/AssetDB.cs b/LevelImposter/DB/AssetDB.cs index 84884ca..1135980 100644 --- a/LevelImposter/DB/AssetDB.cs +++ b/LevelImposter/DB/AssetDB.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.ResourceManagement.AsyncOperations; -using UnityEngine.AddressableAssets; +using BepInEx.Unity.IL2CPP.Utils.Collections; +using Il2CppInterop.Runtime.Attributes; using LevelImposter.Core; using System.Collections; -using BepInEx.Unity.IL2CPP.Utils.Collections; -using Il2CppInterop.Runtime.Attributes; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; namespace LevelImposter.DB { @@ -121,7 +121,7 @@ private IEnumerator CoLoadAssets() var miraPrefab = shipPrefabs[(int)MapType.Mira]; int mapCount = (int)MapType.LevelImposter; while (shipPrefabs.Count <= mapCount) - shipPrefabs.Add(miraPrefab); // Use Own Ship AssetReference + shipPrefabs.Add(miraPrefab); // TODO: Use Own Ship AssetReference while (Constants.MapNames.Count <= mapCount) Constants.MapNames = MapUtils.AddToArr(Constants.MapNames, Constants.MapNames.Count == mapCount ? LIConstants.MAP_NAME : ""); @@ -200,6 +200,7 @@ private void LoadShip(GameObject prefab) "MiraShip" => MapType.Mira, "PolusShip" => MapType.Polus, "Airship" => MapType.Airship, + "FungleShip" => MapType.Fungle, _ => MapType.LevelImposter }; if (mapType == MapType.LevelImposter) @@ -215,7 +216,7 @@ private void LoadShip(GameObject prefab) LILogger.Info($"...{prefab.name} Loaded"); } - + public void Awake() { if (Instance != null) diff --git a/LevelImposter/DB/Sub/TaskDB.cs b/LevelImposter/DB/Sub/TaskDB.cs index 8c5bf15..cdcd432 100644 --- a/LevelImposter/DB/Sub/TaskDB.cs +++ b/LevelImposter/DB/Sub/TaskDB.cs @@ -1,8 +1,8 @@ -using LevelImposter.Core; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using LevelImposter.Core; using System; using System.Linq; using System.Text.Json.Serialization; -using Il2CppInterop.Runtime.InteropTypes.Arrays; namespace LevelImposter.DB { @@ -25,7 +25,7 @@ public override void LoadShip(ShipStatus shipStatus, MapType mapType) { TaskLength.Common => shipStatus.CommonTasks.Cast>(), TaskLength.Long => shipStatus.LongTasks.Cast>(), - TaskLength.Short => shipStatus.NormalTasks.Cast>(), + TaskLength.Short => shipStatus.ShortTasks.Cast>(), _ => shipStatus.SpecialTasks }; diff --git a/LevelImposter/LevelImposter.cs b/LevelImposter/LevelImposter.cs index b7fae71..584326c 100644 --- a/LevelImposter/LevelImposter.cs +++ b/LevelImposter/LevelImposter.cs @@ -1,9 +1,9 @@ using BepInEx; using BepInEx.Unity.IL2CPP; +using Il2CppInterop.Runtime.Injection; using LevelImposter.Core; using LevelImposter.DB; using LevelImposter.Shop; -using Il2CppInterop.Runtime.Injection; using Reactor.Networking.Attributes; namespace LevelImposter @@ -13,6 +13,7 @@ namespace LevelImposter [BepInDependency(ModCompatibility.SUBMERGED_GUID, BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency(ModCompatibility.TOU_GUID, BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency(ModCompatibility.TOR_GUID, BepInDependency.DependencyFlags.SoftDependency)] + [BepInDependency(ModCompatibility.REW_GUID, BepInDependency.DependencyFlags.SoftDependency)] [ReactorModFlags(Reactor.Networking.ModFlags.RequireOnAllClients)] [BepInProcess("Among Us.exe")] public partial class LevelImposter : BasePlugin @@ -29,7 +30,7 @@ public override void Load() FileCache.Init(); LIDeepLink.Init(); ModCompatibility.Init(); - + // IUsable Interface RegisterTypeOptions usableInterface = new() { @@ -58,7 +59,6 @@ public override void Load() ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); - ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); diff --git a/LevelImposter/LevelImposter.csproj b/LevelImposter/LevelImposter.csproj index 346c3d1..9245f63 100644 --- a/LevelImposter/LevelImposter.csproj +++ b/LevelImposter/LevelImposter.csproj @@ -1,10 +1,10 @@ - - LevelImposter - 0.16.1 - Custom Among Us Mapping Studio - DigiWorm - + + LevelImposter + 0.17.0 + Custom Among Us Mapping Studio + DigiWorm + net6.0 latest embedded @@ -13,11 +13,11 @@ C:\Program Files (x86)\Steam\steamapps\common\Among Us - 2023.7.11 + 2023.10.24 - + diff --git a/LevelImposter/Shop/Components/FileHandler.cs b/LevelImposter/Shop/Components/FileHandler.cs deleted file mode 100644 index 51e6958..0000000 --- a/LevelImposter/Shop/Components/FileHandler.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.IO; -using System.Collections; -using UnityEngine; -using BepInEx.Unity.IL2CPP.Utils.Collections; -using System.Text.Json; -using System.Diagnostics; -using Il2CppInterop.Runtime.Attributes; - -namespace LevelImposter.Shop -{ - /// - /// Handles async File IO within Unity - /// - public class FileHandler : MonoBehaviour - { - public FileHandler(IntPtr intPtr) : base(intPtr) - { - } - - public static FileHandler? Instance = null; - - private const float MIN_FRAMERATE = 30.0f; - private Stopwatch _loadTimer = new(); - private bool _shouldLoad - { - get { return _loadTimer.ElapsedMilliseconds <= (1000.0f / MIN_FRAMERATE); } - } - - /// - /// Coroutine to handle File IO - /// - /// Path to read file from - /// Callback on success with deserialized file data - /// Callback on error with error info - /// - [HideFromIl2Cpp] - private IEnumerator CoGet(string filePath, Action? onSuccess, Action? onError) - { - { - // Wait for timing - while (!_shouldLoad) - yield return null; - - // Check if file exists - if (!File.Exists(filePath)) - { - if (onError != null) - onError($"Could not find {filePath} in filesystem"); - } - else - { - // Read/Deserialize file - using FileStream mapStream = File.OpenRead(filePath); - T? mapData = JsonSerializer.Deserialize(mapStream); - if (mapData == null) - { - if (onError != null) - onError($"Failed to read {filePath} from filesystem"); - } - else - { - if (onSuccess != null) - onSuccess(mapData); - } - } - - // Free memory (BepInEx coroutines are buggy af, GC is not called) - filePath = ""; - onSuccess = null; - onError = null; - } - } - - /// - /// Gets a file from the filesystem and deserializes it - /// - /// Type to deserialize to - /// File path to read from - /// Callback on success - /// Callback on error - [HideFromIl2Cpp] - public void Get(string filePath, Action? onSuccess, Action? onError) - { - StartCoroutine(CoGet(filePath, onSuccess, onError).WrapToIl2Cpp()); - } - - public void Awake() - { - if (Instance == null) - { - Instance = this; - DontDestroyOnLoad(gameObject); - } - else - { - Destroy(gameObject); - } - } - public void Update() - { - _loadTimer.Restart(); - } - } -} \ No newline at end of file diff --git a/LevelImposter/Shop/Components/ShopManager.cs b/LevelImposter/Shop/Components/ShopManager.cs index e30dedd..a3beee5 100644 --- a/LevelImposter/Shop/Components/ShopManager.cs +++ b/LevelImposter/Shop/Components/ShopManager.cs @@ -111,18 +111,24 @@ public void SetTab(Tab tab) /// private void SetDownloadsTab() { - Clear(); - string[] mapIDs = MapFileAPI.ListIDs() ?? new string[0]; - foreach (string mapID in mapIDs) - { - MapFileAPI.GetMetadata(mapID, OnDownloadsResponse); - } + StartCoroutine(CoSetDownloadsTab().WrapToIl2Cpp()); } [HideFromIl2Cpp] - private void OnDownloadsResponse(LIMetadata? metadata) + private IEnumerator CoSetDownloadsTab() { - if (metadata != null && _currentTab == Tab.Downloads) - AddBanner(metadata); + { + yield return LegacyConverter.ConvertAllMaps().WrapToIl2Cpp(); + yield return null; + Clear(); + string[] mapIDs = MapFileAPI.ListIDs() ?? new string[0]; + foreach (string mapID in mapIDs) + { + var metadata = MapFileAPI.GetMetadata(mapID); + if (metadata != null) + AddBanner(metadata); + yield return null; + } + } } /// @@ -245,7 +251,7 @@ public static void RegenerateFallbackMap() if (Instance != null) Instance._shouldRegenerateFallback = true; } - + /// /// Toggles the overlay /// @@ -254,7 +260,7 @@ public void SetOverlayEnabled(bool isEnabled) { _overlay?.SetActive(isEnabled); } - + /// /// Modifies the text of the overlay /// @@ -309,7 +315,7 @@ public void Awake() _title = _scroller?.transform.Find("Inner/Title").GetComponent(); _tabs = transform.Find("Header/Tabs").GetComponent(); _bannerPrefab = _scroller?.transform.Find("Inner/MapBanner").GetComponent(); - + } public void Start() { @@ -346,7 +352,7 @@ public void OnDestroy() { ControllerManager.Instance.CloseOverlayMenu(SHOP_NAME); Instance = null; - + _overlay = null; _overlayText = null; _scroller = null; diff --git a/LevelImposter/Shop/IO/LIDeserializer.cs b/LevelImposter/Shop/IO/LIDeserializer.cs new file mode 100644 index 0000000..2d2e45e --- /dev/null +++ b/LevelImposter/Shop/IO/LIDeserializer.cs @@ -0,0 +1,85 @@ +using LevelImposter.Core; +using System; +using System.IO; +using System.Text.Json; + +namespace LevelImposter.Shop +{ + public static class LIDeserializer + { + public static string? CurrentFilePath = null; + + public static LIMap DeserializeMap(Stream dataStream, bool spriteDB = true) + { + // Parse Legacy + byte firstByte = (byte)dataStream.ReadByte(); + dataStream.Position = 0; + if (firstByte == '{') + { + dataStream.Position = 0; + var legacyMap = JsonSerializer.Deserialize(dataStream); + if (legacyMap == null) + LILogger.Error("Failed to deserialize legacy map data"); + LegacyConverter.UpdateMap(legacyMap); + return legacyMap; + } + + // Map Data Length + byte[] mapLengthBytes = new byte[4]; + dataStream.Read(mapLengthBytes, 0, 4); + int mapLength = BitConverter.ToInt32(mapLengthBytes, 0); + + // Read Map Data + byte[] mapDataBytes = new byte[mapLength]; + dataStream.Read(mapDataBytes, 0, mapLength); + string mapDataString = System.Text.Encoding.UTF8.GetString(mapDataBytes); + LIMap? mapData = JsonSerializer.Deserialize(mapDataString); + + // Check Map Data + if (mapData == null) + throw new Exception("Failed to deserialize map data"); + + // Abort if no SpriteDB + if (!spriteDB) + return mapData; + + // Read SpriteDB + mapData.mapAssetDB = new(); + while (dataStream.Position < dataStream.Length) + { + // Read ID + byte[] idBytes = new byte[36]; + dataStream.Read(idBytes, 0, 36); + string idString = System.Text.Encoding.UTF8.GetString(idBytes); + bool isValidGUID = Guid.TryParse(idString, out Guid spriteID); + if (!isValidGUID) + { + LILogger.Error($"Failed to parse sprite ID: {idString}"); + continue; + } + + // Read Length + byte[] lengthBytes = new byte[4]; + dataStream.Read(lengthBytes, 0, 4); + int dataLength = BitConverter.ToInt32(lengthBytes, 0); + + // Check Length + if (dataLength <= 0) + { + LILogger.Error($"Invalid data length: {dataLength}"); + continue; + } + + // Save Chunk + mapData.mapAssetDB.DB[spriteID] = new MapAssetDB.DBElement() + { + fileChunk = new FileChunk(CurrentFilePath ?? "", dataStream.Position, dataLength) + }; + dataStream.Position += dataLength; + } + + // Return + return mapData; + } + } +} diff --git a/LevelImposter/Shop/IO/LISerializer.cs b/LevelImposter/Shop/IO/LISerializer.cs new file mode 100644 index 0000000..c6b880d --- /dev/null +++ b/LevelImposter/Shop/IO/LISerializer.cs @@ -0,0 +1,59 @@ +using LevelImposter.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace LevelImposter.Shop +{ + public static class LISerializer + { + private static JsonSerializerOptions? _options = null; + + /// + /// Serializes a map into a string + /// + /// Map Data to serialize + /// Raw LIM2 file data + public static MemoryStream SerializeMap(LIMap mapData) + { + // Create Options + if (_options == null) + { + _options = new(); + _options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + } + + // Open Stream + MemoryStream stream = new(); + + // Update Legacy Format + if (mapData.isLegacy) + LegacyConverter.UpdateMap(mapData); + + // Map Data + byte[] mapJsonBytes = JsonSerializer.SerializeToUtf8Bytes(mapData, _options); + stream.Write(BitConverter.GetBytes(mapJsonBytes.Length)); + stream.Write(mapJsonBytes); + + // SpriteDB + if (mapData.mapAssetDB != null) + { + foreach (KeyValuePair sprite in mapData.mapAssetDB.DB) + { + var data = sprite.Value.ToBytes(); + var idBytes = System.Text.Encoding.UTF8.GetBytes(sprite.Key.ToString()); + + // Write Element + stream.Write(idBytes); + stream.Write(BitConverter.GetBytes(data.Length)); + stream.Write(data); + } + } + + // Return + stream.Position = 0; + return stream; + } + } +} diff --git a/LevelImposter/Shop/IO/LegacyConverter.cs b/LevelImposter/Shop/IO/LegacyConverter.cs new file mode 100644 index 0000000..eb3b606 --- /dev/null +++ b/LevelImposter/Shop/IO/LegacyConverter.cs @@ -0,0 +1,152 @@ +using Il2CppInterop.Runtime.Attributes; +using LevelImposter.Core; +using System; +using System.Collections; +using System.IO; +using System.Text.Json; + +namespace LevelImposter.Shop +{ + /// + /// Converts LIM to LIM2 files + /// + public static class LegacyConverter + { + public static string GetLegacyPath(string mapID) + { + return Path.Combine(MapFileAPI.GetDirectory(), $"{mapID}.lim"); + } + + /// + /// Auto-Updates all downloaded maps + /// + /// + [HideFromIl2Cpp] + public static IEnumerator ConvertAllMaps() + { + { + string[] legacyMapIDs = Directory.GetFiles(MapFileAPI.GetDirectory(), "*.lim"); + foreach (string legacyMapID in legacyMapIDs) + { + string mapID = Path.GetFileNameWithoutExtension(legacyMapID); + if (!MapFileAPI.Exists(mapID)) + { + ShopManager.Instance?.SetOverlayEnabled(true); + ShopManager.Instance?.SetOverlayText($"Converting legacy maps...\n{mapID}"); + yield return null; + ConvertFile(mapID); + } + } + ShopManager.Instance?.SetOverlayEnabled(false); + } + } + +#pragma warning disable CS0618 // Handles legacy properties + /// + /// Updates legacy map data to a LIM2 data + /// + /// Legacy Map Data + public static void UpdateMap(LIMap map) + { + if (!map.isLegacy) + return; + + LILogger.Info($"Converting legacy map data [{map.id}]"); + + // Update Properties + map.isLegacy = false; + map.mapAssetDB = new(); + + // SpriteDB + foreach (LIElement element in map.elements) + { + // Add Sprite Data + if (element.properties.spriteData != null) + { + Guid spriteID = Guid.NewGuid(); + var spriteData = MapUtils.ParseBase64(element.properties.spriteData); + map.mapAssetDB.Add(spriteID, spriteData); + element.properties.spriteID = spriteID; + element.properties.spriteData = null; + } + + // Add Minigame Data + if (element.properties.minigames != null) + { + foreach (var minigame in element.properties.minigames) + { + var spriteData = MapUtils.ParseBase64(minigame.spriteData ?? ""); + if (spriteData != null) + { + Guid spriteID = Guid.NewGuid(); + map.mapAssetDB.Add(spriteID, spriteData); + minigame.spriteID = spriteID; + } + minigame.spriteData = null; + } + } + + // Add Sound Data + if (element.properties.sounds != null) + { + foreach (var sound in element.properties.sounds) + { + if (sound.isPreset) + { + sound.presetID = sound.data; + } + else + { + var soundData = MapUtils.ParseBase64(sound.data ?? ""); + if (soundData != null) + { + Guid soundID = Guid.NewGuid(); + map.mapAssetDB.Add(soundID, soundData); + sound.dataID = soundID; + } + } + sound.data = null; + } + } + + // TODO: Search for duplicate entries + } + } +#pragma warning restore CS0618 + + public static void ConvertFile(string mapID) + { + LILogger.Info($"Converting legacy map file [{mapID}]"); + + // Get paths + string legacyPath = GetLegacyPath(mapID); + string newPath = MapFileAPI.GetPath(mapID); + + // Check if files exist + if (!File.Exists(legacyPath)) + throw new FileNotFoundException($"Could not find legacy map file {legacyPath}"); + if (File.Exists(newPath)) + throw new FileLoadException($"Map file {newPath} already exists"); + + // Read legacy file + LIMap? mapFile; + using (FileStream legacyFileStream = File.OpenRead(legacyPath)) + mapFile = JsonSerializer.Deserialize(legacyFileStream); + + // Check if file is valid + if (mapFile == null) + throw new FileLoadException($"Could not deserialize legacy map file {legacyPath}"); + + // Update map + UpdateMap(mapFile); + + // Serialize & Write to new file + using (var dataStream = LISerializer.SerializeMap(mapFile)) + using (FileStream outputFileStream = File.OpenWrite(newPath)) + dataStream.CopyTo(outputFileStream); + + // Delete legacy file + File.Move(legacyPath, $"{legacyPath}.bak"); + } + } +} diff --git a/LevelImposter/Shop/Patches/VersionPatch.cs b/LevelImposter/Shop/Patches/VersionPatch.cs index 7aa1901..be16c49 100644 --- a/LevelImposter/Shop/Patches/VersionPatch.cs +++ b/LevelImposter/Shop/Patches/VersionPatch.cs @@ -1,9 +1,8 @@ -using TMPro; -using HarmonyLib; +using HarmonyLib; using LevelImposter.Core; using System; +using TMPro; using UnityEngine; -using UnityEngine.SceneManagement; namespace LevelImposter.Shop { @@ -33,7 +32,7 @@ public static void Postfix() AspectPosition logoPosition = _versionObject.AddComponent(); logoPosition.Alignment = AspectPosition.EdgeAlignments.Right; - logoPosition.DistanceFromEdge = new Vector3(1.4f, -2.3f, 0); + logoPosition.DistanceFromEdge = new Vector3(1.8f, -2.3f, 0); logoPosition.AdjustPosition(); SpriteRenderer logoRenderer = _versionObject.AddComponent(); diff --git a/LevelImposter/Shop/Util/GitHubAPI.cs b/LevelImposter/Shop/Util/GitHubAPI.cs index 8edec8c..cdb7236 100644 --- a/LevelImposter/Shop/Util/GitHubAPI.cs +++ b/LevelImposter/Shop/Util/GitHubAPI.cs @@ -1,9 +1,8 @@ +using Il2CppInterop.Runtime.Attributes; +using LevelImposter.Core; using System; using System.IO; -using System.Text; -using LevelImposter.Core; using System.Text.Json; -using Il2CppInterop.Runtime.Attributes; namespace LevelImposter.Shop { @@ -14,6 +13,7 @@ public static class GitHubAPI { public const string API_PATH = "https://api.github.com/repos/DigiWorm0/LevelImposter/releases?per_page=5"; public const string UPDATE_FORBIDDEN_FLAG = "[NoAutoUpdate]"; + public const string DEV_VERSION_FLAG = "dev"; /// /// Gets the current path where the LevelImposter DLL is stored. @@ -86,7 +86,7 @@ private static bool IsUpdateForbidden(GHRelease[] releases) public static bool IsCurrent(GHRelease release) { string versionString = release.name.Split(" ")[1]; - return versionString == LevelImposter.Version; + return versionString == LevelImposter.Version || LevelImposter.Version.Contains(DEV_VERSION_FLAG); } /// diff --git a/LevelImposter/Shop/Util/LevelImposterAPI.cs b/LevelImposter/Shop/Util/LevelImposterAPI.cs index ead847f..ccd01e8 100644 --- a/LevelImposter/Shop/Util/LevelImposterAPI.cs +++ b/LevelImposter/Shop/Util/LevelImposterAPI.cs @@ -1,8 +1,10 @@ -using System; -using UnityEngine; +using Il2CppInterop.Runtime.Attributes; +using Il2CppInterop.Runtime.InteropTypes.Arrays; using LevelImposter.Core; +using System; +using System.IO; using System.Text.Json; -using Il2CppInterop.Runtime.Attributes; +using UnityEngine; namespace LevelImposter.Shop { @@ -107,32 +109,42 @@ public static void DownloadMap(Guid id, Action? onProgress, Action LILogger.Info($"Downloading map [{id}]..."); GetMap(id, (LIMetadata metadata) => { - HTTPHandler.Instance?.Download(metadata.downloadURL, onProgress, (string mapJson) => + HTTPHandler.Instance?.Download(metadata.downloadURL, onProgress, (Il2CppStructArray fileData) => { LILogger.Info($"Parsing map {metadata}..."); try { - LIMap? mapData = JsonSerializer.Deserialize(mapJson); - mapJson = ""; // Free Memory - if (mapData == null) + using (var memoryStream = new MemoryStream(fileData)) { - onError("Map was null"); - return; - } - mapData.v = metadata.v; - mapData.id = metadata.id; - mapData.name = metadata.name; - mapData.description = metadata.description; - mapData.authorID = metadata.authorID; - mapData.authorName = metadata.authorName; - mapData.isPublic = metadata.isPublic; - mapData.isVerified = metadata.isVerified; - mapData.createdAt = metadata.createdAt; - mapData.thumbnailURL = metadata.thumbnailURL; - mapData.remixOf = metadata.remixOf; + // Deserialize the map + var mapData = LIDeserializer.DeserializeMap(memoryStream); + + // Free Memory + fileData = null; + + // Check Download + if (mapData == null) + { + onError("Map was null"); + return; + } - callback(mapData); - mapData = null; + // Set Metadata + mapData.id = metadata.id; + mapData.name = metadata.name; + mapData.description = metadata.description; + mapData.authorID = metadata.authorID; + mapData.authorName = metadata.authorName; + mapData.isPublic = metadata.isPublic; + mapData.isVerified = metadata.isVerified; + mapData.createdAt = metadata.createdAt; + mapData.thumbnailURL = metadata.thumbnailURL; + mapData.remixOf = metadata.remixOf; + + // Callback + callback(mapData); + mapData = null; + } } catch (Exception e) { @@ -154,7 +166,8 @@ public static void DownloadThumbnail(LIMetadata metadata, Action callbac HTTPHandler.Instance?.Request(metadata.thumbnailURL, (imgData) => { ThumbnailCache.Save(metadata.id, imgData); - SpriteLoader.Instance?.LoadSpriteAsync(imgData, false, (spriteData) => + var imgStream = new MemoryStream(imgData); + SpriteLoader.Instance?.LoadSpriteAsync(imgStream, (spriteData) => { if (spriteData == null) { diff --git a/LevelImposter/Shop/Util/MapFileAPI.cs b/LevelImposter/Shop/Util/MapFileAPI.cs index e6d06e5..be02282 100644 --- a/LevelImposter/Shop/Util/MapFileAPI.cs +++ b/LevelImposter/Shop/Util/MapFileAPI.cs @@ -1,9 +1,8 @@ -using System; +using Il2CppInterop.Runtime.Attributes; +using LevelImposter.Core; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; -using LevelImposter.Core; -using Il2CppInterop.Runtime.Attributes; namespace LevelImposter.Shop { @@ -29,13 +28,13 @@ public static string GetDirectory() } /// - /// Gets the path where a specific map LIM file is stored. + /// Gets the path where a specific map LIM2 file is stored. /// /// ID of the map file /// The path where a specific map is stored public static string GetPath(string mapID) { - return Path.Combine(GetDirectory(), mapID + ".lim"); + return Path.Combine(GetDirectory(), mapID + ".lim2"); } /// @@ -45,7 +44,7 @@ public static string GetPath(string mapID) [HideFromIl2Cpp] public static string[] ListIDs() { - string[] fileNames = Directory.GetFiles(GetDirectory(), "*.lim"); + string[] fileNames = Directory.GetFiles(GetDirectory(), "*.lim2"); for (int i = 0; i < fileNames.Length; i++) fileNames[i] = Path.GetFileNameWithoutExtension(fileNames[i]); return fileNames; @@ -70,17 +69,17 @@ public static bool Exists(string? mapID) /// Callback on success /// Representation of the map file data in the form of a LIMap. [HideFromIl2Cpp] - public static void Get(string mapID, Action callback) + public static LIMap? Get(string mapID, bool spriteDB = true) { string path = GetPath(mapID); - FileHandler.Instance?.Get(path, (LIMap? map) => + using (var stream = File.OpenRead(path)) { - if (map != null) - { - map.id = mapID; - callback(map); - } - }, null); + LIDeserializer.CurrentFilePath = path; + var mapData = LIDeserializer.DeserializeMap(stream, spriteDB); + if (mapData != null) + mapData.id = mapID; + return mapData; + } } /// @@ -91,18 +90,9 @@ public static void Get(string mapID, Action callback) /// Callback on success /// Representation of the map file data in the form of a LIMetadata. [HideFromIl2Cpp] - public static void GetMetadata(string mapID, Action callback) + public static LIMetadata? GetMetadata(string mapID) { - // TODO: Prevent parsing of unnecessary data - string path = GetPath(mapID); - FileHandler.Instance?.Get(path, (LIMetadata? map) => - { - if (map != null) - { - map.id = mapID; - callback(map); - } - }, null); + return Get(mapID, false); } /// @@ -114,10 +104,13 @@ public static void Save(LIMap map) { LILogger.Info($"Saving {map} to filesystem"); string mapPath = GetPath(map.id); - string mapJson = JsonSerializer.Serialize(map, SERIALIZE_OPTIONS); + // Create Directory if (!Directory.Exists(GetDirectory())) Directory.CreateDirectory(GetDirectory()); - File.WriteAllText(mapPath, mapJson); + // Write to File + using (var dataStream = LISerializer.SerializeMap(map)) + using (var outputFileStream = File.OpenWrite(mapPath)) + dataStream.CopyTo(outputFileStream); } /// diff --git a/LevelImposter/Shop/Util/MapLoader.cs b/LevelImposter/Shop/Util/MapLoader.cs index ef85314..91c197c 100644 --- a/LevelImposter/Shop/Util/MapLoader.cs +++ b/LevelImposter/Shop/Util/MapLoader.cs @@ -1,5 +1,5 @@ -using System; using LevelImposter.Core; +using System; namespace LevelImposter.Shop { @@ -35,12 +35,9 @@ public static void LoadMap(LIMap? map, bool isFallback) /// Callback on success public static void LoadMap(string mapID, bool isFallback, Action? callback) { - MapFileAPI.Get(mapID, (mapData) => - { - LoadMap(mapData, isFallback); - if (callback != null) - callback(); - }); + var mapData = MapFileAPI.Get(mapID); + LoadMap(mapData, isFallback); + callback?.Invoke(); // TODO: Make synchronous } /// diff --git a/LevelImposter/Shop/Util/ThumbnailCache.cs b/LevelImposter/Shop/Util/ThumbnailCache.cs index a3feb80..7c568b6 100644 --- a/LevelImposter/Shop/Util/ThumbnailCache.cs +++ b/LevelImposter/Shop/Util/ThumbnailCache.cs @@ -1,8 +1,8 @@ +using Il2CppInterop.Runtime.Attributes; +using LevelImposter.Core; using System; using System.IO; using UnityEngine; -using LevelImposter.Core; -using Il2CppInterop.Runtime.Attributes; namespace LevelImposter.Shop { @@ -42,12 +42,12 @@ public static void Get(string mapID, Action callback) // Read thumbnail from filesystem LILogger.Info($"Loading thumbnail [{mapID}] from filesystem"); bool isInSpriteCache = SpriteLoader.Instance?.IsSpriteInCache(mapID) ?? false; - byte[] thumbnailBytes = new byte[0]; + Stream? thumbnailStream = null; if (!isInSpriteCache) - thumbnailBytes = FileCache.Get($"{mapID}.png") ?? thumbnailBytes; // Not in memory, try to read from file cache + thumbnailStream = File.OpenRead(FileCache.GetPath($"{mapID}.png")); // Load thumbnail into sprite - SpriteLoader.Instance?.LoadSpriteAsync(thumbnailBytes, false, (spriteData) => + SpriteLoader.Instance?.LoadSpriteAsync(thumbnailStream, (spriteData) => { Sprite? sprite = spriteData?.Sprite; if (sprite == null) diff --git a/README.md b/README.md index f67ccd7..46b0def 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,11 @@ Custom Among Us Mapping Studio | Mod | Support | Notes | |:-:|:-:|:-:| | [BetterCrewLink](https://github.com/OhMyGuus/BetterCrewLink) | ✅ | No wall detection | -| [Town of Us-R](https://github.com/eDonnes124/Town-Of-Us-R) | ✅ | | | [The Other Roles](https://github.com/TheOtherRolesAU/TheOtherRoles) | ✅ | | +| [Town of Us-R](https://github.com/eDonnes124/Town-Of-Us-R) | ✅ | | +| [Town of Us Reworked](https://github.com/AlchlcDvl/TownOfUsReworked) | ⚠️ | Mod is in early beta | | [Stellar Roles](https://github.com/Mr-Fluuff/StellarRolesAU) | ✅ | | -| [Submerged](https://github.com/SubmergedAmongUs/Submerged) | ⛔ | Isn\'t up-to-date w/ the latest AU | +| [Submerged](https://github.com/SubmergedAmongUs/Submerged) | ⚠️ | Some minor visual bugs | Don\'t see a compatible mod here? Create a [Pull Request](https://github.com/DigiWorm0/LevelImposter/pulls)!