diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a911bb4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,180 @@ +# Top-most EditorConfig file +root = true + +# All files +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# Markdown and JSON files +[*.{md,json}] +indent_size = 2 + +# XML / Project files +[*.{csproj,props,targets,xml}] +indent_size = 2 + +# C# files +[*.cs] + +#### Core Roslyn Analyzer Defaults #### +dotnet_analyzer_diagnostic.category-Style.severity = suggestion +dotnet_analyzer_diagnostic.category-Performance.severity = suggestion +dotnet_analyzer_diagnostic.category-Design.severity = suggestion +dotnet_analyzer_diagnostic.category-Reliability.severity = warning +dotnet_analyzer_diagnostic.category-Usage.severity = warning + +#### Dead / Unused Code (IDE Rules) #### +dotnet_diagnostic.IDE0051.severity = warning # Remove unused private members +dotnet_diagnostic.IDE0052.severity = warning # Remove unread private members +dotnet_diagnostic.IDE0059.severity = warning # Unnecessary value assignment +dotnet_diagnostic.IDE0060.severity = suggestion # Remove unused parameter +dotnet_diagnostic.IDE0005.severity = suggestion # Remove unnecessary using directives +dotnet_diagnostic.IDE0035.severity = warning # Remove unreachable code +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:warning + +# Compiler warnings for unused items +dotnet_diagnostic.CS0168.severity = warning # Variable declared but never used +dotnet_diagnostic.CS0219.severity = warning # Variable assigned but never used + + +#### Code Style & Formatting #### +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +csharp_style_namespace_declarations = file_scoped:suggestion + +# Newline preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_around_binary_operators = before_and_after + +#### Language Preferences #### +# Var preferences +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = when_on_single_line:suggestion +csharp_style_expression_bodied_indexers = when_on_single_line:suggestion +csharp_style_expression_bodied_accessors = when_on_single_line:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion + +# Pattern matching & modern language features +csharp_style_pattern_matching = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# 'this.' and qualification +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Modifiers & Nullability +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +#### Naming Conventions #### +# PascalCase for types (classes, structs, enums, delegates) +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion + +dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate +dotnet_naming_symbols.types.applicable_accessibilities = * + +# Interfaces must begin with 'I' +dotnet_naming_rule.interfaces_should_start_with_i.symbols = interfaces +dotnet_naming_rule.interfaces_should_start_with_i.style = begins_with_i +dotnet_naming_rule.interfaces_should_start_with_i.severity = suggestion + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = * +dotnet_naming_styles.begins_with_i.required_prefix = I +dotnet_naming_styles.begins_with_i.capitalization = pascal_case + +# Private and internal fields with underscore prefix (_camelCase) +dotnet_naming_rule.private_fields_should_have_underscore_prefix.symbols = private_fields +dotnet_naming_rule.private_fields_should_have_underscore_prefix.style = underscore_camel_case +dotnet_naming_rule.private_fields_should_have_underscore_prefix.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_styles.underscore_camel_case.required_prefix = _ +dotnet_naming_styles.underscore_camel_case.capitalization = camel_case + +# Public and protected fields PascalCase +dotnet_naming_rule.public_fields_should_be_pascal_case.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.public_fields_should_be_pascal_case.severity = suggestion + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, protected, protected_internal + +# Methods, properties, events PascalCase +dotnet_naming_rule.members_should_be_pascal_case.symbols = members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion + +dotnet_naming_symbols.members.applicable_kinds = method, property, event +dotnet_naming_symbols.members.applicable_accessibilities = * + +# Parameters and local variables camelCase +dotnet_naming_rule.parameters_and_locals_should_be_camel_case.symbols = parameters_and_locals +dotnet_naming_rule.parameters_and_locals_should_be_camel_case.style = camel_case +dotnet_naming_rule.parameters_and_locals_should_be_camel_case.severity = suggestion + +dotnet_naming_symbols.parameters_and_locals.applicable_kinds = parameter, local +dotnet_naming_symbols.parameters_and_locals.applicable_accessibilities = * + +dotnet_naming_styles.pascal_case.capitalization = pascal_case +dotnet_naming_styles.camel_case.capitalization = camel_case + +#### Common Code Analysis Quality Rules (CA) #### +dotnet_diagnostic.CA1822.severity = suggestion # Mark members as static +dotnet_diagnostic.CA1806.severity = warning # Do not ignore method results +dotnet_diagnostic.CA2000.severity = suggestion # Dispose objects before losing scope +dotnet_diagnostic.CA2208.severity = warning # Instantiate argument exceptions correctly +dotnet_diagnostic.CA1051.severity = suggestion # Do not declare visible instance fields + +#### Test Project Overrides #### +[**{Test,Tests}*/**.cs] +# Allow underscores in unit test method names (e.g. Method_Condition_ExpectedResult) +dotnet_diagnostic.CA1707.severity = none +dotnet_naming_rule.members_should_be_pascal_case.severity = none +# Relax static member and unused parameter suggestions for tests +dotnet_diagnostic.CA1822.severity = none +dotnet_diagnostic.IDE0060.severity = none diff --git a/CHANGELOG.md b/CHANGELOG.md index d63ee03..2078f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,67 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1-dev-alpha] - 2026-10-02 + +### Added + +* `SceneLoadWaiter.GetPlayer()` — extracted helper that resolves the player `GameObject` from `Deadshot.GameManager`. +* `SceneLoadWaiter.DisableAllCameras()` — extracted helper that disables all cameras except the player's `Head/MainCamera`. +* `SceneLoadWaiter.DisableMenuAndSceneRenderers()` — extracted helper that hides `MainMenu` canvases and `C1L2` scene renderers, excluding the player. +* `SceneLoadWaiter.GetCustomScene()` — extracted helper that scans loaded scenes by name and returns the matching `Scene`. +* `SceneLoadWaiter.FindCustomPlayerSpawn(Scene)` — updated to accept the already-resolved `Scene` instead of re-scanning for it. +* Added null guards and restored deep recursive traversal in `FindChildRecursive` to support nested hierarchy spawn points. +* Player `CharacterController` is now restored in a `finally` block, ensuring it is re-enabled even if player placement throws. +* `SceneManager.BaseGameplaySceneName` constant to avoid hardcoded scene name strings. +* `AsyncOperation` tracking for asynchronous scene unloads to prevent reload race conditions. + +### Changed + +* `SceneLoadWaiter.InitializePlayer()` refactored to delegate player, camera, and renderer setup to dedicated helper methods. +* Corrected the renderer-filtering scene name from `C1L1` to `C1L2`. +* `SceneManager.LoadScene()` cleans up existing waiters and passes any pending unload operation to the new waiter. +* `SceneLoadWaiter` destroys its `GameObject` upon placement completion or spawn search timeout. +* `EventManager.Update()` converted from a static method to an instance lifecycle method on `MonoBehaviour`. + +### Fixed + +* Resolved persistent `SceneLoadWaiter` memory leak and CPU overhead after scene load. +* Fixed broken child traversal in `FindChildRecursive` failing on nested spawn points. +* Fixed race condition where reloading an already loaded scene attempted placement before unloader completed. +* Fixed `EventManager.Update()` not being invoked by Unity's engine loop. +* Removed stray blank lines from `EventManager.LevelPlaytimeEvent()`. + +## [1.1.0-dev-alpha] - 2026-10-02 + +### Added +- `SceneLoadWaiter` MonoBehaviour for additive custom-scene loading while preserving the existing gameplay scene and player. +- `SceneManager.LoadScene(string)` replacing the old `Load()` method — unloads an already-loaded scene before re-loading it, and bootstraps `C1L2` if it is not yet loaded. +- `AssetBundleManager.GetPlayerObject()` helper to retrieve the player `GameObject` from `Deadshot.GameManager`. +- `AssetBundleManager.SpawnPlayerObject()` helper to instantiate the player object. +- `LevelCompleteScreen.SetSecret(string)` to write to `_menu.secretText`. +- `Plugin.cs` now registers `SceneLoadWaiter` as a component on startup. +- `UnityEngine.UIModule` and `UnityEngine.PhysicsModule` references added to `DeadshotModAPI.csproj`. +- `.editorconfig` with comprehensive C# and project-wide style rules, including test-project overrides. +- `EnforceCodeStyleInBuild` enabled in `DeadshotModAPI.csproj`. +- `UniverseLib` explicit project reference added to `DeadshotModAPI.csproj`. + +### Changed +- `SceneManager.LoadModsBundle()` is now `public` and delegates to `AssetBundleManager` instead of using a raw `AssetBundle` field. +- `AssetBundleManager` bundle root path changed from `BepInEx/mods` to `BepInEx/plugins/mods`. +- `AssetBundleManager.LoadAssetBundles()` now returns an empty `List<>` instead of `null` on individual bundle load failure. +- `Input.CheckKeys()` refactored to filter with `Where(IsPressed)` before iterating, removing the inner `if` branch. +- `Input.CheckKeys()` loop variables changed from `var` to explicit `Key[]` / `Key` types. +- `EventManager` level-time comparison changed from `==` to `< 0.001f` float epsilon check. +- `LevelCompleteScreen.SetTime()` simplified — redundant string interpolation removed. +- `Plugin.cs` `_harmony` static field removed; `Harmony` instance is now a local variable. +- `ModLoader.Start()` calls `SceneManager.LoadModsBundle()` on startup. +- `ModLoader` null-guard braces added for `types == null` and `type == null` checks. +- `SceneManager` using directives re-ordered and `Deadshot.Player` namespace added. + +### Fixed +- Fixed `catch(Exception` missing space — normalised to `catch (Exception` throughout `EventManager.cs`. +- Fixed stray blank line inside `EventManager.LevelPlaytimeEvent()`. + ## [1.0.0-dev-alpha] - 2026-09-28 ### Added diff --git a/DeadshotModAPI.csproj b/DeadshotModAPI.csproj index 84083d7..1a97300 100644 --- a/DeadshotModAPI.csproj +++ b/DeadshotModAPI.csproj @@ -5,6 +5,7 @@ disable disable $(DefaultItemExcludes);DeadshotModAPI.Tests/**;examples/** + true @@ -65,6 +66,18 @@ lib\Il2CppSystem.dll + + lib\UniverseLib.BIE.IL2CPP.Interop.dll + + + + lib\UnityEngine.UIModule.dll + + + + lib\UnityEngine.PhysicsModule.dll + + diff --git a/Events/EventManager.cs b/Events/EventManager.cs index e11bcb5..0bfc95f 100644 --- a/Events/EventManager.cs +++ b/Events/EventManager.cs @@ -8,38 +8,28 @@ namespace DeadshotModAPI; public class EventManager : MonoBehaviour { - public static void Update() + internal void Update() { - LevelEvent.Update(); + LevelEvent.LevelPlaytimeEvent(); } public static class LevelEvent { private static float _lastLevelTime; - public static event Action LevelTimeChanged; - public static event Action LevelCompleted; - internal static void Update() - { - LevelPlaytimeEvent(); - } - internal static void LevelPlaytimeEvent() { try { - if (DeadshotGameManager.INSTANCE == null) { - Logger.Error("Cannot restart level: DeadshotGameManager.INSTANCE is null."); return; } - float levelTime = DeadshotGameManager.INSTANCE.CompletionTime; - if (levelTime == _lastLevelTime) + if (levelTime - _lastLevelTime < 0.001f) { return; } @@ -48,7 +38,7 @@ internal static void LevelPlaytimeEvent() LevelTimeChanged?.Invoke(levelTime); } - catch(Exception ex) + catch (Exception ex) { Logger.Error($"Error in LevelPlaytimeEvent(): {ex}"); } @@ -64,15 +54,15 @@ public static float GetLevelPlaytime() { if (DeadshotGameManager.INSTANCE == null) { - Logger.Error($"DeadshotGameManager.INSTANCE is null."); + return 0f; } return DeadshotGameManager.INSTANCE.CompletionTime; } - catch(Exception ex) + catch (Exception ex) { Logger.Error($"Error in GetLevelPlaytime(): {ex}"); - return 0; + return 0f; } } @@ -82,7 +72,7 @@ internal static void LevelCompletedEvent(LevelCompleteScreen menu) { LevelCompleted?.Invoke(menu); } - catch(Exception ex) + catch (Exception ex) { Logger.Error($"Error in LevelCompleted?.Invoke(): {ex}"); } @@ -108,7 +98,7 @@ private static void OnEnable(LevelCompleteMenu __instance) LevelEvent.LevelCompletedEvent(screen); } - catch(Exception ex) + catch (Exception ex) { Logger.Error($"Error in LevelCompleteMenu.OnEnable postfix: {ex}"); } diff --git a/Events/LevelCompleteScreen.cs b/Events/LevelCompleteScreen.cs index 6919388..33a7264 100644 --- a/Events/LevelCompleteScreen.cs +++ b/Events/LevelCompleteScreen.cs @@ -10,7 +10,12 @@ internal LevelCompleteScreen( public void SetTime(string text) { - _menu.timeText.text = $"{text}"; + _menu.timeText.text = text; + } + + public void SetSecret(string text) + { + _menu.secretText.text = text; } // TODO: Add the rest diff --git a/Input/InputManager.cs b/Input/InputManager.cs index dc2cd98..9f20a3f 100644 --- a/Input/InputManager.cs +++ b/Input/InputManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; @@ -21,12 +22,18 @@ public static class Input public static void OnKeyPressed(Key key, Action method) { if (method == null) + { return; + } - if (_keyActions.TryGetValue(key, out var existing)) + if (_keyActions.TryGetValue(key, out Action existing)) + { _keyActions[key] = existing + method; + } else + { _keyActions[key] = method; + } } /// @@ -37,21 +44,27 @@ public static void OnKeyPressed(Key key, Action method) public static void RemoveKeyPressed(Key key, Action method) { if (method == null) + { return; + } - if (_keyActions.TryGetValue(key, out var existing)) + if (_keyActions.TryGetValue(key, out Action existing)) { - var updated = existing - method; + Action updated = existing - method; if (updated == null) + { _keyActions.Remove(key); + } else + { _keyActions[key] = updated; + } } } internal static void TriggerKey(Key key) { - if (_keyActions.TryGetValue(key, out var action) && action != null) + if (_keyActions.TryGetValue(key, out Action action) && action != null) { Delegate[] delegates; try @@ -64,7 +77,7 @@ internal static void TriggerKey(Key key) return; } - foreach (var handler in delegates) + foreach (Delegate handler in delegates) { if (handler is Action callback) { @@ -84,7 +97,9 @@ internal static void TriggerKey(Key key) public static string ToCamelCase(this string text) { if (string.IsNullOrWhiteSpace(text)) + { return text; + } return char.ToLowerInvariant(text[0]) + text.Substring(1); } @@ -92,15 +107,19 @@ public static string ToCamelCase(this string text) private static bool IsPressed(Key key) { if (Keyboard.current == null) + { return false; + } try { string keyName = $"{key.ToString().ToCamelCase()}Key"; - var property = typeof(Keyboard).GetProperty(keyName); + PropertyInfo property = typeof(Keyboard).GetProperty(keyName); if (property == null) + { return false; + } var keyControl = property.GetValue(Keyboard.current) as KeyControl; return keyControl?.wasPressedThisFrame ?? false; @@ -117,14 +136,15 @@ internal static void CheckKeys() try { if (Keyboard.current == null) + { return; + } // Snapshot keys to allow modifying bindings during callback execution - var keys = _keyActions.Keys.ToArray(); - foreach (var key in keys) + Key[] keys = _keyActions.Keys.ToArray(); + foreach (Key key in keys.Where(IsPressed)) { - if (IsPressed(key)) - TriggerKey(key); + TriggerKey(key); } } catch (Exception ex) diff --git a/Plugin.cs b/Plugin.cs index b7c82bb..16c65ee 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,19 +1,23 @@ using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; +using Il2CppInterop.Runtime.Injection; namespace DeadshotModAPI; -[BepInPlugin("com.subaka.deadshotmodapi", "Deadshot Mod API", "v1.0.0-dev-alpha")] +[BepInPlugin("com.subaka.deadshotmodapi", "Deadshot Mod API", "v1.1.1-dev-alpha")] public class Plugin : BasePlugin { - private static Harmony _harmony; - + public override void Load() { Logger.Info("Deadshot Mod API loaded."); - _harmony = new Harmony("DeadshotModAPI"); - _harmony.PatchAll(); + if (!ClassInjector.IsTypeRegisteredInIl2Cpp()) + { + ClassInjector.RegisterTypeInIl2Cpp(); + } + var harmony = new Harmony("DeadshotModAPI"); + harmony.PatchAll(); AddComponent(); AddComponent(); } diff --git a/Runtime/ModLoader.cs b/Runtime/ModLoader.cs index 16a8611..ae7bd25 100644 --- a/Runtime/ModLoader.cs +++ b/Runtime/ModLoader.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; +using System.Threading.Tasks; using BepInEx; using UnityEngine; @@ -12,9 +15,10 @@ namespace DeadshotModAPI; /// public class ModLoader : MonoBehaviour { - public static readonly List LoadedMods = new(); + internal static readonly List LoadedMods = new(); public void Awake() + { Logger.Info("Deadshot Mod API ModLoader Awake."); } @@ -25,6 +29,7 @@ public void Start() try { LoadMods(); + SceneManager.LoadModsBundle(); } catch (Exception ex) { @@ -44,12 +49,12 @@ public void Update() } } - private void LoadMods() + private static void LoadMods() { string rootPath; try { - rootPath = Paths.BepInExRootPath; + rootPath = Path.Combine(Paths.BepInExRootPath, "plugins"); } catch (Exception ex) { @@ -106,7 +111,7 @@ private void LoadMods() } } - private void LoadMod(string path) + private static void LoadMod(string path) { if (string.IsNullOrWhiteSpace(path)) { @@ -120,61 +125,24 @@ private void LoadMod(string path) return; } - Assembly assembly; - try - { - assembly = Assembly.LoadFrom(path); - } - catch (BadImageFormatException ex) - { - Logger.Error($"Corrupt or invalid mod assembly '{Path.GetFileName(path)}': {ex.Message}"); - return; - } - catch (FileNotFoundException ex) - { - Logger.Error($"Mod assembly file not found '{path}': {ex.Message}"); - return; - } - catch (Exception ex) + Assembly assembly = GetAssembly(path); + + if (assembly == null) { - Logger.Error($"Failed to load assembly '{Path.GetFileName(path)}': {ex}"); + Logger.Warning($"Failed to load assembly: {Path.GetFileName(path)}"); return; } - Type[] types; - try - { - types = assembly.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - Logger.Warning($"Type load warnings encountered while inspecting '{Path.GetFileName(path)}':"); - if (ex.LoaderExceptions != null) - { - foreach (var loaderEx in ex.LoaderExceptions) - { - if (loaderEx != null) - { - Logger.Warning($" Loader exception: {loaderEx.Message}"); - } - } - } - types = ex.Types; - } - catch (Exception ex) + Type[] types = GetTypes(assembly); + + if (types == null || types.Length == 0) { - Logger.Error($"Failed to retrieve types from '{Path.GetFileName(path)}': {ex}"); + Logger.Warning($"No types found in assembly: {Path.GetFileName(path)}"); return; } - if (types == null) - return; - - foreach (Type type in types) + foreach (Type type in types.Where(t => t != null)) { - if (type == null) - continue; - try { if (!typeof(IDeadshotMod).IsAssignableFrom(type) || @@ -184,8 +152,7 @@ private void LoadMod(string path) continue; } - IDeadshotMod mod = (IDeadshotMod)Activator.CreateInstance(type)!; - if (mod == null) + if (Activator.CreateInstance(type) is not IDeadshotMod mod) { Logger.Warning($"Failed to instantiate mod '{type.FullName}' from '{path}'."); continue; @@ -202,9 +169,7 @@ private void LoadMod(string path) } Logger.Info($"Loading mod: {modName}"); - try { Logger.Info($" Description: {mod.Description}"); } catch { } - try { Logger.Info($" Creator: {mod.Creator}"); } catch { } - try { Logger.Info($" Version: {mod.Version}"); } catch { } + LogModMetaData(mod); mod.Load(); LoadedMods.Add(mod); @@ -215,4 +180,66 @@ private void LoadMod(string path) } } } + + private static Assembly GetAssembly(string path) + { + try + { + return Assembly.LoadFrom(path); + } + catch (BadImageFormatException ex) + { + Logger.Error($"Corrupt or invalid mod assembly '{Path.GetFileName(path)}': {ex.Message}"); + return null; + } + catch (FileNotFoundException ex) + { + Logger.Error($"Mod assembly file not found '{path}': {ex.Message}"); + return null; + } + catch (Exception ex) + { + Logger.Error($"Failed to load assembly '{Path.GetFileName(path)}': {ex}"); + return null; + } + } + + private static Type[] GetTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + if (ex.LoaderExceptions != null) + { + foreach (Exception loaderEx in ex.LoaderExceptions.Where(e => e != null)) + { + Logger.Warning($" Loader exception: {loaderEx.Message}"); + } + } + return ex.Types; + } + catch (Exception ex) + { + Logger.Error($"Failed to retrieve types from '{assembly.FullName}': {ex}"); + return Array.Empty(); + } + } + + private static void LogModMetaData(IDeadshotMod mod) + { + try + { + Logger.Info($" Name: {mod.Name}\n" + + $" Description: {mod.Description}\n" + + $" Creator: {mod.Creator}\n" + + $" Version: {mod.Version}"); + } + catch (Exception ex) + { + Logger.Warning($"Failed to read mod metadata: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/SceneManager/AssetBundleManager.cs b/SceneManager/AssetBundleManager.cs new file mode 100644 index 0000000..d6432d9 --- /dev/null +++ b/SceneManager/AssetBundleManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using BepInEx; +using Deadshot.Player; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Microsoft.VisualBasic; +using UnityEngine; +using UniverseLib; + +namespace DeadshotModAPI +{ + public class AssetBundleManager + { + private readonly string rootPath = Path.Combine(Paths.BepInExRootPath, "plugins/mods"); + + public UniverseLib.AssetBundle LoadAssetBundle(string bundlesPath) + { + try + { + string path = Path.Combine(rootPath, bundlesPath); + + if (!File.Exists(path)) + { + Logger.Error($"AssetBundle file not found at: {path}"); + } + + var bundle = UniverseLib.AssetBundle.LoadFromFile(path); + + return bundle; + } + catch (Exception ex) + { + Logger.Error($"Failed to load asset bundle. {ex}"); + return null; + } + } + + public List LoadAssetBundles(List assetBundles) + { + try + { + List bundles = new(); + + foreach (string name in assetBundles) + { + string path = Path.Combine(rootPath, name); + + if (!File.Exists(path)) + { + Logger.Error($"AssetBundle file not found at: {path}"); + return new List(); + } + + var bundle = UniverseLib.AssetBundle.LoadFromFile(path); + + if (bundle == null) + { + Logger.Error($"Failed to load asset bundle: {path}"); + return new List(); + } + + bundles.Add(bundle); + } + + return bundles; + } + catch (Exception ex) + { + Logger.Error($"Failed to load asset bundles. {ex}"); + return new List(); + } + } + + public static GameObject GetPlayerObject() + { + try + { + var player = Deadshot.GameManager.INSTANCE.PlayerManager.gameObject; + + Logger.Log($"Player: {player.name}"); + Logger.Log($"Scene: {player.scene.name}"); + Logger.Log($"Root: {player.transform.root.name}"); + + return player; + } + catch (Exception ex) + { + Logger.Error( + $"[DeadshotModAPI] Failed to find player object: {ex}" + ); + + return null; + } + } + + public static void SpawnPlayerObject() + { + try + { + GameObject player = GetPlayerObject(); + + if (player != null) + { + try + { + UnityEngine.Object.Instantiate(player); + } + catch (Exception ex) + { + Logger.Error($"Failed to spawn player object. {ex}"); + } + } + + Logger.Error("Failed to get player object."); + } + catch (Exception ex) + { + Logger.Error($"Failed to spawn player object. {ex}"); + } + } + } +} \ No newline at end of file diff --git a/SceneManager/SceneLoadWaiter.cs b/SceneManager/SceneLoadWaiter.cs new file mode 100644 index 0000000..92e595f --- /dev/null +++ b/SceneManager/SceneLoadWaiter.cs @@ -0,0 +1,376 @@ +using System; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace DeadshotModAPI; + +internal class SceneLoadWaiter : MonoBehaviour +{ + private const int MaxSceneLoadAttempts = 300; + private const int MaxSpawnSearchAttempts = 300; + + private string _sceneName = string.Empty; + private AsyncOperation _unloadOperation; + private GameObject _player; + private bool _initialized; + private bool _sceneLoadRequested; + private bool _playerPlaced; + private int _sceneLoadAttempts; + private int _spawnSearchAttempts; + + internal void Initialize(string sceneName, AsyncOperation unloadOperation = null) + { + try + { + Logger.Log($"SceneLoadWaiter initialized with scene: '{sceneName}'"); + + if (string.IsNullOrWhiteSpace(sceneName)) + { + Logger.Error("SceneLoadWaiter received an empty scene name."); + Destroy(gameObject); + return; + } + + _sceneName = sceneName; + _unloadOperation = unloadOperation; + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.Initialize failed: {ex}"); + } + } + + internal void Update() + { + try + { + if (string.IsNullOrEmpty(_sceneName)) + { + return; + } + + if (_unloadOperation != null) + { + if (!_unloadOperation.isDone) + { + return; + } + + _unloadOperation = null; + } + + if (!_initialized) + { + InitializePlayer(); + } + + Scene customScene = GetCustomScene(); + + if (!customScene.IsValid() || !customScene.isLoaded) + { + if (!_sceneLoadRequested) + { + _sceneLoadRequested = true; + Logger.Log($"Loading custom scene: {_sceneName}"); + + try + { + UnityEngine.SceneManagement.SceneManager.LoadScene(_sceneName, LoadSceneMode.Additive); + } + catch (Exception ex) + { + _sceneLoadRequested = false; + Logger.Error($"Failed to load custom scene '{_sceneName}': {ex}"); + Destroy(gameObject); + return; + } + } + else + { + _sceneLoadAttempts++; + if (_sceneLoadAttempts > MaxSceneLoadAttempts) + { + Logger.Error($"Timed out waiting for scene '{_sceneName}' to load."); + Destroy(gameObject); + return; + } + } + + return; + } + + if (_player == null) + { + _initialized = false; + return; + } + + GameObject spawnPoint = FindCustomPlayerSpawn(customScene); + + if (spawnPoint == null) + { + _spawnSearchAttempts++; + if (_spawnSearchAttempts > MaxSpawnSearchAttempts) + { + Logger.Error($"Could not find CustomPlayerSpawn in scene '{_sceneName}'."); + Destroy(gameObject); + } + + return; + } + + CharacterController controller = _player.GetComponent(); + + if (controller == null) + { + Logger.Error("Failed to find player controller."); + Destroy(gameObject); + return; + } + + DisableAllCameras(); + DisableMenuAndSceneRenderers(); + + controller.enabled = false; + + try + { + _player.transform.SetPositionAndRotation( + spawnPoint.transform.position, + spawnPoint.transform.rotation + ); + } + finally + { + controller.enabled = true; + } + + _playerPlaced = true; + Logger.Log($"Moved player to CustomPlayerSpawn: {spawnPoint.transform.position}"); + Destroy(gameObject); + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.Update failed: {ex}"); + } + } + + internal void OnDestroy() + { + SceneManager.IsSceneLoading = false; + + if (!_playerPlaced) + { + RestoreMenuAndSceneRenderers(); + } + } + + private void InitializePlayer() + { + try + { + if (!GetPlayer()) + { + return; + } + + _initialized = true; + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.InitializePlayer failed: {ex}"); + } + } + + private bool GetPlayer() + { + Deadshot.GameManager gameManager = Deadshot.GameManager.INSTANCE; + + if (gameManager == null || gameManager.PlayerManager == null) + { + return false; + } + + _player = gameManager.PlayerManager.gameObject; + + if (_player == null) + { + return false; + } + + Logger.Log($"Found player: {_player.name}"); + return true; + } + + private bool DisableAllCameras() + { + Transform playerCameraTransform = _player.transform.Find("Head/MainCamera"); + + if (playerCameraTransform == null) + { + return false; + } + + Camera playerCamera = playerCameraTransform.GetComponent(); + + if (playerCamera == null) + { + return false; + } + + foreach (Camera camera in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (camera == null) + { + continue; + } + + camera.enabled = camera == playerCamera; + } + + return true; + } + + private void DisableMenuAndSceneRenderers() + { + foreach (Canvas canvas in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (canvas == null) + { + continue; + } + + if (canvas.gameObject.scene.name == "MainMenu" || canvas.gameObject.scene.name == "MenuScene") + { + canvas.enabled = false; + } + } + + foreach (Renderer renderer in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (renderer == null) + { + continue; + } + + if (renderer.gameObject.scene.name == _sceneName) + { + continue; + } + + if (_player != null && renderer.transform.IsChildOf(_player.transform)) + { + continue; + } + + renderer.enabled = false; + } + } + + private void RestoreMenuAndSceneRenderers() + { + foreach (Renderer renderer in FindObjectsByType( + FindObjectsInactive.Include, + FindObjectsSortMode.None)) + { + if (renderer == null) + { + continue; + } + + if (renderer.gameObject.scene.name == SceneManager.BaseGameplaySceneName || + renderer.gameObject.scene.name == "MenuScene" || + renderer.gameObject.scene.name == "MainMenu") + { + renderer.enabled = true; + } + } + } + + + + private Scene GetCustomScene() + { + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) + { + Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + + if (scene.name == _sceneName) + { + return scene; + } + } + + return default; + } + + private static GameObject FindCustomPlayerSpawn(Scene customScene) + { + try + { + if (!customScene.IsValid() || !customScene.isLoaded) + { + return null; + } + + foreach (GameObject root in customScene.GetRootGameObjects()) + { + if (root == null) + { + continue; + } + + GameObject spawn = FindChildRecursive(root.transform, "CustomPlayerSpawn"); + + if (spawn != null) + { + return spawn; + } + } + } + catch (Exception ex) + { + Logger.Error($"SceneLoadWaiter.FindCustomPlayerSpawn failed: {ex}"); + } + + return null; + } + + private static GameObject FindChildRecursive(Transform parent, string name) + { + if (parent == null) + { + return null; + } + + if (parent.name == name) + { + return parent.gameObject; + } + + for (int i = 0; i < parent.childCount; i++) + { + Transform child = parent.GetChild(i); + + if (child == null) + { + continue; + } + + GameObject result = FindChildRecursive(child, name); + + if (result != null) + { + return result; + } + } + + return null; + } +} \ No newline at end of file diff --git a/SceneManager/SceneManager.cs b/SceneManager/SceneManager.cs index f3473bb..70d3602 100644 --- a/SceneManager/SceneManager.cs +++ b/SceneManager/SceneManager.cs @@ -1,7 +1,8 @@ using System; -using System.IO; +using System.Collections.Generic; +using Il2CppInterop.Runtime.Injection; using UnityEngine; -using Deadshot; +using UnityEngine.SceneManagement; namespace DeadshotModAPI; @@ -10,101 +11,127 @@ namespace DeadshotModAPI; /// public static class SceneManager { - private static AssetBundle _modsBundle; + internal const string BaseGameplaySceneName = "C1L2"; + + internal static bool IsSceneLoading { get; set; } /// - /// Loads a scene by its name using Deadshot's loading system. + /// Loads a scene while keeping Deadshot's gameplay scene loaded + /// so the existing player and gameplay systems remain available. /// /// The name of the scene to load. - public static void Load(string sceneName) + public static void LoadScene(string sceneName) { + Logger.Log($"LoadScene called with: '{sceneName}'"); + if (string.IsNullOrWhiteSpace(sceneName)) { - Logger.Warning("Cannot load scene: sceneName is null, empty, or whitespace."); + Logger.Error("LoadScene received an empty scene name."); return; } - try + if (string.Equals(sceneName, BaseGameplaySceneName, StringComparison.OrdinalIgnoreCase)) { - LoadingScreen.LoadScene(sceneName); - } - catch (Exception ex) - { - Logger.Error($"Failed to load scene '{sceneName}': {ex}"); + Logger.Error($"Cannot load '{BaseGameplaySceneName}' as a custom scene; it is the base gameplay scene."); + return; } - } - /// - /// Loads a scene by its build index using Deadshot's loading system. - /// - /// The build index of the scene to load. - public static void Load(int sceneIndex) - { - if (sceneIndex < 0) + if (IsSceneLoading) { - Logger.Warning($"Cannot load scene: invalid sceneIndex '{sceneIndex}'. Index must be non-negative."); + Logger.Log($"Scene load already in progress; ignoring duplicate request for '{sceneName}'."); return; } try { - LoadingScreen.LoadScene(sceneIndex); + IsSceneLoading = true; + + AsyncOperation unloadOperation = null; + Scene existingScene = default; + + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) + { + Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + + if (scene.name == sceneName) + { + existingScene = scene; + break; + } + } + + if (existingScene.IsValid() && existingScene.isLoaded) + { + Logger.Log($"Scene '{sceneName}' is already loaded. Unloading before reloading."); + unloadOperation = UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync(existingScene); + } + + bool baseGameplaySceneLoaded = false; + + for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) + { + Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); + + if (scene.name == BaseGameplaySceneName && scene.isLoaded) + { + baseGameplaySceneLoaded = true; + break; + } + } + + if (!baseGameplaySceneLoaded) + { + Logger.Log($"Loading Deadshot gameplay scene: {BaseGameplaySceneName}"); + + UnityEngine.SceneManagement.SceneManager.LoadScene(BaseGameplaySceneName, LoadSceneMode.Additive); + } + + if (!ClassInjector.IsTypeRegisteredInIl2Cpp()) + { + ClassInjector.RegisterTypeInIl2Cpp(); + } + + var waiterObject = new GameObject("DeadshotModAPI_SceneLoadWaiter"); + UnityEngine.Object.DontDestroyOnLoad(waiterObject); + SceneLoadWaiter waiter = waiterObject.AddComponent(); + waiter.Initialize(sceneName, unloadOperation); } catch (Exception ex) { - Logger.Error($"Failed to load scene at index {sceneIndex}: {ex}"); + IsSceneLoading = false; + Logger.Error($"Failed to load scene '{sceneName}': {ex}"); } } /// - /// Loads the Deadshot Mod API UI asset bundle from BepInEx/mods/DeadshotModAPI. + /// Loads the Deadshot Mod API asset bundles from + /// BepInEx/mods/DeadshotModAPI. /// internal static void LoadModsBundle() { - if (_modsBundle != null) - { - Logger.Warning("Mods Menu bundle is already loaded."); - return; - } - - string rootPath; try { - rootPath = BepInEx.Paths.BepInExRootPath; - } - catch (Exception ex) - { - Logger.Error($"Failed to determine BepInEx root path: {ex}"); - return; - } + AssetBundleManager bundleManager = new(); - if (string.IsNullOrWhiteSpace(rootPath)) - { - Logger.Error("BepInEx root path is null or empty. Cannot discover mods."); - return; - } + List bundlesFiles = new() + { + "DeadshotModApi/deadshotmodapi", + "DeadshotModApi/deadshotapi_assets" + }; - string path = Path.Combine(rootPath, "mods", "DeadshotModAPI", "deadshotmodapi"); - if (!File.Exists(path)) - { - Logger.Error($"Mod bundle file does not exist at path: {path}"); - return; - } + List bundles = bundleManager.LoadAssetBundles(bundlesFiles); - try - { - _modsBundle = AssetBundle.LoadFromFile(path); - if (_modsBundle == null) + if (bundles == null || bundles.Count == 0) { - Logger.Error($"Failed to load Mods Menu bundle: {path}"); + Logger.Error("Failed to load asset bundles."); return; } - Logger.Info("Mods Menu bundle loaded successfully."); + Logger.Info($"Loaded {bundles.Count} asset bundles."); } catch (Exception ex) { - Logger.Error($"Failed to load Mods Menu bundle from '{path}': {ex}"); + Logger.Error($"Failed to load Mods Menu bundle: {ex}"); } } } \ No newline at end of file diff --git a/lib/UnityEngine.PhysicsModule.dll b/lib/UnityEngine.PhysicsModule.dll new file mode 100644 index 0000000..7d1e557 Binary files /dev/null and b/lib/UnityEngine.PhysicsModule.dll differ diff --git a/lib/UnityEngine.UIModule.dll b/lib/UnityEngine.UIModule.dll new file mode 100644 index 0000000..6939a9f Binary files /dev/null and b/lib/UnityEngine.UIModule.dll differ diff --git a/lib/UniverseLib.BIE.IL2CPP.Interop.dll b/lib/UniverseLib.BIE.IL2CPP.Interop.dll new file mode 100644 index 0000000..4c02ca0 Binary files /dev/null and b/lib/UniverseLib.BIE.IL2CPP.Interop.dll differ