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