863 lines
38 KiB
C#
863 lines
38 KiB
C#
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
|
using MelonLoader;
|
|
using MelonLoader.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.SceneManagement;
|
|
using greg.Diagnostic;
|
|
using greg.Core;
|
|
|
|
|
|
// Namespace gregAssetExporter muss zu deiner AssemblyInfo passen
|
|
[assembly: MelonInfo(typeof(gregAssetExporter.gregMain), "gregCore Framework", greg.Core.gregReleaseVersion.Current, "MLeeM97, Joniii11 (teamGreg)")]
|
|
[assembly: MelonGame("Waseku", "Data Center")]
|
|
|
|
namespace gregAssetExporter
|
|
{
|
|
public class gregMain : MelonMod
|
|
{
|
|
private string exportPath = string.Empty;
|
|
private bool exportBetaNotUsed = true;
|
|
private bool showDebugOverlay = true;
|
|
|
|
private readonly greg.Exporter.Il2CppEventCatalogService eventCatalogService = new greg.Exporter.Il2CppEventCatalogService();
|
|
private readonly greg.Exporter.Il2CppGameplayIndexService gameplayIndexService = new greg.Exporter.Il2CppGameplayIndexService();
|
|
private readonly greg.Exporter.RuntimeHookService runtimeHookService = new greg.Exporter.RuntimeHookService();
|
|
private readonly greg.Exporter.GameSignalSnapshotService gameSignalSnapshotService = new greg.Exporter.GameSignalSnapshotService();
|
|
|
|
private Texture2D debugOverlayBackgroundTexture;
|
|
private int debugHooksAvailable;
|
|
private int debugHookEventsAvailable;
|
|
private int debugNotYetImplemented;
|
|
private bool debugOverlayStatsInitialized;
|
|
|
|
public override void OnInitializeMelon()
|
|
{
|
|
// --- gregCore Diagnostic & Session Logging ---
|
|
greg.Core.Diagnostic.GregSessionLogger.Initialize();
|
|
greg.Core.Diagnostic.GregSessionLogger.Log("Initializing gregCore Framework...");
|
|
|
|
// --- gregCore Framework Internal Initialization ---
|
|
greg.Sdk.Services.GregSaveService.Init();
|
|
greg.Sdk.Services.GregUiService.SetGlobalScale(0.85f); // Use user-preferred 0.85x by default
|
|
greg.Sdk.Services.GregHudService.Initialize();
|
|
greg.Sdk.Services.MCP.GregMCPServer.Start();
|
|
|
|
// Apply Deep-Layer Hijacker Patches
|
|
var harmony = new HarmonyLib.Harmony("greg.core.hijacker");
|
|
harmony.PatchAll(typeof(greg.Sdk.Internal.GregUiHijacker).Assembly);
|
|
|
|
// --- Legacy Exporter Initialization ---
|
|
exportPath = Path.Combine(MelonEnvironment.ModsDirectory, "ExportedAssets");
|
|
if (!Directory.Exists(exportPath)) Directory.CreateDirectory(exportPath);
|
|
|
|
MelonLogger.Msg($"gregCore Framework v{greg.Core.gregReleaseVersion.Current} loaded (SDK-only build).");
|
|
MelonLogger.Msg("Want to help building the future of Modding in DataCenter? Join our Discord: discord.gg/greg");
|
|
MelonLogger.Msg($"gregCore provides {greg.Sdk.Services.GregModRegistry.GetLoadedMods().Count} registered mods.");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModInitializedEvent(DateTime.UtcNow, greg.Core.gregReleaseVersion.Current));
|
|
}
|
|
|
|
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
|
|
{
|
|
MelonLogger.Msg($"[gregCore] Scene Loaded: {sceneName}. Triggering data export...");
|
|
greg.Core.Exporter.DataExporter.RunFullExport();
|
|
|
|
if (sceneName == "MainMenu")
|
|
{
|
|
greg.Core.UI.UIRouter.SetMode(greg.Core.UI.UIMode.MainMenu);
|
|
}
|
|
}
|
|
|
|
public override void OnApplicationQuit()
|
|
{
|
|
greg.Sdk.Services.MCP.GregMCPServer.Stop();
|
|
}
|
|
|
|
public override void OnUpdate()
|
|
{
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModTickEvent(Time.deltaTime, Time.frameCount));
|
|
|
|
// Central Input Management
|
|
greg.Sdk.Services.GregInputManagerService.Update();
|
|
|
|
if (Keyboard.current != null && Keyboard.current.f1Key.wasPressedThisFrame)
|
|
{
|
|
greg.Core.UI.gregModConfigManager.Toggle(!greg.Core.UI.gregModConfigManager.IsOpen);
|
|
}
|
|
|
|
#if DEBUG
|
|
if (Keyboard.current != null && Keyboard.current.f5Key.wasPressedThisFrame)
|
|
{
|
|
showDebugOverlay = !showDebugOverlay;
|
|
}
|
|
|
|
if (!showDebugOverlay) return;
|
|
|
|
if (Keyboard.current != null && Keyboard.current.ctrlKey.isPressed && Keyboard.current.f8Key.wasPressedThisFrame)
|
|
{
|
|
ExportAllResources();
|
|
}
|
|
|
|
if (Keyboard.current != null && Keyboard.current.f6Key.wasPressedThisFrame)
|
|
{
|
|
RefreshDebugOverlayStats(forceHookScan: true);
|
|
}
|
|
|
|
if (Keyboard.current != null && Keyboard.current.f11Key.wasPressedThisFrame)
|
|
{
|
|
ExportIl2CppEventCatalog();
|
|
}
|
|
|
|
if (Keyboard.current != null && Keyboard.current.f12Key.wasPressedThisFrame)
|
|
{
|
|
InstallRuntimeHooks();
|
|
}
|
|
|
|
if (showDebugOverlay)
|
|
{
|
|
if (!debugOverlayStatsInitialized)
|
|
RefreshDebugOverlayStats(forceHookScan: true);
|
|
|
|
var entries = new List<greg.Sdk.Services.GregMetadataEntry>
|
|
{
|
|
new greg.Sdk.Services.GregMetadataEntry("HOOKS", debugHooksAvailable.ToString("D5"), new Color(0.38f, 0.96f, 0.85f)),
|
|
new greg.Sdk.Services.GregMetadataEntry("EVENTS", debugHookEventsAvailable.ToString("D5"), new Color(0.38f, 0.96f, 0.85f)),
|
|
new greg.Sdk.Services.GregMetadataEntry("MISSING", debugNotYetImplemented.ToString("D5"), Color.red),
|
|
new greg.Sdk.Services.GregMetadataEntry("SCENES", UnityEngine.SceneManagement.SceneManager.sceneCount.ToString(), Color.white),
|
|
new greg.Sdk.Services.GregMetadataEntry("FPS", (1f / Time.unscaledDeltaTime).ToString("F0"), Color.yellow)
|
|
};
|
|
|
|
greg.Sdk.Services.GregHudService.UpdateJadeBox(
|
|
"GREG_CORE",
|
|
$"v{greg.Core.gregReleaseVersion.Current} | DBG_MODE",
|
|
entries
|
|
);
|
|
}
|
|
else
|
|
{
|
|
greg.Sdk.Services.GregHudService.HideJadeBox();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if DEBUG
|
|
// OnGUI removed in favor of GregHudService (Premium UI)
|
|
#endif
|
|
|
|
private void EnsureDebugOverlayAssets()
|
|
{
|
|
if (debugOverlayBackgroundTexture == null)
|
|
{
|
|
debugOverlayBackgroundTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
|
debugOverlayBackgroundTexture.SetPixel(0, 0, new Color(0.02f, 0.04f, 0.08f, 0.85f));
|
|
debugOverlayBackgroundTexture.Apply();
|
|
}
|
|
}
|
|
|
|
private void RefreshDebugOverlayStats(bool forceHookScan)
|
|
{
|
|
try
|
|
{
|
|
if (forceHookScan || !debugOverlayStatsInitialized)
|
|
{
|
|
var hookScanResult = runtimeHookService.ScanCandidates(100000);
|
|
debugHooksAvailable = hookScanResult.Candidates.Count;
|
|
}
|
|
|
|
int eventCount = 0;
|
|
var eventFields = typeof(EventIds).GetFields(BindingFlags.Public | BindingFlags.Static);
|
|
for (int index = 0; index < eventFields.Length; index++)
|
|
{
|
|
FieldInfo field = eventFields[index];
|
|
if (field.IsLiteral && field.FieldType == typeof(uint))
|
|
eventCount++;
|
|
}
|
|
|
|
debugHookEventsAvailable = eventCount;
|
|
debugNotYetImplemented = Math.Max(0, debugHooksAvailable - debugHookEventsAvailable);
|
|
debugOverlayStatsInitialized = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"Debug overlay stats update failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
|
|
private void ExportAllGameSignalsOnStartup()
|
|
{
|
|
try
|
|
{
|
|
string diagnosticsPath = Path.Combine(exportPath, "Diagnostics");
|
|
string snapshotPath = gameSignalSnapshotService.ExportAll(diagnosticsPath, eventCatalogService, gameplayIndexService, runtimeHookService);
|
|
MelonLogger.Msg($"Startup-Snapshot erstellt: {snapshotPath}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Error($"Startup-Snapshot fehlgeschlagen: {ex.Message}");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "StartupSnapshot", ex.Message));
|
|
}
|
|
}
|
|
|
|
private void ExportIl2CppEventCatalog()
|
|
{
|
|
try
|
|
{
|
|
string diagnosticsPath = Path.Combine(exportPath, "Diagnostics");
|
|
string filePath = eventCatalogService.ExportCatalog(diagnosticsPath);
|
|
int linesCount = File.ReadAllLines(filePath).Length;
|
|
string gameplayIndex = gameplayIndexService.ExportGameplayIndex(diagnosticsPath);
|
|
|
|
MelonLogger.Msg($"IL2CPP Event-Katalog exportiert: {filePath}");
|
|
MelonLogger.Msg($"IL2CPP Gameplay-Index exportiert: {gameplayIndex}");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.Il2CppCatalogExportedEvent(DateTime.UtcNow, filePath, linesCount));
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.Il2CppGameplayIndexExportedEvent(DateTime.UtcNow, gameplayIndex));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Error($"Fehler beim Export des IL2CPP Event-Katalogs: {ex.Message}");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "Il2CppCatalog", ex.Message));
|
|
}
|
|
}
|
|
|
|
private void InstallRuntimeHooks()
|
|
{
|
|
InstallRuntimeHooks(250);
|
|
}
|
|
|
|
private void InstallRuntimeHooks(int maxHooks)
|
|
{
|
|
try
|
|
{
|
|
var result = runtimeHookService.ScanAndInstall(maxHooks);
|
|
MelonLogger.Msg($"Hook-Scan abgeschlossen. Kandidaten={result.Scanned}, installiert={result.Installed}, fehlgeschlagen={result.Failed}");
|
|
|
|
if (result.Errors.Count > 0)
|
|
{
|
|
string diagnosticsPath = Path.Combine(exportPath, "Diagnostics");
|
|
Directory.CreateDirectory(diagnosticsPath);
|
|
string errorFile = Path.Combine(diagnosticsPath, "hook-install-errors.txt");
|
|
File.WriteAllLines(errorFile, result.Errors);
|
|
MelonLogger.Warning($"Hook-Fehlerliste geschrieben: {errorFile}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Error($"Fehler beim Installieren der Runtime-Hooks: {ex.Message}");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "RuntimeHooks", ex.Message));
|
|
}
|
|
}
|
|
|
|
private void InstallRuntimeHooksFromCatalog(string catalogPath, int maxHooks)
|
|
{
|
|
try
|
|
{
|
|
var result = runtimeHookService.InstallFromCatalog(catalogPath, maxHooks);
|
|
MelonLogger.Msg($"Hook-Catalog verarbeitet. Datei={catalogPath} Kandidaten={result.Scanned}, installiert={result.Installed}, fehlgeschlagen={result.Failed}");
|
|
|
|
if (result.Errors.Count > 0)
|
|
{
|
|
string diagnosticsPath = Path.Combine(exportPath, "Diagnostics");
|
|
Directory.CreateDirectory(diagnosticsPath);
|
|
string errorFile = Path.Combine(diagnosticsPath, "hook-install-errors.txt");
|
|
File.WriteAllLines(errorFile, result.Errors);
|
|
MelonLogger.Warning($"Hook-Fehlerliste geschrieben: {errorFile}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Error($"Fehler beim Installieren der Catalog-Hooks: {ex.Message}");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "CatalogHooks", ex.Message));
|
|
}
|
|
}
|
|
|
|
private void RunAutoHookCommandIfRequested()
|
|
{
|
|
try
|
|
{
|
|
string[] args = Environment.GetCommandLineArgs();
|
|
bool autoScan = HasArg(args, "--greg-hooks-auto");
|
|
bool installAll = HasArg(args, "--greg-hooks-all");
|
|
string catalogPath = GetArgValue(args, "--greg-hooks-catalog=");
|
|
|
|
if (!autoScan && string.IsNullOrWhiteSpace(catalogPath))
|
|
return;
|
|
|
|
int defaultMax = installAll ? int.MaxValue : 250;
|
|
int maxHooks = GetIntArgValue(args, "--greg-hooks-max=", defaultMax);
|
|
|
|
if (!string.IsNullOrWhiteSpace(catalogPath))
|
|
{
|
|
MelonLogger.Msg($"AutoHook-Command erkannt (catalog). maxHooks={maxHooks}");
|
|
InstallRuntimeHooksFromCatalog(catalogPath, maxHooks);
|
|
return;
|
|
}
|
|
|
|
MelonLogger.Msg($"AutoHook-Command erkannt (scan). maxHooks={maxHooks}");
|
|
InstallRuntimeHooks(maxHooks);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"AutoHook-Command konnte nicht ausgeführt werden: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static bool HasArg(IEnumerable<string> args, string name)
|
|
{
|
|
foreach (string arg in args)
|
|
{
|
|
if (string.Equals(arg, name, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string GetArgValue(IEnumerable<string> args, string prefix)
|
|
{
|
|
foreach (string arg in args)
|
|
{
|
|
if (!arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
return arg.Substring(prefix.Length).Trim('"');
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private static int GetIntArgValue(IEnumerable<string> args, string prefix, int fallback)
|
|
{
|
|
string raw = GetArgValue(args, prefix);
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return fallback;
|
|
|
|
return int.TryParse(raw, out int parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
private static void OnHookTriggered(greg.Exporter.HookTriggeredEvent evt)
|
|
{
|
|
if (evt.TriggerCount <= 3 || evt.TriggerCount % 100 == 0)
|
|
{
|
|
MelonLogger.Msg($"Hook Trigger: {evt.MethodName} (count={evt.TriggerCount})");
|
|
}
|
|
}
|
|
|
|
private void ExportAllResources()
|
|
{
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ExportStartedEvent(DateTime.UtcNow, exportPath));
|
|
|
|
string currentGamePath = Path.Combine(exportPath, "CurrentGame");
|
|
string modelsPath = Path.Combine(currentGamePath, "Models");
|
|
string texturesPath = Path.Combine(currentGamePath, "Textures");
|
|
string spritesPath = Path.Combine(currentGamePath, "Sprites");
|
|
string materialsPath = Path.Combine(currentGamePath, "Materials");
|
|
string scriptsPath = Path.Combine(currentGamePath, "Scripts");
|
|
string settingsPath = Path.Combine(currentGamePath, "Settings");
|
|
string notUsedPath = Path.Combine(currentGamePath, "NotUsed");
|
|
string notUsedModelsPath = Path.Combine(notUsedPath, "Models");
|
|
string notUsedTexturesPath = Path.Combine(notUsedPath, "Textures");
|
|
|
|
Directory.CreateDirectory(currentGamePath);
|
|
Directory.CreateDirectory(modelsPath);
|
|
Directory.CreateDirectory(texturesPath);
|
|
Directory.CreateDirectory(spritesPath);
|
|
Directory.CreateDirectory(materialsPath);
|
|
Directory.CreateDirectory(scriptsPath);
|
|
Directory.CreateDirectory(settingsPath);
|
|
if (exportBetaNotUsed)
|
|
{
|
|
Directory.CreateDirectory(notUsedPath);
|
|
Directory.CreateDirectory(notUsedModelsPath);
|
|
Directory.CreateDirectory(notUsedTexturesPath);
|
|
}
|
|
|
|
File.WriteAllText(
|
|
Path.Combine(currentGamePath, "README_NOT_USED.txt"),
|
|
"Dieser Ordner enthält verwendete Assets aus dem aktuellen Spielstand (aktiv + inaktiv).\n" +
|
|
"Struktur: Models, Textures, Sprites, Materials, Scripts, Settings.\n" +
|
|
"Optional werden nicht verwendete, aber geladene Assets nach 'NotUsed/Models' und 'NotUsed/Textures' exportiert."
|
|
);
|
|
|
|
MelonLogger.Msg("Starte Export: verwendete Assets (aktiv + inaktiv) aus allen geladenen Szenen...");
|
|
|
|
HashSet<int> usedMeshIds = new HashSet<int>();
|
|
HashSet<int> usedTextureIds = new HashSet<int>();
|
|
HashSet<int> usedSpriteTextureIds = new HashSet<int>();
|
|
HashSet<string> exportedCurrentGame = new HashSet<string>();
|
|
HashSet<string> exportedScriptTypes = new HashSet<string>();
|
|
HashSet<string> exportedMaterials = new HashSet<string>();
|
|
List<string> settingLines = new List<string>();
|
|
List<string> materialInfoLines = new List<string>();
|
|
|
|
foreach (GameObject obj in EnumerateAllSceneObjects(includeInactive: true))
|
|
{
|
|
try
|
|
{
|
|
settingLines.Add($"{GetGameObjectPath(obj)} | activeSelf={obj.activeSelf} | activeInHierarchy={obj.activeInHierarchy} | layer={obj.layer} | tag={obj.tag} | scene={obj.scene.name}");
|
|
|
|
Component[] components = GetComponentsSafe(obj);
|
|
for (int componentIndex = 0; componentIndex < components.Length; componentIndex++)
|
|
{
|
|
Component component = components[componentIndex];
|
|
if (component == null) continue;
|
|
string typeName = component.GetType().FullName;
|
|
if (!string.IsNullOrWhiteSpace(typeName))
|
|
exportedScriptTypes.Add(typeName);
|
|
}
|
|
|
|
MeshFilter meshFilter = obj.GetComponent<MeshFilter>();
|
|
if (meshFilter != null && meshFilter.sharedMesh != null)
|
|
{
|
|
usedMeshIds.Add(meshFilter.sharedMesh.GetInstanceID());
|
|
if (TryRegister(exportedCurrentGame, $"mesh:{meshFilter.sharedMesh.name}"))
|
|
SaveMesh(meshFilter.sharedMesh, modelsPath);
|
|
}
|
|
|
|
SkinnedMeshRenderer skinnedMeshRenderer = obj.GetComponent<SkinnedMeshRenderer>();
|
|
if (skinnedMeshRenderer != null && skinnedMeshRenderer.sharedMesh != null)
|
|
{
|
|
usedMeshIds.Add(skinnedMeshRenderer.sharedMesh.GetInstanceID());
|
|
if (TryRegister(exportedCurrentGame, $"mesh:{skinnedMeshRenderer.sharedMesh.name}"))
|
|
SaveMesh(skinnedMeshRenderer.sharedMesh, modelsPath);
|
|
}
|
|
|
|
Renderer renderer = obj.GetComponent<Renderer>();
|
|
if (renderer != null)
|
|
{
|
|
Material[] materials = renderer.sharedMaterials;
|
|
for (int materialIndex = 0; materialIndex < materials.Length; materialIndex++)
|
|
{
|
|
Material material = materials[materialIndex];
|
|
if (material == null) continue;
|
|
|
|
if (TryRegister(exportedMaterials, $"mat:{material.name}"))
|
|
{
|
|
materialInfoLines.Add($"material={material.name} | shader={material.shader?.name ?? "null"} | object={GetGameObjectPath(obj)}");
|
|
}
|
|
|
|
string[] texturePropertyNames = material.GetTexturePropertyNames();
|
|
for (int texturePropertyIndex = 0; texturePropertyIndex < texturePropertyNames.Length; texturePropertyIndex++)
|
|
{
|
|
string propertyName = texturePropertyNames[texturePropertyIndex];
|
|
Texture texture = material.GetTexture(propertyName);
|
|
if (texture is Texture2D tex2D)
|
|
{
|
|
usedTextureIds.Add(tex2D.GetInstanceID());
|
|
materialInfoLines.Add($"material={material.name} | texProp={propertyName} | texture={tex2D.name}");
|
|
if (TryRegister(exportedCurrentGame, $"tex:{tex2D.name}"))
|
|
SaveTexture(tex2D, texturesPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component uiImage = obj.GetComponent("Image");
|
|
if (uiImage != null)
|
|
{
|
|
PropertyInfo spriteProperty = uiImage.GetType().GetProperty("sprite");
|
|
Sprite sprite = spriteProperty?.GetValue(uiImage) as Sprite;
|
|
if (sprite != null && sprite.texture != null)
|
|
{
|
|
usedTextureIds.Add(sprite.texture.GetInstanceID());
|
|
usedSpriteTextureIds.Add(sprite.texture.GetInstanceID());
|
|
if (TryRegister(exportedCurrentGame, $"tex:{sprite.texture.name}"))
|
|
SaveTexture(sprite.texture, spritesPath);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MelonLogger.Warning($"Export-Fehler bei Objekt '{obj.name}': {ex.Message}");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "ExportObject", ex.Message));
|
|
}
|
|
}
|
|
|
|
File.WriteAllLines(Path.Combine(scriptsPath, "components.txt"), exportedScriptTypes);
|
|
File.WriteAllLines(Path.Combine(settingsPath, "objects.txt"), settingLines);
|
|
File.WriteAllLines(Path.Combine(materialsPath, "materials.txt"), materialInfoLines);
|
|
|
|
int notUsedMeshCount = 0;
|
|
int notUsedTextureCount = 0;
|
|
|
|
if (exportBetaNotUsed)
|
|
{
|
|
MelonLogger.Msg("Starte Beta-Export: nicht verwendete, aber geladene Assets...");
|
|
|
|
HashSet<string> exportedBeta = new HashSet<string>();
|
|
|
|
Mesh[] loadedMeshes = Resources.FindObjectsOfTypeAll<Mesh>();
|
|
for (int meshIndex = 0; meshIndex < loadedMeshes.Length; meshIndex++)
|
|
{
|
|
Mesh mesh = loadedMeshes[meshIndex];
|
|
if (mesh == null) continue;
|
|
if (usedMeshIds.Contains(mesh.GetInstanceID())) continue;
|
|
if (!IsCandidateNotUsedMesh(mesh)) continue;
|
|
if (!TryRegister(exportedBeta, $"mesh:{mesh.name}")) continue;
|
|
SaveMesh(mesh, notUsedModelsPath);
|
|
notUsedMeshCount++;
|
|
}
|
|
|
|
Texture2D[] loadedTextures = Resources.FindObjectsOfTypeAll<Texture2D>();
|
|
for (int textureIndex = 0; textureIndex < loadedTextures.Length; textureIndex++)
|
|
{
|
|
Texture2D tex = loadedTextures[textureIndex];
|
|
if (tex == null) continue;
|
|
if (usedTextureIds.Contains(tex.GetInstanceID())) continue;
|
|
if (!IsCandidateNotUsedTexture(tex)) continue;
|
|
if (!TryRegister(exportedBeta, $"tex:{tex.name}")) continue;
|
|
SaveTexture(tex, notUsedTexturesPath);
|
|
notUsedTextureCount++;
|
|
}
|
|
|
|
MelonLogger.Msg($"Export abgeschlossen! Verbaute Assets: {currentGamePath} | Nicht verwendet: {notUsedPath}");
|
|
}
|
|
else
|
|
{
|
|
MelonLogger.Msg($"Export abgeschlossen! Verbaute Assets: {currentGamePath} | NotUsed-Export deaktiviert (F10 zum Umschalten).");
|
|
}
|
|
|
|
var summaryLines = new List<string>
|
|
{
|
|
$"timestamp={DateTime.Now:yyyy-MM-dd HH:mm:ss}",
|
|
$"scenesLoaded={SceneManager.sceneCount}",
|
|
$"objectsScanned={settingLines.Count}",
|
|
$"uniqueComponents={exportedScriptTypes.Count}",
|
|
$"usedMeshes={usedMeshIds.Count}",
|
|
$"usedTextures={usedTextureIds.Count}",
|
|
$"usedSpriteTextures={usedSpriteTextureIds.Count}",
|
|
$"usedMaterials={exportedMaterials.Count}",
|
|
$"notUsedEnabled={exportBetaNotUsed}",
|
|
$"notUsedMeshes={notUsedMeshCount}",
|
|
$"notUsedTextures={notUsedTextureCount}"
|
|
};
|
|
File.WriteAllLines(Path.Combine(settingsPath, "summary.txt"), summaryLines);
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ExportCompletedEvent(DateTime.UtcNow, currentGamePath, settingLines.Count));
|
|
}
|
|
|
|
private IEnumerable<GameObject> EnumerateAllSceneObjects(bool includeInactive)
|
|
{
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
{
|
|
Scene scene = SceneManager.GetSceneAt(i);
|
|
if (!scene.IsValid() || !scene.isLoaded) continue;
|
|
|
|
GameObject[] roots = scene.GetRootGameObjects();
|
|
for (int rootIndex = 0; rootIndex < roots.Length; rootIndex++)
|
|
{
|
|
GameObject root = roots[rootIndex];
|
|
if (root == null) continue;
|
|
|
|
Queue<Transform> queue = new Queue<Transform>();
|
|
queue.Enqueue(root.transform);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
Transform current = queue.Dequeue();
|
|
if (current == null || current.gameObject == null)
|
|
continue;
|
|
|
|
GameObject currentObject = current.gameObject;
|
|
if (includeInactive || currentObject.activeInHierarchy)
|
|
yield return currentObject;
|
|
|
|
int childCount;
|
|
try
|
|
{
|
|
childCount = current.childCount;
|
|
}
|
|
catch
|
|
{
|
|
childCount = 0;
|
|
}
|
|
|
|
for (int childIndex = 0; childIndex < childCount; childIndex++)
|
|
{
|
|
Transform child;
|
|
try
|
|
{
|
|
child = current.GetChild(childIndex);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (child != null)
|
|
queue.Enqueue(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LogUiPathUnderCursor()
|
|
{
|
|
if (Mouse.current == null)
|
|
{
|
|
MelonLogger.Warning("Keine Maus verfügbar.");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "UIPath", "Keine Maus verfügbar"));
|
|
return;
|
|
}
|
|
|
|
Type eventSystemType = Type.GetType("UnityEngine.EventSystems.EventSystem, UnityEngine.UI");
|
|
Type pointerEventDataType = Type.GetType("UnityEngine.EventSystems.PointerEventData, UnityEngine.UI");
|
|
Type raycastResultType = Type.GetType("UnityEngine.EventSystems.RaycastResult, UnityEngine.UI");
|
|
|
|
if (eventSystemType == null || pointerEventDataType == null || raycastResultType == null)
|
|
{
|
|
MelonLogger.Warning("UI EventSystem-Typen konnten nicht aufgelöst werden.");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "UIPath", "UI EventSystem-Typen konnten nicht aufgelöst werden"));
|
|
return;
|
|
}
|
|
|
|
object currentEventSystem = eventSystemType.GetProperty("current", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
|
|
if (currentEventSystem == null)
|
|
{
|
|
MelonLogger.Warning("Kein aktives EventSystem gefunden.");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "UIPath", "Kein aktives EventSystem gefunden"));
|
|
return;
|
|
}
|
|
|
|
object pointerEventData = Activator.CreateInstance(pointerEventDataType, currentEventSystem);
|
|
pointerEventDataType.GetProperty("position")?.SetValue(pointerEventData, Mouse.current.position.ReadValue());
|
|
|
|
Type il2CppListGeneric = Type.GetType("Il2CppSystem.Collections.Generic.List`1, Il2Cppmscorlib");
|
|
if (il2CppListGeneric == null)
|
|
{
|
|
MelonLogger.Warning("Il2Cpp-Liste für UI-Raycasts konnte nicht aufgelöst werden.");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "UIPath", "Il2Cpp-Liste für UI-Raycasts konnte nicht aufgelöst werden"));
|
|
return;
|
|
}
|
|
|
|
Type listType = il2CppListGeneric.MakeGenericType(raycastResultType);
|
|
object results = Activator.CreateInstance(listType);
|
|
eventSystemType.GetMethod("RaycastAll")?.Invoke(currentEventSystem, new[] { pointerEventData, results });
|
|
|
|
int resultCount = (int)(listType.GetProperty("Count")?.GetValue(results) ?? 0);
|
|
|
|
if (resultCount == 0)
|
|
{
|
|
MelonLogger.Msg("Kein UI-Element unter dem Cursor gefunden.");
|
|
return;
|
|
}
|
|
|
|
MethodInfo getItemMethod = listType.GetMethod("get_Item");
|
|
if (getItemMethod == null)
|
|
{
|
|
MelonLogger.Warning("Il2Cpp-Raycast-Liste konnte nicht gelesen werden.");
|
|
greg.Exporter.ModFramework.Events.Publish(new greg.Exporter.ModErrorEvent(DateTime.UtcNow, "UIPath", "Il2Cpp-Raycast-Liste konnte nicht gelesen werden"));
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < resultCount; i++)
|
|
{
|
|
object result = getItemMethod.Invoke(results, new object[] { i });
|
|
GameObject gameObject = raycastResultType.GetProperty("gameObject")?.GetValue(result) as GameObject;
|
|
if (gameObject == null) continue;
|
|
|
|
string path = gameObject.name;
|
|
Transform parent = gameObject.transform.parent;
|
|
|
|
while (parent != null)
|
|
{
|
|
path = parent.name + "/" + path;
|
|
parent = parent.parent;
|
|
}
|
|
|
|
MelonLogger.Msg("UI-Pfad gefunden: " + path);
|
|
}
|
|
}
|
|
|
|
private static bool TryRegister(HashSet<string> exportedNames, string rawName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(rawName)) return false;
|
|
if (rawName.ToLowerInvariant().Contains("unity")) return false;
|
|
return exportedNames.Add(rawName);
|
|
}
|
|
|
|
private static string GetGameObjectPath(GameObject gameObject)
|
|
{
|
|
string path = gameObject.name;
|
|
Transform parent = gameObject.transform.parent;
|
|
while (parent != null)
|
|
{
|
|
path = parent.name + "/" + path;
|
|
parent = parent.parent;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
private static bool IsCandidateNotUsedMesh(Mesh mesh)
|
|
{
|
|
if (mesh == null) return false;
|
|
if (mesh.vertexCount <= 0) return false;
|
|
if (string.IsNullOrWhiteSpace(mesh.name)) return false;
|
|
if (mesh.hideFlags == HideFlags.HideAndDontSave) return false;
|
|
return true;
|
|
}
|
|
|
|
private static bool IsCandidateNotUsedTexture(Texture2D tex)
|
|
{
|
|
if (tex == null) return false;
|
|
if (string.IsNullOrWhiteSpace(tex.name)) return false;
|
|
if (tex.width <= 4 && tex.height <= 4) return false;
|
|
if (tex.hideFlags == HideFlags.HideAndDontSave) return false;
|
|
return true;
|
|
}
|
|
|
|
private void SaveTexture(Texture2D tex, string targetDirectory)
|
|
{
|
|
if (tex == null || string.IsNullOrEmpty(tex.name) || tex.name.Contains("unity")) return;
|
|
if (!Directory.Exists(targetDirectory)) Directory.CreateDirectory(targetDirectory);
|
|
|
|
// RenderTexture Trick um Read/Write-Sperre zu umgehen
|
|
RenderTexture tmp = RenderTexture.GetTemporary(tex.width, tex.height, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
|
|
Graphics.Blit(tex, tmp);
|
|
RenderTexture previous = RenderTexture.active;
|
|
RenderTexture.active = tmp;
|
|
|
|
Texture2D readableTex = new Texture2D(tex.width, tex.height);
|
|
readableTex.ReadPixels(new Rect(0, 0, tmp.width, tmp.height), 0, 0);
|
|
readableTex.Apply();
|
|
|
|
RenderTexture.active = previous;
|
|
RenderTexture.ReleaseTemporary(tmp);
|
|
|
|
byte[] bytes = ImageConversion.EncodeToPNG(readableTex);
|
|
string safeName = string.Join("_", tex.name.Split(Path.GetInvalidFileNameChars()));
|
|
string filePath = EnsureUniquePath(targetDirectory, safeName, ".png");
|
|
File.WriteAllBytes(filePath, bytes);
|
|
|
|
// Objekt zerstören um Speicher zu sparen während des Exports
|
|
UnityEngine.Object.Destroy(readableTex);
|
|
}
|
|
|
|
private void SaveMesh(Mesh mesh, string targetDirectory)
|
|
{
|
|
if (mesh == null || string.IsNullOrEmpty(mesh.name) || mesh.name.Contains("unity")) return;
|
|
if (!Directory.Exists(targetDirectory)) Directory.CreateDirectory(targetDirectory);
|
|
|
|
string safeName = string.Join("_", mesh.name.Split(Path.GetInvalidFileNameChars()));
|
|
string filePath = EnsureUniquePath(targetDirectory, safeName, ".obj");
|
|
|
|
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
|
sb.Append("g ").Append(safeName).Append("\n");
|
|
|
|
// Nutze die expliziten Unity-Typen um Konflikte mit System.Numerics zu vermeiden
|
|
UnityEngine.Vector3[] vertices = mesh.vertices;
|
|
for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
|
|
{
|
|
UnityEngine.Vector3 vertex = vertices[vertexIndex];
|
|
sb.Append(string.Format("v {0} {1} {2}\n", vertex.x, vertex.y, vertex.z).Replace(",", "."));
|
|
}
|
|
|
|
sb.Append("\n");
|
|
|
|
UnityEngine.Vector3[] normals = mesh.normals;
|
|
for (int normalIndex = 0; normalIndex < normals.Length; normalIndex++)
|
|
{
|
|
UnityEngine.Vector3 normal = normals[normalIndex];
|
|
sb.Append(string.Format("vn {0} {1} {2}\n", normal.x, normal.y, normal.z).Replace(",", "."));
|
|
}
|
|
|
|
sb.Append("\n");
|
|
|
|
UnityEngine.Vector2[] uvs = mesh.uv;
|
|
for (int uvIndex = 0; uvIndex < uvs.Length; uvIndex++)
|
|
{
|
|
UnityEngine.Vector2 uv = uvs[uvIndex];
|
|
sb.Append(string.Format("vt {0} {1}\n", uv.x, uv.y).Replace(",", "."));
|
|
}
|
|
|
|
for (int i = 0; i < mesh.subMeshCount; i++)
|
|
{
|
|
int[] triangles = mesh.GetTriangles(i);
|
|
for (int j = 0; j < triangles.Length; j += 3)
|
|
{
|
|
// OBJ Format Indizes starten bei 1
|
|
sb.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n",
|
|
triangles[j] + 1, triangles[j + 1] + 1, triangles[j + 2] + 1));
|
|
}
|
|
}
|
|
|
|
File.WriteAllText(filePath, sb.ToString());
|
|
}
|
|
|
|
private static string EnsureUniquePath(string directory, string baseName, string extension)
|
|
{
|
|
string filePath = Path.Combine(directory, baseName + extension);
|
|
int i = 1;
|
|
while (File.Exists(filePath))
|
|
{
|
|
filePath = Path.Combine(directory, $"{baseName}_{i}{extension}");
|
|
i++;
|
|
}
|
|
|
|
return filePath;
|
|
}
|
|
|
|
private static Component[] GetComponentsSafe(GameObject gameObject)
|
|
{
|
|
if (gameObject == null)
|
|
return Array.Empty<Component>();
|
|
|
|
try
|
|
{
|
|
return gameObject.GetComponents<Component>() ?? Array.Empty<Component>();
|
|
}
|
|
catch (Exception ex) when (IsSpanInteropMethodMissing(ex))
|
|
{
|
|
MelonLogger.Warning($"Unity6/Il2Cpp Span fallback aktiv für Objekt '{gameObject.name}'.");
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
try
|
|
{
|
|
MethodInfo getComponentsByType = typeof(GameObject).GetMethod("GetComponents", new[] { typeof(Type) });
|
|
if (getComponentsByType == null)
|
|
return Array.Empty<Component>();
|
|
|
|
object raw = getComponentsByType.Invoke(gameObject, new object[] { typeof(Component) });
|
|
if (raw is not Array rawArray)
|
|
return Array.Empty<Component>();
|
|
|
|
Component[] managedComponents = new Component[rawArray.Length];
|
|
for (int index = 0; index < rawArray.Length; index++)
|
|
managedComponents[index] = rawArray.GetValue(index) as Component;
|
|
|
|
return managedComponents;
|
|
}
|
|
catch
|
|
{
|
|
return Array.Empty<Component>();
|
|
}
|
|
}
|
|
|
|
private static bool IsSpanInteropMethodMissing(Exception exception)
|
|
{
|
|
Exception current = exception;
|
|
while (current != null)
|
|
{
|
|
string message = current.Message ?? string.Empty;
|
|
if (message.Contains("GetPinnableReference", StringComparison.OrdinalIgnoreCase)
|
|
&& message.Contains("ReadOnlySpan", StringComparison.OrdinalIgnoreCase)
|
|
&& message.Contains("Method not found", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
current = current.InnerException;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|