chore: sync gregCore workspace updates
Sponsor Tier Sync / sync (push) Failing after 34s
gregCore CI / build (push) Has been cancelled

This commit is contained in:
Marvin
2026-04-20 03:02:39 +02:00
parent 3772ed13a3
commit 9e6f25c54a
119 changed files with 4762 additions and 12209 deletions
+6
View File
@@ -4,6 +4,12 @@ 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).
## [0.1.0] - 2026-04-18
### Added
- NuGet packaging baseline for `gregCore` with symbol package output (`.snupkg`).
- Local + GitHub Packages feed configuration and package source mapping.
- Downstream `build/gregCore.props` integration for reference-only package usage.
## [v1.0.0.7] - 2026-04-12
### Added
- **gregUI**: Complete UI manipulation layer for UGUI (`src/UI/`).
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +0,0 @@
{
"id": "template.SampleServer",
"name": "Sample Template Server",
"rackUnits": 2,
"powerUsageWatts": 450,
"maxIOPS": 5000,
"price": 1200.0,
"tags": ["Standard", "GeneralPurpose"]
}
@@ -1,9 +0,0 @@
{
"id": "author.PackName",
"name": "Custom Content Pack Template",
"version": "1.0.0",
"author": "YourName",
"description": "Template for creating custom content packs for Data Center.",
"dependencies": ["greg.Core"]
}
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<MELON_PATH Condition="'$(MELON_PATH)' == ''">
C:\Program Files (x86)\Steam\steamapps\common\Data Center\MelonLoader
</MELON_PATH>
</PropertyGroup>
<ItemGroup>
<PackageReference Update="gregCore">
<ExcludeAssets>runtime</ExcludeAssets>
<PrivateAssets>none</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
+2
View File
@@ -0,0 +1,2 @@
<Project>
</Project>
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Erzeugt leere Stub-DLLs für CI-Build ohne echtes MelonLoader
# Diese enthalten nur die nötigen Type-Definitionen
dotnet new classlib -n MelonLoaderStub -f net6.0
+125
View File
@@ -0,0 +1,125 @@
# 1) Globale Architektur & Singletons
Analysebasis: `gregReferences/Assembly-CSharp/Il2Cpp/*.cs` (IL2CPP-Wrapper). Relevante Komponente liegt in der Schicht **Unity Spiel / IL2CPP Assembly** (nicht GregCore/Core-SDK selbst).
- `Il2Cpp.MainGameManager`
- Singleton: `public static unsafe MainGameManager instance`
- Verantwortlich für Session-/Spielzustand, Kundenfluss, Autosave-Settings, zentrale Objekt-Referenzen.
- Wichtige Methoden: `public unsafe void SetAutoSaveInterval(float minutes)`, `public unsafe void SetAutoSaveEnabled(bool enabled)`, `public unsafe int GetFreeVlanId()`.
- `Il2Cpp.NetworkMap`
- Singleton: `public static unsafe NetworkMap instance`
- Zentrale Topologie-/Routing-Instanz für Geräte, Kabel, Verbindungen.
- `Il2Cpp.PlayerManager`
- Singleton: `public static unsafe PlayerManager instance`
- Input-/Movement-Gating und Interaktionszustand (Objekt in Hand, Interaktionsflags).
- `Il2Cpp.TimeController`
- Singleton-ähnliche globale Zeitinstanz mit statischem Callback `public static unsafe TimeController.OnEndOfTheDay onEndOfTheDayCallback`.
- `Il2Cpp.SaveData`
- Globaler Save-State: `public static unsafe SaveData instance` + `public static unsafe SaveData _current`.
- `Il2Cpp.SaveSystem`
- Statischer Save/Load-Orchestrator (kein klassischer Singleton, aber globaler Entry-Point über statische API).
# 2) Spieler & Entities (Data Structures)
Hauptobjekte mit C-ABI-relevanten Datentypen (für stabile Bridge-Übergabe bevorzugt: primitive numerische Typen + explizite Structs).
- `Il2Cpp.Player`
- Ökonomie-/Progress-Felder: `money` (`float`), `xp` (`float`), `reputation` (`float`), `previousCoins` (`float`).
- Choke-Methoden: `public unsafe bool UpdateCoin(float _coinChhangeAmount, bool withoutSound = false)`, `public unsafe bool UpdateXP(float amount)`, `public unsafe void UpdateReputation(float amount)`.
- `Il2Cpp.PlayerData` (`[Serializable]`)
- Felder: `List<int> activeObjectives`, `float coins`, `float xp`, `float reputation`, `Il2CppStructArray<float> position`.
- Für C-ABI gut geeignet: `float`, `int`; bei `position` sauber als 3er-Array (`x,y,z`) flatten.
- `Il2Cpp.SaveData` (`[Serializable]`)
- Persistenz-Aggregat mit `playerData`, `networkData`, `loadedScenes`, `technicianData`, `repairJobQueue`, `commandCenterLevel` u. a.
- Mappings enthalten viele primitive Felder (`int`, `bool`, `float`, `string`) plus Listen/Arrays.
- `Il2Cpp.Item` (`ScriptableObject`)
- Preis-/Wertfelder: `int price`, `float weight`, `float deprecation`, `bool isStackable`, `int unlockedFromXP`.
- `Il2Cpp.ShopItemSO` (`ScriptableObject`)
- Shop-relevante Primitive: `int xpToUnlock`, `int price`, `int itemID`, `float eol`, `bool isCustomColor`.
- ECS-nahe explizite Layout-Typen (`[StructLayout(LayoutKind.Explicit)]`)
- `Il2Cpp.CableIDComponent`: `int CableId` (Offset 0), `int SwitchId` (Offset 4).
- `Il2Cpp.PacketComponent`: enthält `float3`, `float4`, `int cableId`, `int customerId`, `float moveSpeed` etc.
- Diese Typen sind für Low-Level-Hooks/Interop besonders wertvoll, da Feldlayout explizit ist.
# 3) Native Event-Systeme
Primär Delegate-basierte C#-Events/Callbacks in der **Unity Spiel / IL2CPP Assembly**-Schicht:
- Save-/Load-Callbacks (`Il2Cpp.SaveSystem`)
- `public static unsafe SaveSystem.OnSavingData onSavingData`
- `public static unsafe SaveSystem.OnLoadingData onLoadingData`
- `public static unsafe SaveSystem.OnLoadingDataLater onLoadingDataLater`
- Delegates sind `MulticastDelegate`-Typen mit `Invoke()`.
- Szenen-/Load-Callback (`Il2Cpp.LoadingScreen`)
- `public static unsafe LoadingScreen.GameIsLoaded onGameIsLoadedCallback`
- `LoadingScreen.GameIsLoaded` ist `MulticastDelegate` mit `Invoke()`.
- Tageszyklus-Callback (`Il2Cpp.TimeController`)
- `public static unsafe TimeController.OnEndOfTheDay onEndOfTheDayCallback`
- Delegate `OnEndOfTheDay.Invoke()` als stabiler Lifecycle-Hook.
- Pause-Menü-Callbacks (`Il2Cpp.PauseMenu`)
- `public static unsafe PauseMenu.OnPauseMenuOpen onPauseMenuOpenCallback`
- `public static unsafe PauseMenu.OnPauseMenuClose onPauseMenuCloseCallback`
- Input-Rebind-Events (`Il2Cpp.InputManager`)
- `public static unsafe void add_rebindComplete(Il2CppSystem.Action value)` / `remove_rebindComplete(...)`
- `add_rebindCanceled(...)` / `remove_rebindCanceled(...)`
- `add_rebindStarted(Il2CppSystem.Action<InputAction, int> value)` / `remove_rebindStarted(...)`
# 4) Kritische Hook-Ziele (Harmony Patch Candidates)
Kernziel: Choke-Points patchen (Prefix/Postfix), nicht breit `Update()`-spammen.
- Save/Load (höchste Priorität)
- `Il2Cpp.SaveSystem.SaveGame(string savename = null, string stringNameOfSave = null)`
- `Il2Cpp.SaveSystem.LoadGame(string savename)`
- `Il2Cpp.SaveSystem.Load(string savename, bool isFromPauseMenu)`
- `Il2Cpp.SaveSystem.AutoSave()`
- `Il2Cpp.SaveSystem.SaveGameData()` / `LoadGameData()`
- Economy/Progress
- `Il2Cpp.Player.UpdateCoin(float _coinChhangeAmount, bool withoutSound = false)`
- `Il2Cpp.Player.UpdateXP(float amount)`
- `Il2Cpp.Player.UpdateReputation(float amount)`
- Shop-Zahlungspfad: `Il2Cpp.ComputerShop.ButtonCheckOut()`, `UpdateCartTotal()`, `BuyNewItem(...)`, `BuyAnotherItem(...)`.
- Netzwerk-/Topologie
- `Il2Cpp.NetworkMap.AddDevice(string name, CableLink.TypeOfLink type, int customerID = -1)`
- `Il2Cpp.NetworkMap.RemoveDevice(string name)`
- `Il2Cpp.NetworkMap.Connect(string from, string to)` / `Disconnect(string from, string to)`
- `Il2Cpp.NetworkMap.RegisterCableConnection(...)` / `RemoveCableConnection(int cableId, bool preserveLACP = false)`
- Validierung: `Il2Cpp.NetworkMap.IsIpAddressDuplicate(string ip, Server serverToExclude)`
- Geräte-/Serverzustand
- `Il2Cpp.Server.PowerButton(bool forceState = false)`
- `Il2Cpp.Server.SetIP(string _ip)`
- `Il2Cpp.Server.UpdateCustomer(int newCustomerID)`
- `Il2Cpp.Server.UpdateAppID(int _appID)`
- `Il2Cpp.Server.ItIsBroken()` / `RepairDevice()`
# 5) UI & Szenen-Management
UI-/Scene-Flows mit hoher Relevanz für kontrollierte Mod-Intervention:
- Main Menu (`Il2Cpp.MainMenu`)
- `Continue()`, `NewGame()`, `LoadGame()`, `Settings()`, `QuitGame()`.
- Pause Flow (`Il2Cpp.PauseMenu`)
- `Pause(int openMenu)`, `Resume()`, `Save(string saveName = null, string _stringNameOfSave = null)`, `Load(string savename)`.
- Gute Hook-Stellen für mod-seitige Gatekeeper/Overlay-Interlocks.
- Loading/Scenes (`Il2Cpp.LoadingScreen`)
- `LoadGameScenesVoid(...)`, `LoadLevel(int sceneIndex)`, `UnLoadLevel(int sceneIndex)`
- `AsynchronousLoad(int sceneIndex)`, `AsynchronousUnLoad(int sceneIndex)`, `IsSceneLoaded(string name)`
- `onGameIsLoadedCallback` als zuverlässiger End-of-Load Synchronisationspunkt.
- Shop-/Asset-UI
- `Il2Cpp.ComputerShop`: Kauf- und Warenkorb-UI-Endpunkte.
- `Il2Cpp.AssetManagement`: Asset-Filter/Repair-UI-Workflows (relevant für automatische Tasking-Mods).
# 6) Obfuscation & Auffälligkeiten
- Obfuscation-Marker vorhanden
- `Il2Cpp.ObjectPrivateAbstractSealedInVo0` trägt `[ObfuscatedName("$BurstDirectCallInitializer")]`.
- Deutet auf generierte/Burst-nahe Initialisierer hin (kein klassischer Gameplay-Entry-Point).
- IL2CPP-/Interop-Spezifika
- Weit verbreitete `unsafe` Wrapper, `NativeFieldInfoPtr_*`, `NativeMethodInfoPtr_*`, `il2cpp_runtime_invoke`.
- Für Harmony-Patching sind semantische High-Level-Methoden stabiler als intern generierte Helper.
- Explizite Layouts als technische Auffälligkeit
- `[StructLayout(LayoutKind.Explicit)]` in u. a. `CableIDComponent`, `PacketComponent`, `_PrivateImplementationDetails_`.
- Wichtig für deterministische Feld-Offsets bei nativer Bridge/ABI.
- Anti-Cheat-Indikatoren (Textscan)
- Kein direkter String-/Symboltreffer auf typische Marker wie `EasyAntiCheat`, `BattlEye`, `VAC`, `GameGuard`, `AntiCheat` im gescannten Dump.
- Das ist **kein** kryptografischer Nachweis „anti-cheat-frei“, aber im vorliegenden C#-Dump gibt es keine offensichtlichen API-Hooks darauf.
+83 -5
View File
@@ -1,16 +1,94 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- ── Build ──────────────────────────────────────────────────── -->
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>gregCore</AssemblyName>
<Version>1.0.0.30-pre</Version>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>gregCore</AssemblyName>
<NoWarn>CS1701;CS1702</NoWarn>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- ── NuGet Identity ────────────────────────────────────────── -->
<PackageId>gregCore</PackageId>
<Version>0.1.0</Version>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<Authors>mleem97</Authors>
<Company>mleem97</Company>
<Product>gregCore</Product>
<Description>
Framework SDK for Data Center (Unity 6 IL2CPP) MelonLoader mods.
Provides GregHookBus, Services, Registries and Harmony-safe patch
infrastructure. Reference-only package — runtime DLL ships separately
via MelonLoader Mods folder.
</Description>
<PackageTags>melonloader;unity;il2cpp;modding;datacenter;gregcore</PackageTags>
<PackageProjectUrl>https://github.com/mleem97/gregCore</PackageProjectUrl>
<RepositoryUrl>https://github.com/mleem97/gregCore</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- ── Lizenz ────────────────────────────────────────────────── -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<!-- ── Icon & README ─────────────────────────────────────────── -->
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- ── Symbol-Paket (für Debugging) ─────────────────────────── -->
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedAllSources>true</EmbedAllSources>
<!-- ── Reference-Only-Paket ──────────────────────────────────── -->
<IsTool>false</IsTool>
<DevelopmentDependency>false</DevelopmentDependency>
<SuppressDependenciesWhenPacking>false</SuppressDependenciesWhenPacking>
<!-- ── Deterministic Build ───────────────────────────────────── -->
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<!-- ── Ausgabe-Pfad ───────────────────────────────────────────── -->
<PackageOutputPath>../../nupkgs</PackageOutputPath>
</PropertyGroup>
<Import Project="../shared/gregFramework.props" />
<!-- ── MelonLoader & Unity als PrivateAssets ─────────────────────── -->
<ItemGroup>
<Reference Include="MelonLoader">
<HintPath>$(MELON_PATH)\MelonLoader.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(MELON_PATH)\0Harmony.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Il2CppInterop.Runtime">
<HintPath>$(MELON_PATH)\Il2CppAssemblies\Il2CppInterop.Runtime.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>$(MELON_PATH)\Il2CppAssemblies\Assembly-CSharp.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(MELON_PATH)\Il2CppAssemblies\UnityEngine.CoreModule.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<!-- ── Statische Dateien ins Paket einbinden ─────────────────────── -->
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)../README.md" Pack="true" PackagePath="/" />
<None Include="$(MSBuildThisFileDirectory)../icon.png" Pack="true" PackagePath="/" />
<None Include="$(MSBuildThisFileDirectory)../LICENSE" Pack="true" PackagePath="/" />
<None Include="$(MSBuildThisFileDirectory)build/gregCore.props" Pack="true" PackagePath="build/" />
<None Include="$(MSBuildThisFileDirectory)build/gregCore.targets" Pack="true" PackagePath="build/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="4.1.0" />
</ItemGroup>
@@ -20,4 +98,4 @@
<EmbeddedResource Remove="tests\**" />
<None Remove="tests\**" />
</ItemGroup>
</Project>
</Project>
+1 -1
View File
@@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gregCore", "gregCore.csproj
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gregFramework", "gregFramework", "{9A0CCAB9-4303-13B4-2371-F1B97FF5B728}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gregCore.Tests", "gregTests\gregCore.Tests.csproj", "{FAF3FBA7-1865-41EF-BA90-215D9E7BF597}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gregCore.Tests", "tests\gregCore.Tests.csproj", "{FAF3FBA7-1865-41EF-BA90-215D9E7BF597}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
-862
View File
@@ -1,862 +0,0 @@
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;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace gregAssetExporter
{
internal static class MainCiBuildMarker
{
}
}
+196
View File
@@ -0,0 +1,196 @@
# modding_core_architecture_summary
> Zweck: Single Source of Truth für AI-/Bridge-Entwicklung (Lua, Rust, Go, Python, TS/JS) gegen gregCore.
>
> Scope: Laufzeit im Spielprozess (MelonLoader + Unity IL2CPP), inklusive FFI, IPC, Eventing und Sicherheitsgrenzen.
## 1) Runtime Lifecycle, Bootstrap & Schichten
### Schichtzuordnung (verbindlich)
- **Unity Spiel / IL2CPP Assembly (Game Layer):** gepatchte Spieltypen/Methoden (z. B. `Player.UpdateCoin`, `ServerPowerButton`, UI/Save-Methoden).
- **GregFramework Core SDK (Core Layer):** `src/gregModLoader/gregCore.cs`, `gregHarmonyPatches.cs`, `gregFfiBridge.cs`, `gregGameApi.cs`, `gregEventDispatcher.cs`.
- **Plugin Layer:** `src/gregModLoader/Plugins/*` (`gregPluginBase`, `gregRegistry`, Dependency-Resolver).
- **Language Bridges:** `src/Scripting/*` (`iGregLanguageBridge`, `gregLanguageBridgeHost`, Lua/JS/Rust/Go-Bridges).
- **Mod Layer:** User-Mods, native DLL-Mods (FFI), Script-Mods.
### Haupt-Entry und Aufrufreihenfolge
- **Core-Layer Entry:** `gregCoreLoader : MelonMod` in `src/gregModLoader/gregCore.cs`.
- Relevante Lifecycle-Methoden:
- `OnInitializeMelon()`
- `OnSceneWasLoaded(int buildIndex, string sceneName)`
- `OnUpdate()`
- `OnFixedUpdate()`
- `OnGUI()`
- `OnApplicationQuit()`
### Effektive Initialisierungssequenz
1. Core initialisiert Konfiguration, Aktivierung/Flags, Logging.
2. Core erstellt/initialisiert FFI (`gregFfiBridge`) und API-Tabelle (`gregGameApi`).
3. Core initialisiert Script-Host (`gregLanguageBridgeHost`) und lädt Bridges.
4. Core installiert Harmony-Patches (`gregHarmonyPatches`).
5. Plugins werden registriert/aufgelöst (`gregRegistry`, `gregDependencyResolver`) und über Ready-Callbacks aktiviert.
6. Runtime-Loop verteilt Update-Ticks an Core, Bridges und FFI-Module.
### Shutdown-Semantik
- `OnApplicationQuit()` triggert geordnetes Stoppen:
- Script-Host/Bridges herunterfahren,
- native FFI-Module via `mod_shutdown` und `FreeLibrary` freigeben,
- Pointer/Handles im `gregGameApi.Dispose()` bereinigen.
---
## 2) IL2CPP Hooking-Modell & Ausführungsfluss
### Hook-Ebene
- **Core Layer:** `src/gregModLoader/gregHarmonyPatches.cs` patcht IL2CPP-Spielmethoden mit Harmony Prefix/Postfix.
- Ziel ist **Event-Proxying**, nicht direkter unkontrollierter Mod-Zugriff auf Unity-Typen.
### Hook-zu-Event Pipeline
1. IL2CPP-Methode wird gepatcht (Prefix/Postfix).
2. Patch extrahiert primitive/struct-basierte Daten.
3. Dispatch über `EventDispatcher`/`gregEventDispatcher` mit numerischer `EventIds`-ID.
4. `GregHookIntegration` mappt `eventId -> greg.*` Hookname (`gregNativeEventHooks`).
5. Hook-Payload wird normalisiert (`BuildPayload(...)`) und an Bus/FFI weitergereicht.
### Canonical Hook-Namen
- Mapping zentral in `src/gregSdk/gregNativeEventHooks.cs`.
- Primärquelle für Namen: `greg_hooks.json` + framework-only Ergänzungen via `gregHookName.Create(...)`.
- Fallback bei unbekannten IDs: `greg.SYSTEM.UnmappedNativeEvent`.
### Cancelable vs Non-cancelable
- `gregEventDispatcher` unterstützt normale und cancelable Listener (`Func<string, object, bool>`).
- Cancel-Pfad wird in Patchpunkten genutzt, wo Spielaktion blockierbar ist.
---
## 3) Interop, FFI & IPC (ABI, Ports, Protokolle)
### Native FFI (Core ↔ Native Mod)
- **Core Layer Datei:** `src/gregModLoader/gregFfiBridge.cs`.
- Win32 Loader-API:
- `LoadLibrary`
- `GetProcAddress`
- `FreeLibrary`
- Erwartete native Exports (C-ABI):
- `mod_info`
- `mod_init`
- `mod_update`
- `mod_fixed_update`
- `mod_on_scene_loaded`
- `mod_on_gui`
- `mod_shutdown`
- `mod_on_event`
### API-Table für native Module
- **Core Layer Datei:** `src/gregModLoader/gregGameApi.cs`.
- Versioniertes Struct: `GameAPITable` (aktuell auf v12 erweitert).
- Delegates werden via `Marshal.GetFunctionPointerForDelegate(...)` als Funktionszeiger exportiert.
### IPC/Netzwerkflächen
- **MCP HTTP Server (Core/Tooling-Grenze):** `GregMCPServer` via `HttpListener` (localhost).
- **Multiplayer Transport (Plugin/Core):** WebSocket-Client in `GregMultiplayerService`.
- **Plugin-Sync (Core/Service):** `HttpClient` in `gregPluginSyncService`.
- **Native Plattform-Interop:** Steam-P2P via `steam_api64`-Imports in `gregGameApi.cs`.
### Bridge-Host für Sprachen
- **Language Bridge Layer:** `iGregLanguageBridge` + `gregLanguageBridgeHost`.
- Lua: MoonSharp; JS/TS: Jint; Rust/Go über Bridge-Adapter.
- Host ist der Isolations- und Lifecycle-Knoten für alle Script-Runtimes.
---
## 4) Memory Boundaries, Ownership & Lifetime
### Ownership-Regeln an der FFI-Grenze
- **Unmanaged Allokation durch Core:** `Marshal.AllocHGlobal`.
- **Freigabe durch Core nach Callback:** `Marshal.FreeHGlobal` (symmetrisch im Dispatch-Pfad).
- **String-Marshalling:** `Marshal.StringToHGlobalAnsi` / `Marshal.PtrToStringAnsi`.
- **Blittable Struct Transfer:** `[StructLayout(LayoutKind.Sequential)]` in Event-Payloads.
### Delegate-/Function-Pointer-Lifetime
- Delegates für API-Funktionen werden als Felder gehalten, damit der GC keine Funktionszeiger invalidiert.
- `GameAPITable` speichert `IntPtr` auf Delegate-Stubs; Freigabe zentral im `Dispose()`.
### GCHandle-Verwendung
- Für Event-/Payload-Weitergabe werden `GCHandle.Alloc(...)` Handles erzeugt.
- Handles werden nach Verwendung explizit gelöst (`GCHandle.Free()`), um Leaks/Pinning-Druck zu verhindern.
### Fehlerresilienz an Grenzstellen
- FFI-Aufrufe sind einzeln in `try/catch` eingefasst.
- Fehler in einem Modul dürfen den Core-Loop nicht terminieren (Fail-isolated execution).
### Kritische ABI-Regeln für externe Bindings
- Struct-Reihenfolge/Field-Typen sind ABI-kritisch; keine Reorder/Pack-Änderung ohne Version-Bump.
- Auf der Bridge-Seite nur stabile primitive Typen (`int`, `uint`, `float`, `byte[]`, `IntPtr`) übergeben.
- Keine Ownership-Mehrdeutigkeit: jeder Pointer braucht eindeutige „who frees“-Regel.
---
## 5) DTOs, Manifeste & Serialisierung
### Typfamilien
- **Runtime Event DTOs (Core):** `src/gregModLoader/Events/*.cs` (`iModEvent` mit `DateTime OccurredAtUtc`).
- **Native Event Struct DTOs (Core):** `src/gregModLoader/gregEventDispatcher.cs` (z. B. `ValueChangedData`, `DayEndedData`, `ShopItemAddedData`).
- **SDK/Config DTOs (Core/SDK):**
- `GregUiReplacementManifest`
- `ModelOverrideManifest`
- `ServerDefinition`
- `ItemDefinition`
- `PluginSyncConfig`, `PluginSyncManifest`
### Serializer-Stack
- `System.Text.Json` für Runtime-/Service-Pfade (`GregPersistenceService`, MCP-Antworten).
- `Newtonsoft.Json` in Teilen des Config-Stacks (`GregConfigService`).
- Konsequenz: DTO-Contracts sind serializer-agnostisch zu halten (öffentliche Properties/Fields stabil).
### Feld- und Versionsstabilität
- Event- und Manifest-Felder sind externe Verträge für Bridges/Mods.
- Änderungen nur additiv und versioniert (insb. API-Table + manifestartige Contracts).
- Entfernen/Umbenennen erzeugt harte Breaking Changes für native und Script-Bindings.
### Datentyp-Praxis für Multi-Language Bindings
- Bevorzugt JSON-kompatible Primitiven und flache Objekte.
- Für binäre Übergaben klar deklarierte Byte-Arrays + Länge.
- Keine impliziten Unity-/IL2CPP-Objektreferenzen in öffentlichen Bridge-DTOs.
---
## 6) Event-System, Hook-API & Sandboxing-Einschränkungen
### Event-Systeme (parallel vorhanden)
1. **String-basierter Hook-Bus (Core):** `gregEventDispatcher` mit Hooknamen `greg.<DOMAIN>.<Event>`.
2. **Type-safe Bus (SDK):** `GregEventBus` (`Subscribe<T>`, `Publish<T>`, `Unsubscribe<T>`).
3. **Native Event-ID Dispatch (Core↔FFI):** `EventIds` + struct-payload + `mod_on_event`.
### Hook-Namenskonvention
- Kanonisch: `greg.<Domain>.<Event>` (bzw. in Altbeständen teils uppercase Domains).
- Mappingquelle für native IDs: `gregNativeEventHooks.ByEventId`.
- Hook-Integration: `GregHookIntegration.EmitForSimple/EmitForStruct`.
### Sandboxing-Realität (wichtig für AI/Bridge-Design)
- **Lua I/O Modul:** `src/Scripting/Lua/LuaModules/gregIoLuaModule.cs` enthält Dateioperationen (`read_file`, `write_file`, `list_files` etc.).
- Kommentar behauptet Sandbox-Scope, Implementierung zeigt aktuell keine harte Pfad-Isolation auf OS-Ebene.
- **ReferenceScanner:** lädt Assemblies via `Assembly.LoadFrom` aus Dateisystemscan; keine AppDomain/Process-Isolation.
- Damit gilt: aktuelle Isolation ist primär **kooperativ/logisch**, nicht sicherheitstechnisch stark.
### Verbindliche Einschränkungen für neue Bindings
- Bridge-Code darf Core/Unity-Aufrufe nur über freigegebene API/Hooks ausführen.
- Keine direkten unsicheren Pointer-Operationen außerhalb definierter FFI-Wrapper.
- Untrusted Script/Native Mods als potenziell fehlerhaft behandeln (guard clauses + timeout/backpressure + exception fences).
- Für echte Sandbox-Anforderungen ist Prozessisolation (separater Host) nötig; im aktuellen In-Process-Modell nicht garantiert.
---
## Appendix: Operative Leitplanken für Bridge-Autoren
- **Schichtdisziplin:**
- Mod/Bridge → `greg.*` Hook/API,
- kein direkter Unity-Objektzugriff als öffentliches Contract.
- **ABI-Disziplin:**
- `StructLayout.Sequential`, feste Feldtypen, versionierte Erweiterung.
- **Fehlerdisziplin:**
- Jede Grenzstelle (`FFI`, `JSON parse`, `network`) mit `try/catch` + Logging.
- **Lifecycle-Disziplin:**
- `init -> update/fixed_update/gui -> shutdown` strikt einhalten.
- **Kompatibilität:**
- Runtime-Ziel bleibt `.NET 6` (IL2CPP/MelonLoader-kompatibel).
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-90
View File
@@ -1,90 +0,0 @@
#ifndef GREG_API_H
#define GREG_API_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
uint32_t api_version;
void (*log_info)(const char* msg);
void (*log_warning)(const char* msg);
void (*log_error)(const char* msg);
double (*get_player_money)();
void (*set_player_money)(double value);
float (*get_time_scale)();
void (*set_time_scale)(float value);
uint32_t (*get_server_count)();
uint32_t (*get_rack_count)();
const char* (*get_current_scene)();
// v2
double (*get_player_xp)();
void (*set_player_xp)(double value);
double (*get_player_reputation)();
void (*set_player_reputation)(double value);
float (*get_time_of_day)();
uint32_t (*get_day)();
float (*get_seconds_in_full_day)();
void (*set_seconds_in_full_day)(float value);
uint32_t (*get_switch_count)();
uint32_t (*get_satisfied_customer_count)();
// v3
void (*set_netwatch_enabled)(uint32_t enabled);
uint32_t (*is_netwatch_enabled)();
uint32_t (*get_netwatch_stats)();
// v4
uint32_t (*get_broken_server_count)();
uint32_t (*get_broken_switch_count)();
uint32_t (*get_eol_server_count)();
uint32_t (*get_eol_switch_count)();
uint32_t (*get_free_technician_count)();
uint32_t (*get_total_technician_count)();
int32_t (*dispatch_repair_server)();
int32_t (*dispatch_repair_switch)();
int32_t (*dispatch_replace_server)();
int32_t (*dispatch_replace_switch)();
// v5
int32_t (*register_custom_employee)(const char* id, const char* name, const char* desc, float salary, float req_rep, uint32_t confirm);
uint32_t (*is_custom_employee_hired)(const char* id);
int32_t (*fire_custom_employee)(const char* id);
int32_t (*register_salary)(int32_t monthly);
// v6
int32_t (*show_notification)(const char* msg);
float (*get_money_per_second)();
float (*get_expenses_per_second)();
float (*get_xp_per_second)();
uint32_t (*is_game_paused)();
void (*set_game_paused)(uint32_t paused);
int32_t (*get_difficulty)();
int32_t (*trigger_save)();
// v7
uint64_t (*steam_get_my_id)();
const char* (*steam_get_friend_name)(uint64_t id);
// ... other steam functions ...
void (*reserved[15])(); // Padding for v7 overflow
void (*get_player_position)(float* x, float* y, float* z, float* ry);
// v8
const char* (*payload_get_string)(void* payload, const char* field, const char* fallback);
void (*subscribe_event)(const char* hook, void (*handler)(void*), const char* mod_id);
void (*gui_begin_panel)(const char* id, float x, float y, float w, float h);
void (*gui_label)(const char* text);
void (*gui_end_panel)();
int32_t (*raycast_forward)(float max_dist, const char** out_name, float* out_dist, float* out_x, float* out_y, float* out_z);
void (*publish_tick)(float dt, int32_t frame);
} greg_api_t;
#ifdef __cplusplus
}
#endif
#endif
File diff suppressed because it is too large Load Diff
+82 -47
View File
@@ -3,18 +3,18 @@
$ErrorActionPreference = 'Stop'
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
$il2cppDir = Join-Path $repoRoot 'gregReferences\il2cpp-unpack\Assembly-CSharp\Il2Cpp'
$assemblyRoot = Join-Path $repoRoot 'gregReferences\Assembly-CSharp'
$harmonyPatches = Join-Path $repoRoot 'gregCore\framework\ModLoader\HarmonyPatches.cs'
$outJsonRoot = Join-Path $repoRoot 'greg_hooks.json'
$outJsonFramework = Join-Path $repoRoot 'gregCore\framework\greg_hooks.json'
$outHooksDir = Join-Path $repoRoot 'gregCore\framework\harmony'
if (-not (Test-Path $il2cppDir)) { throw "Missing Il2Cpp sources: $il2cppDir" }
if (-not (Test-Path $assemblyRoot)) { throw "Missing assembly source root: $assemblyRoot" }
New-Item -ItemType Directory -Force -Path $outHooksDir | Out-Null
function Get-HarmonyExclusions([string]$path) {
$set = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
if (-not (Test-Path $path)) { return $set }
if (-not (Test-Path $path)) { return ,$set }
$text = Get-Content $path -Raw
foreach ($m in [regex]::Matches($text, 'typeof\((\w+)\)\s*,\s*nameof\(\w+\.(\w+)\)')) {
[void]$set.Add("$($m.Groups[1].Value)|$($m.Groups[2].Value)")
@@ -22,20 +22,23 @@ function Get-HarmonyExclusions([string]$path) {
foreach ($m in [regex]::Matches($text, 'typeof\((\w+)\)\s*,\s*"(\w+)"')) {
[void]$set.Add("$($m.Groups[1].Value)|$($m.Groups[2].Value)")
}
return $set
return ,$set
}
$excluded = Get-HarmonyExclusions $harmonyPatches
[void]$excluded.Add('NetworkMap|RemapDeviceId')
[void]$excluded.Add('NetworkMap|RemoveIsolatedDevice')
[void]$excluded.Add('Technician|CacheDeviceBounds')
# Curated game surface for stable compile (full Il2Cpp tree needs extra Unity/asm refs).
$gameHookClasses = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
# Curated game surface for stable compile-time Harmony class generation.
$harmonyEmitClasses = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
@(
'Player', 'PlayerManager', 'PlayerHit', 'PlayerData',
'Server', 'MainGameManager', 'ComputerShop', 'HRSystem', 'SaveSystem', 'CustomerBase',
'CablePositions', 'CableLink', 'Rack', 'NetworkMap', 'BalanceSheet', 'MainMenu',
'TimeController', 'TechnicianManager', 'Technician', 'Objectives',
'PacketSpawnerSystem', 'NetworkSwitch', 'SFPModule', 'SFPBox', 'PatchPanel'
) | ForEach-Object { [void]$gameHookClasses.Add($_) }
) | ForEach-Object { [void]$harmonyEmitClasses.Add($_) }
function Test-SkipTypeName([string]$n) {
if ($n -match 'd__\d+$') { return $true }
@@ -120,6 +123,34 @@ function Test-SkipClass([string]$className, [string]$baseClause) {
return $false
}
function Get-SourceDirectories([string]$rootPath) {
$dirs = [System.Collections.Generic.List[string]]::new()
$includePatterns = @('Il2Cpp*', 'Unity*', 'UnityEngine*')
foreach ($pattern in $includePatterns) {
foreach ($dir in (Get-ChildItem -Path $rootPath -Directory -Filter $pattern -ErrorAction SilentlyContinue)) {
[void]$dirs.Add($dir.FullName)
}
}
if ($dirs.Count -eq 0) {
throw "No source directories matched under $rootPath (expected Il2Cpp*/Unity*)."
}
return $dirs
}
function Get-AssemblyNameFromPath([string]$filePath, [string]$rootPath) {
$relative = [System.IO.Path]::GetRelativePath($rootPath, $filePath)
if ([string]::IsNullOrWhiteSpace($relative)) { return 'Assembly-CSharp' }
$parts = $relative -split '[\\/]'
$first = if ($parts.Length -gt 0) { $parts[0] } else { '' }
if ([string]::IsNullOrWhiteSpace($first)) { return 'Assembly-CSharp' }
return $first
}
function Test-SkipInteropSignature([string]$returnType, [string]$argList) {
$blob = "$returnType $argList"
if ($blob -match 'EntityCommandBuffer|SystemState|BlobArray|ComponentLookup|BufferLookup') { return $true }
@@ -185,14 +216,13 @@ function Get-Il2CppPatchSignature([string]$className, [string]$methodName, [stri
function Split-MethodLine([string]$line, [ref]$isStatic) {
$isStatic.Value = $false
$m = [regex]::Match($line, '^\t{2}public unsafe static (?<sig>.+?)\s*\((?<args>[^\)]*)\)\s*$')
if ($m.Success) { $isStatic.Value = $true }
else {
$m = [regex]::Match($line, '^\t{2}public unsafe (?<sig>.+?)\s*\((?<args>[^\)]*)\)\s*$')
}
$m = [regex]::Match($line, '^\s+(?:public|private|protected|internal)\s+(?<mods>(?:(?:static|unsafe|virtual|override|abstract|sealed|new|extern|partial)\s+)*)?(?<sig>.+?)\s*\((?<args>[^\)]*)\)\s*$')
if (-not $m.Success) { return $null }
$mods = $m.Groups['mods'].Value
if ($mods -match '\bstatic\b') { $isStatic.Value = $true }
$sig = $m.Groups['sig'].Value.Trim()
if ($sig.StartsWith('static ')) { $sig = $sig.Substring(7).Trim() }
$argList = $m.Groups['args'].Value.Trim()
$idx = $sig.LastIndexOf(' ')
if ($idx -lt 0) { return $null }
@@ -235,22 +265,22 @@ function Build-EmitAnonymousBody([string]$className, [bool]$isStatic) {
$hooks = [System.Collections.Generic.List[object]]::new()
$byDomain = @{}
$scanDirectories = Get-SourceDirectories $assemblyRoot
$sourceFiles = $scanDirectories | ForEach-Object { Get-ChildItem -Path $_ -Filter '*.cs' -File -Recurse }
Get-ChildItem $il2cppDir -Filter '*.cs' -File | ForEach-Object {
$lines = Get-Content $_.FullName
$overloadFirst = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
foreach ($sourceFile in $sourceFiles) {
$assemblyName = Get-AssemblyNameFromPath $sourceFile.FullName $assemblyRoot
$lines = Get-Content $sourceFile.FullName
$harmonyOverloadFirst = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
$currentClass = $null
$currentBase = ''
$classRegex = [regex]'^\tpublic (sealed )?class (?<name>[A-Za-z0-9_]+)\s*:\s*(?<base>[^\r\n{]+)'
$classRegex = [regex]'^\s*(?:public|private|protected|internal)\s+(?:sealed\s+)?class\s+(?<name>[A-Za-z0-9_]+)(\s*:\s*(?<base>[^\r\n{]+))?'
foreach ($line in $lines) {
$cm = $classRegex.Match($line)
if ($cm.Success) {
$cn = $cm.Groups['name'].Value
$cb = $cm.Groups['base'].Value.Trim()
if (Test-SkipTypeName $cn) { $currentClass = $null; $currentBase = ''; continue }
if (Test-SkipClass $cn $cb) { $currentClass = $null; $currentBase = ''; continue }
if (-not $gameHookClasses.Contains($cn)) { $currentClass = $null; $currentBase = ''; continue }
$currentClass = $cn
$currentBase = $cb
continue
@@ -265,18 +295,9 @@ Get-ChildItem $il2cppDir -Filter '*.cs' -File | ForEach-Object {
$mn = $parsed.Name
$argList = $parsed.ArgList
if (-not (Test-ShouldEmitHook $mn $ret)) { continue }
if (Test-SkipInteropSignature $ret $argList) { continue }
if ($excluded.Contains("$currentClass|$mn")) { continue }
$ovKey = "$currentClass|$mn"
if ($overloadFirst.Contains($ovKey)) { continue }
[void]$overloadFirst.Add($ovKey)
$domain = Get-GregDomain $currentClass
$action = Get-SemanticAction $currentClass $mn
$strategy = Get-HookStrategy $mn $ret
if ($strategy -eq 'None') { continue }
$strategy = 'Postfix'
$hookName = "greg.$($domain.ToUpperInvariant()).$action"
$patchTarget = Get-Il2CppPatchSignature $currentClass $mn $argList
@@ -314,29 +335,41 @@ Get-ChildItem $il2cppDir -Filter '*.cs' -File | ForEach-Object {
name = $hookName
legacy = $null
patchTarget = $patchTarget
assembly = $assemblyName
strategy = $(if ($strategy -eq 'PrefixPostfix') { 'Prefix+Postfix' } else { 'Postfix' })
description = "Auto-generated from Il2Cpp unpack: $currentClass.$mn"
description = "Auto-generated from IL2CPP sources: $assemblyName/$currentClass.$mn"
payloadSchema = $payload
})
if (-not $byDomain.ContainsKey($domain)) { $byDomain[$domain] = [System.Collections.Generic.List[object]]::new() }
[void]$byDomain[$domain].Add([ordered]@{
Class = $currentClass
Method = $mn
Ret = $ret
Args = $argList
Action = $action
Strategy = $strategy
HookName = $hookName
IsStatic = $staticFlag
})
$harmonySignatureCompatible = $line -match '^\s+public unsafe(?:\s+static)?\s+'
$allowHarmonyEmit = $harmonyEmitClasses.Contains($currentClass) -and -not (Test-SkipTypeName $currentClass) -and -not (Test-SkipClass $currentClass $currentBase) -and $harmonySignatureCompatible
if ($allowHarmonyEmit -and (Test-ShouldEmitHook $mn $ret) -and -not (Test-SkipInteropSignature $ret $argList) -and -not $excluded.Contains("$currentClass|$mn")) {
$harmonyOvKey = "$currentClass|$mn"
if ($harmonyOverloadFirst.Contains($harmonyOvKey)) { continue }
[void]$harmonyOverloadFirst.Add($harmonyOvKey)
$harmonyStrategy = Get-HookStrategy $mn $ret
if ($harmonyStrategy -eq 'None') { continue }
if (-not $byDomain.ContainsKey($domain)) { $byDomain[$domain] = [System.Collections.Generic.List[object]]::new() }
[void]$byDomain[$domain].Add([ordered]@{
Class = $currentClass
Method = $mn
Ret = $ret
Args = $argList
Action = $action
Strategy = $harmonyStrategy
HookName = $hookName
IsStatic = $staticFlag
})
}
}
}
$seen = @{}
$unique = [System.Collections.Generic.List[object]]::new()
foreach ($h in $hooks) {
$k = [string]$h.patchTarget
$k = "$($h.assembly)|$([string]$h.patchTarget)"
if ($seen.ContainsKey($k)) { continue }
$seen[$k] = $true
[void]$unique.Add($h)
@@ -345,7 +378,7 @@ foreach ($h in $hooks) {
$doc = [ordered]@{
version = 2
description = 'Canonical greg hook registry. Schema: greg.<DOMAIN>.<Action>. Generated from Il2Cpp C# unpack; regenerate with gregCore/scripts/Generate-GregHooksFromIl2CppDump.ps1 when MergedCode.md / interop changes.'
generatedFrom = 'gregReferences/il2cpp-unpack/Assembly-CSharp/Il2Cpp/*.cs'
generatedFrom = 'gregReferences/Assembly-CSharp/{Il2Cpp*,Unity*,UnityEngine*}/**/*.cs'
legacyPrefixes = @()
hooks = $unique
}
@@ -370,7 +403,8 @@ foreach ($kv in $byDomain.GetEnumerator()) {
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine('using System;')
[void]$sb.AppendLine('using HarmonyLib;')
[void]$sb.AppendLine('using gregFramework.Core;')
[void]$sb.AppendLine('using greg.Core;')
[void]$sb.AppendLine('using greg.Sdk;')
[void]$sb.AppendLine('using Il2Cpp;')
[void]$sb.AppendLine('using Il2CppSystem.Collections.Generic;')
[void]$sb.AppendLine('using Il2CppInterop.Runtime.InteropTypes.Arrays;')
@@ -420,8 +454,8 @@ foreach ($kv in $byDomain.GetEnumerator()) {
[void]$sb.AppendLine(' {')
[void]$sb.AppendLine(' try')
[void]$sb.AppendLine(' {')
[void]$sb.AppendLine(' GregEventDispatcher.Emit(')
[void]$sb.AppendLine(" GregHookName.Create(GregDomain.$gregDomain, `"$action`"),")
[void]$sb.AppendLine(' gregEventDispatcher.Emit(')
[void]$sb.AppendLine(" gregHookName.Create(GregDomain.$gregDomain, `"$action`"),")
[void]$sb.AppendLine(' new')
[void]$sb.AppendLine(' {')
[void]$sb.AppendLine($emitBody)
@@ -461,3 +495,4 @@ internal static class GregPowerHooks
Write-Host "Wrote stub $powerFile"
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für Konfigurations-Management.
/// Maintainer: Wird für mod.json und globale Configs (Newtonsoft.Json) genutzt.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregConfigService
{
T? LoadConfig<T>(string filePath) where T : class;
void SaveConfig<T>(string filePath, T config) where T : class;
}
+14
View File
@@ -0,0 +1,14 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für das Event-Bus System.
/// Maintainer: Zentraler Message-Broker für alle Mods und Hooks.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregEventBus
{
void Subscribe(string hookName, Action<EventPayload> handler);
void Unsubscribe(string hookName, Action<EventPayload> handler);
bool Publish(string hookName, EventPayload payload);
}
+13
View File
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für Foreign Function Interface (Win32 FFI).
/// Maintainer: Lädt und bindet native Bibliotheken (C++/Rust).
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregFfiBridge
{
void Initialize();
void LoadNativeMod(string dllPath);
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für Skript-Sprachen-Brücken (Lua/JS).
/// Maintainer: Ermöglicht das Hinzufügen neuer Skriptsprachen ohne Core-Änderung.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregLanguageBridge
{
void Initialize();
void ExecuteScript(string scriptContent);
}
+16
View File
@@ -0,0 +1,16 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für Logging.
/// Maintainer: Einziger Vertrag für Logs, entkoppelt MelonLogger vom Framework.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregLogger
{
void Debug(string message);
void Info(string message);
void Warning(string message);
void Error(string message, Exception? ex = null);
IGregLogger ForContext(string context);
}
+13
View File
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für den MCP-Endpunkt (Model Context Protocol).
/// Maintainer: Bereitstellung von HTTP-basierten Debug/Mod-Schnittstellen.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregMcpServer
{
void Start(int port);
void Stop();
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für Daten-Persistenz.
/// Maintainer: Wird für Spielstände und Mod-Daten (System.Text.Json) genutzt.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregPersistenceService
{
T? LoadData<T>(string key) where T : class;
void SaveData<T>(string key, T data) where T : class;
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für die Registrierung von Mods/Plugins.
/// Maintainer: Verwaltet den Lifecycle aller geladenen Plugins.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregPluginRegistry
{
void LoadAll();
IReadOnlyList<PluginInfo> GetLoadedPlugins();
}
+16
View File
@@ -0,0 +1,16 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Definiert eindeutige IDs für Native FFI Events.
/// Maintainer: Muss synchron mit C/Rust Headern bleiben.
/// </file-summary>
namespace gregCore.Core.Events;
public static class EventIds
{
// [GREG_SYNC_INSERT_EVENTIDS]
public const int PlayerCoinUpdated = 1001;
public const int GameSaved = 2001;
public const int ServerStatusChanged = 3001;
}
+37
View File
@@ -0,0 +1,37 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Hilfsklasse zum schnellen Erstellen von EventPayloads.
/// Maintainer: Reduziert Boilerplate beim Dispatchen von Events.
/// </file-summary>
namespace gregCore.Core.Events;
public static class EventPayloadBuilder
{
public static EventPayload ForScene(int buildIndex, string sceneName) =>
new EventPayload
{
HookName = HookName.Create("lifecycle", "SceneLoaded").Full,
OccurredAtUtc = DateTime.UtcNow,
Data = new Dictionary<string, object>
{
{ "BuildIndex", buildIndex },
{ "SceneName", sceneName }
},
IsCancelable = false
};
public static EventPayload ForValueChange(string propertyName, object oldValue, object newValue) =>
new EventPayload
{
HookName = string.Empty, // Wird vom Caller überschrieben
OccurredAtUtc = DateTime.UtcNow,
Data = new Dictionary<string, object>
{
{ "Property", propertyName },
{ "OldValue", oldValue },
{ "NewValue", newValue }
},
IsCancelable = true
};
}
+143
View File
@@ -0,0 +1,143 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Thread-sichere Implementierung des IGregEventBus.
/// Maintainer: Publish läuft synchron auf dem aufrufenden Thread. Caching für Hotpath.
/// </file-summary>
namespace gregCore.Core.Events;
public sealed class GregEventBus : IGregEventBus, IDisposable
{
private readonly IGregLogger _logger;
private readonly Dictionary<string, List<Action<EventPayload>>> _handlers = new();
private readonly Dictionary<string, Action<EventPayload>[]> _cachedHandlers = new();
private readonly ReaderWriterLockSlim _rwLock = new();
private bool _isDirty = true;
private bool _disposed;
public GregEventBus(IGregLogger logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger.ForContext("EventBus");
}
public void Subscribe(string hookName, Action<EventPayload> handler)
{
ArgumentNullException.ThrowIfNull(hookName);
ArgumentNullException.ThrowIfNull(handler);
_rwLock.EnterWriteLock();
try
{
if (!_handlers.TryGetValue(hookName, out var list))
{
list = new List<Action<EventPayload>>();
_handlers[hookName] = list;
}
list.Add(handler);
_isDirty = true;
}
finally
{
_rwLock.ExitWriteLock();
}
}
public void Unsubscribe(string hookName, Action<EventPayload> handler)
{
ArgumentNullException.ThrowIfNull(hookName);
ArgumentNullException.ThrowIfNull(handler);
_rwLock.EnterWriteLock();
try
{
if (_handlers.TryGetValue(hookName, out var list))
{
list.Remove(handler);
_isDirty = true;
}
}
finally
{
_rwLock.ExitWriteLock();
}
}
public bool Publish(string hookName, EventPayload payload)
{
ArgumentNullException.ThrowIfNull(hookName);
Action<EventPayload>[]? handlersToInvoke = null;
_rwLock.EnterReadLock();
try
{
if (_isDirty)
{
_rwLock.ExitReadLock();
_rwLock.EnterWriteLock();
try
{
if (_isDirty)
{
_cachedHandlers.Clear();
foreach (var kvp in _handlers)
{
_cachedHandlers[kvp.Key] = kvp.Value.ToArray();
}
_isDirty = false;
}
}
finally
{
_rwLock.ExitWriteLock();
_rwLock.EnterReadLock();
}
}
_cachedHandlers.TryGetValue(hookName, out handlersToInvoke);
}
finally
{
_rwLock.ExitReadLock();
}
if (handlersToInvoke == null || handlersToInvoke.Length == 0) return true;
var currentPayload = payload with { HookName = hookName };
foreach (var handler in handlersToInvoke)
{
try
{
handler(currentPayload);
if (currentPayload.IsCancelable && currentPayload.IsCancelled)
{
return false;
}
}
catch (Exception ex)
{
_logger.Error($"Fehler in Handler für {hookName}", ex);
}
}
return true;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_rwLock.Dispose();
}
_disposed = true;
}
}
+21
View File
@@ -0,0 +1,21 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Mappt Hook-Namen auf Native Event-IDs für FFI.
/// Maintainer: Erlaubt es nativen Mods, Events via ID zu empfangen.
/// </file-summary>
namespace gregCore.Core.Events;
public static class NativeEventHooks
{
private static readonly Dictionary<string, int> _hookToId = new()
{
// [GREG_SYNC_INSERT_MAPPINGS]
{ HookName.Create("economy", "PlayerCoinUpdated").Full, EventIds.PlayerCoinUpdated },
{ HookName.Create("persistence", "GameSaved").Full, EventIds.GameSaved },
{ HookName.Create("hardware", "ServerStatusChanged").Full, EventIds.ServerStatusChanged }
};
public static bool TryGetEventId(string hookName, out int eventId) =>
_hookToId.TryGetValue(hookName, out eventId);
}
+13
View File
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Exception für ABI (Application Binary Interface) Mismatches bei nativen Mods.
/// Maintainer: Wird geworfen, wenn GameAPITable Versionen nicht übereinstimmen.
/// </file-summary>
namespace gregCore.Core.Exceptions;
public class GregAbiException : GregCoreException
{
public GregAbiException(string message) : base(message) { }
public GregAbiException(string message, Exception inner) : base(message, inner) { }
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Exception für Fehler innerhalb von Language Bridges (Lua/JS/FFI).
/// Maintainer: Sollte abgefangen und isoliert geloggt werden, crasht nie den Main Thread.
/// </file-summary>
namespace gregCore.Core.Exceptions;
public class GregBridgeException : GregCoreException
{
public GregBridgeException(string message) : base(message) { }
public GregBridgeException(string message, Exception inner) : base(message, inner) { }
}
+13
View File
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Basis-Exception für alle frameworkeigenen Exceptions.
/// Maintainer: Alle eigenen Exceptions sollten hiervon erben.
/// </file-summary>
namespace gregCore.Core.Exceptions;
public abstract class GregCoreException : Exception
{
protected GregCoreException(string message) : base(message) { }
protected GregCoreException(string message, Exception inner) : base(message, inner) { }
}
+13
View File
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Exception für Initialisierungsfehler des Frameworks.
/// Maintainer: Framework-eigene Exception, keine Unity-Abhängigkeit.
/// </file-summary>
namespace gregCore.Core.Exceptions;
public class GregInitException : GregCoreException
{
public GregInitException(string message) : base(message) { }
public GregInitException(string message, Exception inner) : base(message, inner) { }
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Exception für Fehler beim Laden von Plugins oder Auflösen von Abhängigkeiten.
/// Maintainer: Wird geworfen bei zyklischen oder fehlenden Abhängigkeiten.
/// </file-summary>
namespace gregCore.Core.Exceptions;
public class GregPluginLoadException : GregCoreException
{
public GregPluginLoadException(string message) : base(message) { }
public GregPluginLoadException(string message, Exception inner) : base(message, inner) { }
}
+12
View File
@@ -0,0 +1,12 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Enthält die Versionsnummer der GameAPITable für FFI.
/// Maintainer: Nach jeder ABI-Änderung in GameApiTable.cs muss diese Version erhöht werden.
/// </file-summary>
namespace gregCore.Core.Models;
public static class ApiTableVersion
{
public const int Current = 12;
}
+19
View File
@@ -0,0 +1,19 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Datenmodell für Events, die über den IGregEventBus verschickt werden.
/// Maintainer: Blittable struct wo möglich. IsCancelled ist das einzige mutable Feld.
/// </file-summary>
namespace gregCore.Core.Models;
// [GREG_SYNC_INSERT_DTOS]
[StructLayout(LayoutKind.Sequential)]
public record struct EventPayload
{
public string HookName { get; init; }
public DateTime OccurredAtUtc { get; init; }
public IReadOnlyDictionary<string, object> Data { get; init; }
public bool IsCancelable { get; init; }
public bool IsCancelled { get; set; }
}
+36
View File
@@ -0,0 +1,36 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Repräsentiert einen eindeutigen Hook-Namen.
/// Maintainer: Format "greg.{Domain}.{Event}".
/// </file-summary>
namespace gregCore.Core.Models;
public readonly record struct HookName
{
public string Domain { get; init; }
public string Event { get; init; }
private readonly string? _full;
public string Full => _full ?? $"greg.{Domain}.{Event}";
public static HookName Parse(string full)
{
ArgumentNullException.ThrowIfNull(full);
var parts = full.Split('.');
if (parts.Length >= 3 && parts[0] == "greg")
{
return new HookName { Domain = parts[1], Event = parts[2], _full = full };
}
throw new ArgumentException($"Invalid HookName format: {full}");
}
public static HookName Create(string domain, string eventName)
{
ArgumentNullException.ThrowIfNull(domain);
ArgumentNullException.ThrowIfNull(eventName);
return new HookName { Domain = domain, Event = eventName, _full = $"greg.{domain}.{eventName}" };
}
public override string ToString() => Full;
}
+16
View File
@@ -0,0 +1,16 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Datenmodell für das Manifest eines Mods (mod.json).
/// Maintainer: Reines DTO, serializer-agnostisch.
/// </file-summary>
namespace gregCore.Core.Models;
public record ModManifest
{
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public string Version { get; init; } = "1.0.0";
public string Author { get; init; } = string.Empty;
public IReadOnlyList<string> Dependencies { get; init; } = Array.Empty<string>();
}
+14
View File
@@ -0,0 +1,14 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Datenmodell für die Metadaten eines geladenen Plugins.
/// Maintainer: Reines DTO, serializer-agnostisch.
/// </file-summary>
namespace gregCore.Core.Models;
public record PluginInfo
{
public string AssemblyPath { get; init; } = string.Empty;
public ModManifest Manifest { get; init; } = new();
public bool IsNative { get; init; }
}
+15
View File
@@ -0,0 +1,15 @@
# gregCore Architektur
## Schichten
Core/ → Framework-Kern, kein Unity-Coupling, vollständig testbar
Infrastructure/ → Implementierungen, kennt MelonLoader/Win32/Mono
GameLayer/ → IL2CPP + Harmony, nur Daten-Extraktion + Dispatch
PublicApi/ → Was Modder nutzen dürfen
## Goldene Regeln
1. Patches haben KEINE Business-Logic
2. GregCoreLoader hat MAX 50 Zeilen
3. GameAPITable: Neue Felder NUR ans Ende
4. Alle Services via Interface — nie direkt instanziieren
5. MelonLogger nur in MelonLoggerAdapter
6. Assembly.LoadFrom VERBOTEN — immer Mono.Cecil
+24
View File
@@ -0,0 +1,24 @@
namespace greg.Diagnostic;
public sealed class FrameLimiterConfig
{
public bool Enabled { get; set; } = true;
public FpsProfile Menu { get; set; } = new() { TargetFps = 30, VSync = 0 };
public FpsProfile Gameplay { get; set; } = new() { TargetFps = 144, VSync = 0 };
public AfkProfile Afk { get; set; } = new();
public FpsProfile Minimized { get; set; } = new() { TargetFps = 5, VSync = 0 };
public FpsProfile Background { get; set; } = new() { TargetFps = 20, VSync = 0 };
}
public class FpsProfile
{
public int TargetFps { get; set; } = 60;
public int VSync { get; set; } = 0;
}
public sealed class AfkProfile : FpsProfile
{
public bool Enabled { get; set; } = true;
public float AfkAfterSeconds { get; set; } = 60f;
public AfkProfile() { TargetFps = 15; }
}
+150
View File
@@ -0,0 +1,150 @@
using System;
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
namespace greg.Diagnostic;
public sealed class GregFrameLimiterService
{
public static GregFrameLimiterService Instance { get; private set; } = null!;
public enum GameState { Unknown, Menu, Gameplay, Loading }
public string CurrentStateName => _currentState.ToString();
private GameState _currentState = GameState.Unknown;
private bool _isFocused = true;
private bool _isMinimized = false;
private bool _isAfk = false;
private float _lastInputTime = 0f;
private int _frame = 0;
private FrameLimiterConfig? _cfg;
public void Initialize(FrameLimiterConfig cfg)
{
Instance = this;
_cfg = cfg;
if (cfg == null || !cfg.Enabled)
{
MelonLogger.Msg("[FrameLimiter] Disabled via config.");
return;
}
SetState(GameState.Menu);
MelonLogger.Msg("[FrameLimiter] Initialized. Menu limit active.");
}
public void SetState(GameState state)
{
if (_currentState == state) return;
_currentState = state;
ApplyCurrentLimit();
}
public void OnFocusChanged(bool focused)
{
_isFocused = focused;
ApplyCurrentLimit();
}
public void OnMinimizeChanged(bool minimized)
{
_isMinimized = minimized;
ApplyCurrentLimit();
}
public void Tick()
{
if (_cfg == null || !_cfg.Enabled) return;
bool anyInput = false;
try
{
if (Keyboard.current != null)
{
anyInput = Keyboard.current.anyKey.isPressed;
}
if (!anyInput && Mouse.current != null)
{
anyInput = Mouse.current.delta.ReadValue().sqrMagnitude > 0.01f;
}
}
catch { }
if (anyInput)
{
_lastInputTime = Time.realtimeSinceStartup;
if (_isAfk)
{
_isAfk = false;
ApplyCurrentLimit();
MelonLogger.Msg("[FrameLimiter] AFK ended — restoring limit.");
}
}
else if (!_isAfk
&& _cfg?.Afk?.Enabled == true
&& _currentState == GameState.Gameplay
&& (Time.realtimeSinceStartup - _lastInputTime) > _cfg.Afk.AfkAfterSeconds)
{
_isAfk = true;
ApplyCurrentLimit();
MelonLogger.Msg("[FrameLimiter] AFK detected — reducing FPS.");
}
}
private void ApplyCurrentLimit()
{
if (_cfg == null || !_cfg.Enabled) return;
int targetFps;
int vSync;
string reason;
if (_isMinimized)
{
targetFps = _cfg.Minimized.TargetFps;
vSync = _cfg.Minimized.VSync;
reason = "minimized";
}
else if (!_isFocused)
{
targetFps = _cfg.Background.TargetFps;
vSync = _cfg.Background.VSync;
reason = "background";
}
else if (_isAfk)
{
targetFps = _cfg.Afk.TargetFps;
vSync = _cfg.Afk.VSync;
reason = "afk";
}
else
{
(targetFps, vSync, reason) = _currentState switch
{
GameState.Menu => (_cfg.Menu.TargetFps, _cfg.Menu.VSync, "menu"),
GameState.Loading => (_cfg.Menu.TargetFps, _cfg.Menu.VSync, "loading"),
GameState.Gameplay => (_cfg.Gameplay.TargetFps, _cfg.Gameplay.VSync, "gameplay"),
_ => (_cfg.Menu.TargetFps, _cfg.Menu.VSync, "unknown"),
};
}
try
{
if (Application.targetFrameRate == targetFps
&& QualitySettings.vSyncCount == vSync)
return;
Application.targetFrameRate = targetFps;
QualitySettings.vSyncCount = vSync;
MelonLogger.Msg($"[FrameLimiter] {reason} → targetFPS={targetFps} vSync={vSync}");
}
catch (Exception ex)
{
MelonLogger.Warning($"[FrameLimiter] ApplyCurrentLimit failed: {ex.Message}");
}
}
}
+46
View File
@@ -0,0 +1,46 @@
using System.Text.Json.Serialization;
namespace greg.Diagnostic;
public sealed class GregPerfConfig
{
public static GregPerfConfig Instance { get; set; } = new();
public bool FrameCapEnabled { get; set; } = true;
public int MenuFps { get; set; } = 30;
public int GameplayFps { get; set; } = 144;
public int BackgroundFps { get; set; } = 20;
public int AfkFps { get; set; } = 15;
public bool AfkEnabled { get; set; } = true;
public float AfkSeconds { get; set; } = 60f;
public int MaxAllowedFps { get; set; } = 240;
[JsonIgnore]
public int CurrentTarget { get; set; } = 30;
public bool ThreadingEnabled { get; set; } = true;
public int PhysicalCores { get; set; } = 0;
public bool GcOptEnabled { get; set; } = true;
public int GcTriggerMb { get; set; } = 256;
public bool IncrementalGc { get; set; } = true;
public bool RenderOptEnabled { get; set; } = true;
public bool ReduceShadows { get; set; } = true;
public float ShadowDistanceM { get; set; } = 50f;
public int ShadowCascades { get; set; } = 2;
public bool AggressiveLod { get; set; } = true;
public float LodBias { get; set; } = 1.0f;
public int MaxLodLevel { get; set; } = 0;
public bool LimitPixelLights { get; set; } = true;
public int MaxPixelLights { get; set; } = 2;
public bool ReduceTextureQuality { get; set; } = false;
public int TextureMipMapLimit { get; set; } = 0;
public bool DisableSoftParticles { get; set; } = true;
public bool DisableHeavyPostProcessing { get; set; } = true;
public bool DisableMotionBlur { get; set; } = true;
public bool DisableBloom { get; set; } = false;
public bool DisableDoF { get; set; } = true;
public bool DisableAO { get; set; } = false;
public bool DisableSSR { get; set; } = true;
}
+48
View File
@@ -0,0 +1,48 @@
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
namespace greg.Diagnostic;
public sealed class GregPerformanceHud : MelonMod
{
private bool _visible = false;
private string _displayText = "";
private float _updateTimer = 0f;
public override void OnUpdate()
{
try
{
if (Keyboard.current?.f9Key?.wasPressedThisFrame == true)
_visible = !_visible;
}
catch { }
if (!_visible) return;
_updateTimer += Time.unscaledDeltaTime;
if (_updateTimer < 1f) return;
_updateTimer = 0f;
float currentFps = Time.unscaledDeltaTime > 0 ? 1f / Time.unscaledDeltaTime : 0f;
float targetFps = Application.targetFrameRate;
string state = GregFrameLimiterService.Instance?.CurrentStateName ?? "?";
long ramMb = System.GC.GetTotalMemory(false) / 1024 / 1024;
int gpuMb = SystemInfo.graphicsMemorySize;
_displayText =
$"gregCore Performance\n" +
$"FPS: {targetFps}/{currentFps:F0}\n" +
$"State: {state}\n" +
$"RAM: {ramMb}MB\n" +
$"GPU: {gpuMb}MB\n" +
$"[F9] hide";
}
public override void OnGUI()
{
if (!_visible) return;
GUI.Box(new Rect(10, 10, 200, 160), _displayText);
}
}
+461
View File
@@ -0,0 +1,461 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using HarmonyLib;
using Il2Cpp;
using MelonLoader;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
namespace greg.Diagnostic;
// ── Main-thread dispatch queue ─────────────────────────────────────────────
internal static class MainThreadDispatch
{
private static readonly ConcurrentQueue<Action> _queue = new();
public static void Enqueue(Action action) => _queue.Enqueue(action);
public static void Drain()
{
while (_queue.TryDequeue(out var action))
{
try { action(); }
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] MainThreadDispatch error: {ex.Message}"); }
}
}
}
public static class GregPerformanceOptimizer
{
// ── Preferences ───────────────────────────────────────────────────────
public static MelonPreferences_Entry<bool> CanvasThrottleEnabled;
public static MelonPreferences_Entry<float> CanvasUpdateInterval;
public static MelonPreferences_Entry<bool> IndicatorThrottleEnabled;
public static MelonPreferences_Entry<float> IndicatorUpdateInterval;
public static MelonPreferences_Entry<bool> ThrottlePulsating;
public static MelonPreferences_Entry<float> PulsatingUpdateInterval;
public static MelonPreferences_Entry<float> RouteEvalCooldown;
public static MelonPreferences_Entry<bool> AsyncRouteEval;
public static MelonPreferences_Entry<float> AutoSaveMinutes;
// NPCs
public static MelonPreferences_Entry<bool> NpcEnabled;
public static MelonPreferences_Entry<float> NpcThrottleDistance;
public static MelonPreferences_Entry<float> NpcThrottleInterval;
// Memory
public static MelonPreferences_Entry<bool> MemoryEnabled;
public static MelonPreferences_Entry<int> TextureMipmapLimit;
public static MelonPreferences_Entry<bool> StreamingMipmaps;
public static MelonPreferences_Entry<float> StreamingMipmapsBudgetMB;
public static MelonPreferences_Entry<float> PeriodicGCIntervalSeconds;
// Graphics
public static MelonPreferences_Entry<bool> GraphicsEnabled;
public static MelonPreferences_Entry<float> ShadowDistance;
public static MelonPreferences_Entry<float> CameraFarClip;
public static MelonPreferences_Entry<float> LodBias;
public static MelonPreferences_Entry<bool> DisableSSAO;
public static MelonPreferences_Entry<bool> DisableContactShadows;
public static MelonPreferences_Entry<bool> DisableGlobalIllumination;
public static MelonPreferences_Entry<bool> DisableSSR;
public static MelonPreferences_Entry<bool> DisableVolumetricFog;
// ── OLD TWEAKS ────────────────────────────────────────────────────────
public static MelonPreferences_Entry<int> TargetFPS;
public static void ApplySettings()
{
Initialize();
}
public static void Initialize()
{
var cat = MelonPreferences.CreateCategory("PerfFix");
TargetFPS = cat.CreateEntry("TargetFPS", 120, "TargetFPS", "Target framerate. 0 = uncap.");
CanvasThrottleEnabled = cat.CreateEntry("CanvasThrottle", true, "CanvasThrottle",
"Throttle WorldCanvasCuller.Update() to CanvasUpdateInterval Hz instead of every frame.");
CanvasUpdateInterval = cat.CreateEntry("CanvasUpdateInterval", 0.1f, "CanvasUpdateInterval",
"Seconds between WorldCanvasCuller distance checks (0.1 = 10 Hz). Vanilla runs at full framerate.");
IndicatorThrottleEnabled = cat.CreateEntry("IndicatorThrottle", true, "IndicatorThrottle",
"Throttle PositionIndicator.Update() (warning/error triangles) — re-projects world→screen every frame.");
IndicatorUpdateInterval = cat.CreateEntry("IndicatorUpdateInterval", 0.1f, "IndicatorUpdateInterval",
"Seconds between PositionIndicator screen-position updates (0.1 = 10 Hz).");
ThrottlePulsating = cat.CreateEntry("ThrottlePulsating", true, "ThrottlePulsating",
"Throttle PulsatingImageColor and PulsatingText Update() calls.");
PulsatingUpdateInterval = cat.CreateEntry("PulsatingUpdateInterval", 0.05f, "PulsatingUpdateInterval",
"Seconds between pulsating effect updates (0.05 = 20 Hz).");
RouteEvalCooldown = cat.CreateEntry("RouteEvalCooldown", 2.0f, "RouteEvalCooldown",
"Minimum seconds between full ECS cable-route re-evaluations.");
AsyncRouteEval = cat.CreateEntry("AsyncRouteEval", false, "AsyncRouteEval",
"EXPERIMENTAL (DISABLED!): Run EvaluateAllRoutes on a background thread. CAUSES IL2CPP CRASHES.");
AutoSaveMinutes = cat.CreateEntry("AutoSaveMinutes", 10.0f, "AutoSaveMinutes",
"Minutes between auto-saves. Large saves cause frame hitches. Set to 0 to disable.");
GraphicsEnabled = cat.CreateEntry("GraphicsEnabled", true, "GraphicsEnabled",
"Master toggle for the graphics fixes below.");
ShadowDistance = cat.CreateEntry("ShadowDistance", 20f, "ShadowDistance",
"HDRP shadow cull distance in metres. Vanilla is ~150 m.");
CameraFarClip = cat.CreateEntry("CameraFarClip", 80f, "CameraFarClip",
"Player camera far clip plane in metres. Vanilla is ~1000 m.");
LodBias = cat.CreateEntry("LodBias", 0.4f, "LodBias",
"Unity LOD bias. Lower = switch to cheaper LOD meshes sooner.");
DisableSSAO = cat.CreateEntry("DisableSSAO", true, "DisableSSAO",
"Disable HDRP Screen-Space Ambient Occlusion.");
DisableContactShadows = cat.CreateEntry("DisableContactShadows", true, "DisableContactShadows",
"Disable HDRP Contact Shadows.");
DisableGlobalIllumination = cat.CreateEntry("DisableGlobalIllumination", true, "DisableGlobalIllumination",
"Disable HDRP Screen-Space Global Illumination.");
DisableSSR = cat.CreateEntry("DisableSSR", true, "DisableSSR",
"Disable HDRP Screen-Space Reflections on floors/racks.");
DisableVolumetricFog = cat.CreateEntry("DisableVolumetricFog", false, "DisableVolumetricFog",
"Disable HDRP Volumetric Fog.");
NpcEnabled = cat.CreateEntry("NpcEnabled", true, "NpcEnabled",
"Master toggle for NPC/Technician optimizations.");
NpcThrottleDistance = cat.CreateEntry("NpcThrottleDistance", 15f, "NpcThrottleDistance",
"Distance in metres beyond which Technician FixedUpdate and LateUpdate are throttled.");
NpcThrottleInterval = cat.CreateEntry("NpcThrottleInterval", 0.2f, "NpcThrottleInterval",
"Seconds between FixedUpdate/LateUpdate ticks for distant technicians.");
MemoryEnabled = cat.CreateEntry("MemoryEnabled", true, "MemoryEnabled",
"Master toggle for memory reduction settings below.");
TextureMipmapLimit = cat.CreateEntry("TextureMipmapLimit", 1, "TextureMipmapLimit",
"Global texture mipmap skip level. 0=full resolution, 1=half resolution, 2=quarter resolution.");
StreamingMipmaps = cat.CreateEntry("StreamingMipmaps", true, "StreamingMipmaps",
"Enable Unity mipmap streaming.");
StreamingMipmapsBudgetMB = cat.CreateEntry("StreamingMipmapsBudgetMB", 512f, "StreamingMipmapsBudgetMB",
"Memory budget for streamed mipmaps in megabytes.");
PeriodicGCIntervalSeconds = cat.CreateEntry("PeriodicGCIntervalSeconds", 0f, "PeriodicGCIntervalSeconds",
"Seconds between forced garbage collection passes. Set to 0 to disable. (WARNING: Enabled GC has been known to crash Il2CppInterop)");
MelonLogger.Msg($"[gregCore.PerfFix] Loaded. " +
$"Canvas={CanvasUpdateInterval.Value}s " +
$"RouteEval={RouteEvalCooldown.Value}s " +
$"AutoSave={AutoSaveMinutes.Value}min " +
$"FarClip={CameraFarClip.Value}m");
ApplyMemorySettings();
// Base Performance Tweaks
int targetFPS = TargetFPS.Value > 0 ? TargetFPS.Value : Screen.currentResolution.refreshRate;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = targetFPS > 0 ? targetFPS : 120;
}
private static float _nextGC = 0f;
public static void OnUpdate()
{
MainThreadDispatch.Drain();
float interval = PeriodicGCIntervalSeconds?.Value ?? 0f;
if (interval > 0f && Time.realtimeSinceStartup >= _nextGC && _nextGC > 0f)
{
_nextGC = Time.realtimeSinceStartup + interval;
MelonCoroutines.Start(RunPeriodicGC());
}
}
private static System.Collections.IEnumerator RunPeriodicGC()
{
yield return null;
// GC forcing removed - Causes Il2CppInterop/Unity Finalizer NullReferenceException
MelonLogger.Msg($"[gregCore.PerfFix] Periodic GC skipped to prevent Il2CppInterop crashes.");
}
public static void OnSceneLoaded()
{
ApplySimulationFixes();
MelonCoroutines.Start(ApplyGraphicsFixesNextFrame());
if (PeriodicGCIntervalSeconds != null)
{
float interval = PeriodicGCIntervalSeconds.Value;
_nextGC = interval > 0f ? Time.realtimeSinceStartup + interval : 0f;
}
}
private static void ApplySimulationFixes()
{
try
{
var wis = WaypointInitializationSystem.Instance;
if (wis != null)
{
wis.SetEvaluationCooldown(RouteEvalCooldown.Value);
MelonLogger.Msg($"[gregCore.PerfFix] RouteEvalCooldown → {RouteEvalCooldown.Value}s");
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] RouteEvalCooldown: {ex.Message}"); }
try
{
var mgr = MainGameManager.instance;
if (mgr != null)
{
if (AutoSaveMinutes.Value <= 0f)
{
mgr.SetAutoSaveEnabled(false);
MelonLogger.Msg("[gregCore.PerfFix] AutoSave disabled.");
}
else
{
mgr.SetAutoSaveEnabled(true);
mgr.SetAutoSaveInterval(AutoSaveMinutes.Value);
MelonLogger.Msg($"[gregCore.PerfFix] AutoSave interval → {AutoSaveMinutes.Value} min");
}
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] AutoSave: {ex.Message}"); }
}
private static System.Collections.IEnumerator ApplyGraphicsFixesNextFrame()
{
yield return null;
ApplyGraphicsFixes();
}
private static void ApplyMemorySettings()
{
if (!MemoryEnabled.Value) return;
try
{
QualitySettings.globalTextureMipmapLimit = TextureMipmapLimit.Value;
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] TextureMipmapLimit: {ex.Message}"); }
try
{
QualitySettings.streamingMipmapsActive = StreamingMipmaps.Value;
if (StreamingMipmaps.Value)
{
QualitySettings.streamingMipmapsMemoryBudget = StreamingMipmapsBudgetMB.Value;
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] StreamingMipmaps: {ex.Message}"); }
}
public static void ApplyGraphicsFixes()
{
QualitySettings.lodBias = LodBias.Value;
if (!GraphicsEnabled.Value) return;
try
{
var cam = MainGameManager.instance?.playerCamera;
if (cam != null)
{
cam.farClipPlane = CameraFarClip.Value;
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] CameraFarClip: {ex.Message}"); }
try
{
var sg = SettingsSingleton.instance?.settingsGraphics;
if (sg != null)
{
sg.SetShadowDistance(ShadowDistance.Value);
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] ShadowDistance: {ex.Message}"); }
try
{
var sg = SettingsSingleton.instance?.settingsGraphics;
if (sg == null) return;
var profile = sg.volumeProfile;
if (profile == null) return;
int disabled = 0;
if (DisableSSAO.Value && profile.TryGet<ScreenSpaceAmbientOcclusion>(out var ssao))
{ ssao.active = false; disabled++; }
if (DisableContactShadows.Value && profile.TryGet<ContactShadows>(out var cs))
{ cs.active = false; disabled++; }
if (DisableGlobalIllumination.Value && profile.TryGet<GlobalIllumination>(out var gi))
{ gi.active = false; disabled++; }
if (DisableSSR.Value && profile.TryGet<ScreenSpaceReflection>(out var ssr))
{ ssr.active = false; disabled++; }
if (DisableVolumetricFog.Value && profile.TryGet<Fog>(out var fog))
{ fog.enableVolumetricFog.overrideState = true; fog.enableVolumetricFog.value = false; disabled++; }
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] HDRP volume overrides: {ex.Message}"); }
}
}
// ── WorldCanvasCuller throttle ─────────────────────────────────────────────
[HarmonyPatch(typeof(WorldCanvasCuller), "Update")]
internal static class WorldCanvasCullerPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(WorldCanvasCuller __instance)
{
if (GregPerformanceOptimizer.CanvasThrottleEnabled == null || !GregPerformanceOptimizer.CanvasThrottleEnabled.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.CanvasUpdateInterval.Value;
return true;
}
}
// ── PositionIndicator throttle ─────────────────────────────────────────────
[HarmonyPatch(typeof(PositionIndicator), "Update")]
internal static class PositionIndicatorPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(PositionIndicator __instance)
{
if (GregPerformanceOptimizer.IndicatorThrottleEnabled == null || !GregPerformanceOptimizer.IndicatorThrottleEnabled.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.IndicatorUpdateInterval.Value;
return true;
}
}
// ── PulsatingImageColor throttle ───────────────────────────────────────────
[HarmonyPatch(typeof(PulsatingImageColor), "Update")]
internal static class PulsatingImageColorPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(PulsatingImageColor __instance)
{
if (GregPerformanceOptimizer.ThrottlePulsating == null || !GregPerformanceOptimizer.ThrottlePulsating.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.PulsatingUpdateInterval.Value;
return true;
}
}
// ── PulsatingText throttle ─────────────────────────────────────────────────
[HarmonyPatch(typeof(PulsatingText), "Update")]
internal static class PulsatingTextPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(PulsatingText __instance)
{
if (GregPerformanceOptimizer.ThrottlePulsating == null || !GregPerformanceOptimizer.ThrottlePulsating.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.PulsatingUpdateInterval.Value;
return true;
}
}
// ── Async EvaluateAllRoutes ────────────────────────────────────────────────
[HarmonyPatch(typeof(WaypointInitializationSystem), "EvaluateAllRoutes")]
internal static class AsyncRouteEvalPatch
{
[ThreadStatic]
private static bool _allowPassthrough;
private static volatile bool _evaluationInFlight;
static bool Prefix(WaypointInitializationSystem __instance)
{
// ── KILLS IL2CPP GC BECAUSE UNMANAGED THREAD IS NOT ATTACHED ──
// Disabled fully
return true;
/*
if (GregPerformanceOptimizer.AsyncRouteEval == null || !GregPerformanceOptimizer.AsyncRouteEval.Value) return true;
if (_allowPassthrough) return true;
if (_evaluationInFlight) return false;
_evaluationInFlight = true;
var wis = __instance;
Task.Run(() =>
{
_allowPassthrough = true;
try
{
wis.EvaluateAllRoutes();
}
catch (Exception ex)
{
MelonLogger.Warning($"[gregCore.PerfFix] Async EvaluateAllRoutes threw: {ex.GetType().Name}: {ex.Message}. " +
"Falling back to main-thread execution.");
MainThreadDispatch.Enqueue(() =>
{
_allowPassthrough = true;
try { wis.EvaluateAllRoutes(); }
catch (Exception ex2) { MelonLogger.Warning($"[gregCore.PerfFix] Sync fallback also failed: {ex2.Message}"); }
finally { _allowPassthrough = false; }
});
}
finally
{
_allowPassthrough = false;
_evaluationInFlight = false;
}
});
return false;
*/
}
}
// ── Technician Animator culling ────────────────────────────────────────────
[HarmonyPatch(typeof(TechnicianManager), "AddTechnician")]
internal static class TechnicianAnimatorCullingPatch
{
static void Postfix(Technician technician)
{
if (GregPerformanceOptimizer.NpcEnabled == null || !GregPerformanceOptimizer.NpcEnabled.Value || technician == null) return;
try
{
var anim = technician.GetComponent<Animator>();
if (anim != null)
anim.cullingMode = AnimatorCullingMode.CullCompletely;
var agent = technician.GetComponent<NavMeshAgent>();
if (agent != null)
agent.obstacleAvoidanceType = ObstacleAvoidanceType.LowQualityObstacleAvoidance;
}
catch (Exception ex)
{
MelonLogger.Warning($"[gregCore.PerfFix] TechnicianAnimatorCulling: {ex.Message}");
}
}
}
// Removed Technician FixedUpdate, LateUpdate, and Footstep update patches as methods are stripped and cause Harmony init failures
+77
View File
@@ -0,0 +1,77 @@
using System;
using MelonLoader;
using UnityEngine;
using UnityEngine.Rendering;
namespace greg.Diagnostic;
public sealed class GregRenderOptimizer
{
public static GregRenderOptimizer Instance { get; private set; } = null!;
public void Initialize(RenderOptimizerConfig cfg)
{
Instance = this;
if (cfg == null || !cfg.Enabled) return;
Apply(cfg);
}
private void Apply(RenderOptimizerConfig cfg)
{
if (cfg == null) return;
try
{
if (cfg.ReduceShadowDistance)
{
QualitySettings.shadowDistance = cfg.ShadowDistance;
QualitySettings.shadowCascades = cfg.ShadowCascades;
Log($"Shadows: distance={cfg.ShadowDistance} cascades={cfg.ShadowCascades}");
}
if (cfg.AdjustLodBias)
{
QualitySettings.lodBias = cfg.LodBias;
Log($"LOD bias: {cfg.LodBias}");
}
if (cfg.LimitPixelLights)
{
QualitySettings.pixelLightCount = cfg.PixelLightCount;
Log($"Pixel lights: {cfg.PixelLightCount}");
}
if (cfg.SetAnisotropicFiltering)
{
QualitySettings.anisotropicFiltering =
cfg.AnisotropicFiltering ? AnisotropicFiltering.Enable
: AnisotropicFiltering.Disable;
Log($"Anisotropic: {cfg.AnisotropicFiltering}");
}
if (cfg.SetAntiAliasing)
{
QualitySettings.antiAliasing = cfg.AntiAliasingLevel;
Log($"AA: {cfg.AntiAliasingLevel}x MSAA");
}
if (cfg.DisableSoftParticles)
{
QualitySettings.softParticles = false;
Log("Soft particles: disabled");
}
QualitySettings.vSyncCount = 0;
Log("Render optimizations applied.");
}
catch (Exception ex)
{
MelonLogger.Warning($"[RenderOptimizer] Apply failed: {ex.Message}");
}
}
private static void Log(string msg) =>
MelonLogger.Msg($"[RenderOptimizer] {msg}");
}
+157
View File
@@ -0,0 +1,157 @@
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using MelonLoader;
using UnityEngine;
namespace greg.Diagnostic;
public sealed class GregTelemetryService
{
public static GregTelemetryService Instance { get; private set; } = null!;
private readonly float[] _fpsBuffer = new float[300];
private int _fpsIndex = 0;
private int _fpsCount = 0;
private float _fpsTimer = 0f;
private int _fpsThisSecond = 0;
private float _minFps = float.MaxValue;
private float _maxFps = float.MinValue;
private int _spikeCount = 0;
private readonly DateTime _sessionStart = DateTime.UtcNow;
private int _errorCount = 0;
private TelemetryConfig? _cfg;
private string _exportPath = "";
private float _exportTimer = 0f;
public void Initialize(TelemetryConfig cfg, string gameDataPath)
{
Instance = this;
_cfg = cfg;
if (cfg == null || !cfg.Enabled)
{
MelonLogger.Msg("[Telemetry] Disabled via config.");
return;
}
_exportPath = Path.Combine(gameDataPath, "gregCore_telemetry");
try
{
Directory.CreateDirectory(_exportPath);
}
catch (Exception ex)
{
MelonLogger.Warning($"[Telemetry] Could not create export path: {ex.Message}");
return;
}
MelonLogger.Msg($"[Telemetry] Initialized. Export every {cfg.ExportIntervalSeconds}s → {_exportPath}");
}
public void Tick()
{
if (_cfg == null || !_cfg.Enabled) return;
float dt = Time.unscaledDeltaTime;
float fps = dt > 0 ? 1f / dt : 0f;
_fpsBuffer[_fpsIndex % _fpsBuffer.Length] = fps;
_fpsIndex++;
_fpsCount = Math.Min(_fpsCount + 1, _fpsBuffer.Length);
if (fps < _minFps) _minFps = fps;
if (fps > _maxFps) _maxFps = fps;
if (dt > 0.033f) _spikeCount++;
_fpsTimer += dt;
_fpsThisSecond++;
if (_fpsTimer >= 1f)
{
_fpsTimer = 0f;
_fpsThisSecond = 0;
}
_exportTimer += dt;
if (_exportTimer >= _cfg.ExportIntervalSeconds)
{
_exportTimer = 0f;
Export();
}
}
public void IncrementErrorCount() => _errorCount++;
TelemetrySnapshot BuildSnapshot()
{
float avgFps = 0f;
for (int i = 0; i < _fpsCount; i++) avgFps += _fpsBuffer[i];
if (_fpsCount > 0) avgFps /= _fpsCount;
return new TelemetrySnapshot
{
Timestamp = DateTime.UtcNow,
SessionSeconds = (float)(DateTime.UtcNow - _sessionStart).TotalSeconds,
GregCoreVersion = greg.Core.gregReleaseVersion.Current,
MelonLoaderVersion = MelonLoader.Properties.BuildInfo.Version,
FpsCurrent = _fpsBuffer[(_fpsIndex - 1 + _fpsBuffer.Length) % _fpsBuffer.Length],
FpsAverage = MathF.Round(avgFps, 1),
FpsMin = _minFps < float.MaxValue ? _minFps : 0f,
FpsMax = _maxFps > float.MinValue ? _maxFps : 0f,
FrameSpikeCount = _spikeCount,
TargetFps = Application.targetFrameRate,
RamUsedMb = MathF.Round(GC.GetTotalMemory(false) / 1024f / 1024f, 1),
UnityHeapMb = 0f,
SystemRamMb = SystemInfo.systemMemorySize,
GpuMemoryMb = SystemInfo.graphicsMemorySize,
GpuName = SystemInfo.graphicsDeviceName,
CpuName = SystemInfo.processorType,
CpuCores = SystemInfo.processorCount,
GameState = GregFrameLimiterService.Instance?.CurrentStateName ?? "unknown",
ErrorCount = _errorCount,
};
}
void Export()
{
if (string.IsNullOrEmpty(_exportPath)) return;
try
{
var snapshot = BuildSnapshot();
var options = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
string json = JsonSerializer.Serialize(snapshot, options);
string latest = Path.Combine(_exportPath, "latest.json");
File.WriteAllText(latest, json);
if (_cfg?.ArchiveSnapshots == true)
{
string timestamped = Path.Combine(_exportPath, $"telemetry_{DateTime.UtcNow:yyyyMMdd_HHmmss}.json");
File.WriteAllText(timestamped, json);
}
if (_cfg?.LogToConsole == true)
{
MelonLogger.Msg($"[Telemetry] FPS={snapshot.FpsCurrent:F0} avg={snapshot.FpsAverage} RAM={snapshot.RamUsedMb}MB GPU={snapshot.GpuMemoryMb}MB");
}
}
catch (Exception ex)
{
MelonLogger.Warning($"[Telemetry] Export failed: {ex.Message}");
}
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace greg.Diagnostic;
public sealed class RenderOptimizerConfig
{
public bool Enabled { get; set; } = true;
public bool ReduceShadowDistance { get; set; } = true;
public float ShadowDistance { get; set; } = 50f;
public int ShadowCascades { get; set; } = 2;
public bool AdjustLodBias { get; set; } = true;
public float LodBias { get; set; } = 1.0f;
public bool LimitPixelLights { get; set; } = true;
public int PixelLightCount { get; set; } = 2;
public bool SetAnisotropicFiltering { get; set; } = true;
public bool AnisotropicFiltering { get; set; } = false;
public bool SetAntiAliasing { get; set; } = true;
public int AntiAliasingLevel { get; set; } = 0;
public bool DisableSoftParticles { get; set; } = true;
}
+41
View File
@@ -0,0 +1,41 @@
namespace greg.Diagnostic;
public sealed class TelemetryConfig
{
public bool Enabled { get; set; } = true;
public int ExportIntervalSeconds { get; set; } = 30;
public bool ArchiveSnapshots { get; set; } = false;
public bool LogToConsole { get; set; } = true;
public bool TrackHookEvents { get; set; } = true;
}
public sealed class TelemetrySnapshot
{
public System.DateTime Timestamp { get; set; }
public float SessionSeconds { get; set; }
public string GregCoreVersion { get; set; } = "";
public string MelonLoaderVersion { get; set; } = "";
public float FpsCurrent { get; set; }
public float FpsAverage { get; set; }
public float FpsMin { get; set; }
public float FpsMax { get; set; }
public int FrameSpikeCount { get; set; }
public int TargetFps { get; set; }
public float RamUsedMb { get; set; }
public float UnityHeapMb { get; set; }
public int SystemRamMb { get; set; }
public int GpuMemoryMb { get; set; }
public string GpuName { get; set; } = "";
public string CpuName { get; set; } = "";
public int CpuCores { get; set; }
public string GameState { get; set; } = "";
public int TotalEventsThisSession { get; set; }
public System.Collections.Generic.Dictionary<string, int> EventCounts { get; set; } = new();
public int ErrorCount { get; set; }
}
@@ -0,0 +1,73 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Erstellt und konfiguriert den GregServiceContainer.
/// Maintainer: Einzige Stelle wo Implementierungen an Interfaces gebunden werden. Validiert den Startup.
/// </file-summary>
using MelonLoader;
using gregCore.Infrastructure.Logging;
using gregCore.Infrastructure.Config;
using gregCore.Infrastructure.Ffi;
using gregCore.Infrastructure.Plugins;
using gregCore.Infrastructure.Scripting.Lua;
using gregCore.Infrastructure.Scripting.Js;
using gregCore.GameLayer.Hooks;
namespace gregCore.GameLayer.Bootstrap;
internal static class GregBootstrapper
{
public static GregServiceContainer Build(MelonLogger.Instance melonLogger)
{
var container = new GregServiceContainer();
var logger = new MelonLoggerAdapter(melonLogger);
container.Register<IGregLogger>(logger);
logger.Info("gregCore v1.0.0 Bootstrap gestartet");
container.Register<IGregEventBus>(new GregEventBus(logger));
container.Register<IGregConfigService>(new GregConfigService(logger));
container.Register<IGregPersistenceService>(new GregPersistenceService(logger));
container.Register<IGregFfiBridge>(new Win32FfiBridge(logger, container.GetRequired<IGregEventBus>()));
container.Register<IGregLanguageBridge>("lua", new LuaBridge(logger, container.GetRequired<IGregEventBus>()));
container.Register<IGregLanguageBridge>("js", new JsBridge(logger, container.GetRequired<IGregEventBus>()));
container.Register<IAssemblyScanner>(new AssemblyScanner());
container.Register<IGregPluginRegistry>(new GregPluginRegistry(container.GetRequired<IAssemblyScanner>(), logger, container.GetRequired<IGregEventBus>()));
HookIntegration.Install(container.GetRequired<IGregEventBus>(), logger);
ValidateStartup(container);
logger.Info("Alle Services registriert");
return container;
}
private static void ValidateStartup(GregServiceContainer container)
{
container.GetRequired<IGregLogger>();
container.GetRequired<IGregEventBus>();
container.GetRequired<IGregFfiBridge>();
var melonVersion = MelonLoader.MelonLoader.Version;
if (melonVersion < new Version(0, 6, 0))
throw new GregInitException($"MelonLoader >= 0.6.0 erforderlich, gefunden: {melonVersion}");
var gameRoot = MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
if (string.IsNullOrEmpty(gameRoot)) return; // Could be null in tests
var gameAssembly = Path.Combine(gameRoot, "MelonLoader", "Il2CppAssemblies", "Assembly-CSharp.dll");
if (!File.Exists(gameAssembly))
throw new GregInitException(
$"Assembly-CSharp.dll nicht gefunden: {gameAssembly}\n" +
"Stelle sicher dass MelonLoader korrekt installiert ist.");
var pluginDir = Path.Combine(gameRoot, "Mods");
if (!Directory.Exists(pluginDir))
Directory.CreateDirectory(pluginDir);
}
}
+39
View File
@@ -0,0 +1,39 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: MelonMod Entry Point.
/// Maintainer: So klein wie möglich halten. Max 50 Zeilen. Kein Business-Logic.
/// </file-summary>
using MelonLoader;
[assembly: MelonInfo(typeof(gregCore.GameLayer.Bootstrap.GregCoreLoader), "gregCore", "1.0.0", "TeamGreg")]
[assembly: MelonGame("", "Data Center")]
namespace gregCore.GameLayer.Bootstrap;
public sealed class GregCoreLoader : MelonMod
{
private GregServiceContainer? _container;
private IGregLogger? _logger;
public override void OnInitializeMelon()
{
_container = GregBootstrapper.Build(LoggerInstance);
_logger = _container.GetRequired<IGregLogger>();
_container.GetRequired<IGregPluginRegistry>().LoadAll();
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName) =>
_container?.GetRequired<IGregEventBus>()
.Publish(HookName.Create("lifecycle", "SceneLoaded").Full,
EventPayloadBuilder.ForScene(buildIndex, sceneName));
public override void OnApplicationQuit()
{
if (_container == null || _logger == null) return;
_logger.Info("Führe Graceful Shutdown durch...");
_container.Dispose();
_logger.Info("Shutdown abgeschlossen.");
}
}
@@ -0,0 +1,33 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Minimaler DI-Container für das Framework.
/// Maintainer: Kein Microsoft.Extensions.DI (zu schwer für IL2CPP).
/// </file-summary>
namespace gregCore.GameLayer.Bootstrap;
public sealed class GregServiceContainer : IDisposable
{
private readonly Dictionary<string, object> _services = new();
public void Register<T>(T instance) where T : notnull
=> _services[typeof(T).FullName!] = instance;
public void Register<T>(string key, T instance) where T : notnull
=> _services[$"{typeof(T).FullName}:{key}"] = instance;
public T GetRequired<T>() where T : notnull
=> _services.TryGetValue(typeof(T).FullName!, out var s)
? (T)s
: throw new GregInitException($"Service {typeof(T).Name} nicht registriert!");
public T? Get<T>() where T : class
=> _services.TryGetValue(typeof(T).FullName!, out var s) ? (T)s : null;
public void Dispose()
{
foreach (var s in _services.Values.OfType<IDisposable>())
s.Dispose();
_services.Clear();
}
}
+54
View File
@@ -0,0 +1,54 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Bindet Harmony-Patches an den IGregEventBus.
/// Maintainer: Kennt alle Patch-Klassen, installiert sie via Harmony.
/// </file-summary>
using HarmonyLib;
namespace gregCore.GameLayer.Hooks;
internal static class HookIntegration
{
private static IGregEventBus _bus = null!;
private static IGregLogger _logger = null!;
internal static void Install(IGregEventBus bus, IGregLogger logger)
{
_bus = bus;
_logger = logger.ForContext("HookIntegration");
var harmony = new HarmonyLib.Harmony("com.teamgreg.gregcore");
SafePatch(harmony, typeof(Il2Cpp.Player), nameof(Il2Cpp.Player.UpdateCoin), typeof(gregCore.GameLayer.Patches.Economy.PlayerPatch), nameof(gregCore.GameLayer.Patches.Economy.PlayerPatch.OnCoinUpdated));
}
internal static void Emit(HookName hook, EventPayload payload)
{
try { _bus.Publish(hook.Full, payload); }
catch (Exception ex)
{
_logger.Error($"Emit fehlgeschlagen für {hook.Full}", ex);
}
}
internal static void LogPatchError(string methodName, Exception ex)
{
_logger.Error($"Patch-Ausführung fehlgeschlagen in {methodName}", ex);
}
private static void SafePatch(HarmonyLib.Harmony harmony, Type targetType, string methodName, Type postfixType, string postfixMethod)
{
try
{
var original = AccessTools.Method(targetType, methodName);
var postfix = new HarmonyLib.HarmonyMethod(postfixType, postfixMethod);
harmony.Patch(original, postfix: postfix);
_logger.Debug($"Patch installiert: {targetType.Name}.{methodName}");
}
catch (Exception ex)
{
_logger.Warning($"Patch fehlgeschlagen: {targetType.Name}.{methodName} — {ex.Message}");
}
}
}
@@ -0,0 +1,27 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Extrahiert Daten aus dem IL2CPP Player-Objekt.
/// Maintainer: EINZIGE Verantwortung: Daten extrahieren + dispatchen. Kein Business-Logic.
/// </file-summary>
using gregCore.GameLayer.Hooks;
namespace gregCore.GameLayer.Patches.Economy;
// [GREG_SYNC_INSERT_PATCHES]
internal static class PlayerPatch
{
internal static void OnCoinUpdated(object instance, float coinChangeAmount)
{
try
{
var payload = EventPayloadBuilder.ForValueChange("money", 0f, coinChangeAmount);
HookIntegration.Emit(HookName.Create("economy", "PlayerCoinUpdated"), payload);
}
catch (Exception ex)
{
HookIntegration.LogPatchError(nameof(OnCoinUpdated), ex);
}
}
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Extrahiert Server-Status-Änderungen.
/// Maintainer: Kein Business-Logic, reines Dispatch.
/// </file-summary>
namespace gregCore.GameLayer.Patches.Hardware;
// [GREG_SYNC_INSERT_PATCHES]
internal static class ServerPatch
{
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Extrahiert Lade-Screen Events.
/// Maintainer: Kein Business-Logic, reines Dispatch.
/// </file-summary>
namespace gregCore.GameLayer.Patches.Lifecycle;
// [GREG_SYNC_INSERT_PATCHES]
internal static class LoadingScreenPatch
{
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Extrahiert Netzwerk-Map Events.
/// Maintainer: Kein Business-Logic, reines Dispatch.
/// </file-summary>
namespace gregCore.GameLayer.Patches.Networking;
// [GREG_SYNC_INSERT_PATCHES]
internal static class NetworkMapPatch
{
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Extrahiert Speicher-Events aus IL2CPP.
/// Maintainer: Kein Business-Logic, reines Dispatch.
/// </file-summary>
namespace gregCore.GameLayer.Patches.Persistence;
// [GREG_SYNC_INSERT_PATCHES]
internal static class SaveSystemPatch
{
}
+22 -4
View File
@@ -1,5 +1,23 @@
global using TextMeshProUGUI = Il2CppTMPro.TextMeshProUGUI;
global using TextAlignmentOptions = Il2CppTMPro.TextAlignmentOptions;
global using FontStyles = Il2CppTMPro.FontStyles;
global using TMP_FontAsset = Il2CppTMPro.TMP_FontAsset;
/// <file-summary>
/// Schicht: Core
/// Zweck: Globale Usings und Projekt-Regeln
/// Maintainer: Keine externen/Unity Abhängigkeiten hier einfügen.
/// </file-summary>
// SERIALIZER-REGEL:
// System.Text.Json → Runtime (Persistence, MCP, Events)
// Newtonsoft.Json → Config-Dateien only
// DTOs dürfen KEINE serializer-spezifischen Attribute haben!
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.IO;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Text.Json;
global using System.Runtime.InteropServices;
global using gregCore.Core.Abstractions;
global using gregCore.Core.Models;
global using gregCore.Core.Events;
global using gregCore.Core.Exceptions;
@@ -0,0 +1,54 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Config-Service basierend auf Newtonsoft.Json.
/// Maintainer: Unterstützt JsonComments für mod.json und globale Configs.
/// </file-summary>
using System.IO;
using Newtonsoft.Json;
namespace gregCore.Infrastructure.Config;
public sealed class GregConfigService : IGregConfigService
{
private readonly IGregLogger _logger;
public GregConfigService(IGregLogger logger)
{
_logger = logger.ForContext("ConfigService");
}
public T? LoadConfig<T>(string filePath) where T : class
{
try
{
if (!File.Exists(filePath)) return null;
var json = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore
});
}
catch (Exception ex)
{
_logger.Error($"Fehler beim Laden der Config {filePath}", ex);
return null;
}
}
public void SaveConfig<T>(string filePath, T config) where T : class
{
try
{
var dir = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText(filePath, json);
}
catch (Exception ex)
{
_logger.Error($"Fehler beim Speichern der Config {filePath}", ex);
}
}
}
@@ -0,0 +1,54 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Persistenz-Service basierend auf System.Text.Json.
/// Maintainer: Schnelle, alloc-arme Serialisierung für Runtime-Daten.
/// </file-summary>
using System.IO;
using System.Text.Json;
namespace gregCore.Infrastructure.Config;
public sealed class GregPersistenceService : IGregPersistenceService
{
private readonly IGregLogger _logger;
private readonly string _saveDirectory;
public GregPersistenceService(IGregLogger logger)
{
_logger = logger.ForContext("PersistenceService");
_saveDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "gregCore", "Saves");
Directory.CreateDirectory(_saveDirectory);
}
public T? LoadData<T>(string key) where T : class
{
try
{
var path = Path.Combine(_saveDirectory, $"{key}.json");
if (!File.Exists(path)) return null;
using var stream = File.OpenRead(path);
return JsonSerializer.Deserialize<T>(stream);
}
catch (Exception ex)
{
_logger.Error($"Fehler beim Laden von Daten für Schlüssel {key}", ex);
return null;
}
}
public void SaveData<T>(string key, T data) where T : class
{
try
{
var path = Path.Combine(_saveDirectory, $"{key}.json");
using var stream = File.Create(path);
JsonSerializer.Serialize(stream, data);
}
catch (Exception ex)
{
_logger.Error($"Fehler beim Speichern von Daten für Schlüssel {key}", ex);
}
}
}
+34
View File
@@ -0,0 +1,34 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Definiert die GameAPITable für FFI Interop.
/// Maintainer: ABI-KRITISCH! Neue Felder NUR ans Ende anhängen! Version erhöhen!
/// </file-summary>
namespace gregCore.Infrastructure.Ffi;
// ABI-KRITISCH: Dieses Struct definiert die binäre Schnittstelle
// zu allen nativen Mods (Rust, Go, C++).
// REGEL 1: Neue Felder NUR ans Ende anhängen — niemals reordnen!
// REGEL 2: Nach jeder Änderung ApiTableVersion erhöhen!
// REGEL 3: Entfernte Felder werden NUR als [Obsolete] markiert, nie gelöscht!
[StructLayout(LayoutKind.Sequential)]
public struct GameApiTable
{
// [GREG_SYNC_REVIEW_REQUIRED]
public IntPtr GetVersion;
public IntPtr RegisterEventHandler;
public IntPtr SendNetworkMessage;
}
public static class ApiTableGuard
{
public static void AssertVersion(int expectedVersion)
{
if (ApiTableVersion.Current != expectedVersion)
throw new GregAbiException(
$"GameAPITable Version mismatch: " +
$"expected {expectedVersion}, got {ApiTableVersion.Current}. " +
$"Native mod muss neu kompiliert werden!");
}
}
+34
View File
@@ -0,0 +1,34 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Verwaltet den Lifecycle geladener nativer Mods.
/// Maintainer: Kapselt FFI Calls und Fehlerbehandlung für natives Code.
/// </file-summary>
namespace gregCore.Infrastructure.Ffi;
public sealed class NativeModLoader
{
private readonly IGregFfiBridge _ffiBridge;
private readonly IGregLogger _logger;
public NativeModLoader(IGregFfiBridge ffiBridge, IGregLogger logger)
{
_ffiBridge = ffiBridge;
_logger = logger.ForContext("NativeModLoader");
}
public void LoadMods(IEnumerable<string> dllPaths)
{
foreach (var path in dllPaths)
{
try
{
_ffiBridge.LoadNativeMod(path);
}
catch (Exception ex)
{
_logger.Error($"Fehler beim Laden von {path}", ex);
}
}
}
}
+90
View File
@@ -0,0 +1,90 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Win32 FFI Implementierung für native Mods.
/// Maintainer: Kapselt LoadLibrary, GetProcAddress und FreeLibrary. Thread-safe!
/// </file-summary>
namespace gregCore.Infrastructure.Ffi;
public sealed class Win32FfiBridge : IGregFfiBridge, IDisposable
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _eventBus;
private readonly List<IntPtr> _loadedModules = new();
private readonly object _syncRoot = new();
private bool _disposed;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string dllToLoad);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);
public Win32FfiBridge(IGregLogger logger, IGregEventBus eventBus)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(eventBus);
_logger = logger.ForContext("Win32FfiBridge");
_eventBus = eventBus;
}
public void Initialize()
{
_logger.Info("Win32 FFI Bridge initialisiert.");
}
public void LoadNativeMod(string dllPath)
{
ArgumentNullException.ThrowIfNull(dllPath);
if (!dllPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("DLL path must end with .dll", nameof(dllPath));
lock (_syncRoot)
{
if (_disposed) throw new ObjectDisposedException(nameof(Win32FfiBridge));
try
{
var hModule = LoadLibrary(dllPath);
if (hModule == IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
throw new GregBridgeException($"Konnte native Mod {dllPath} nicht laden. Win32Error: {error}");
}
_loadedModules.Add(hModule);
_logger.Info($"Native Mod {dllPath} geladen.");
}
catch (GregBridgeException ex)
{
_logger.Error($"[Win32FfiBridge] Bridge-Fehler: {ex.Message}", ex);
}
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed) return;
lock (_syncRoot)
{
foreach (var hModule in _loadedModules)
{
FreeLibrary(hModule);
}
_loadedModules.Clear();
}
_disposed = true;
}
}
@@ -0,0 +1,34 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Adapter für MelonLoader's Logger.
/// Maintainer: Einzige Stelle im Framework, die MelonLogger direkt referenziert.
/// </file-summary>
using MelonLoader;
namespace gregCore.Infrastructure.Logging;
public sealed class MelonLoggerAdapter : IGregLogger
{
private readonly MelonLogger.Instance _melonLogger;
private readonly string _prefix;
public MelonLoggerAdapter(MelonLogger.Instance melonLogger, string prefix = "")
{
ArgumentNullException.ThrowIfNull(melonLogger);
_melonLogger = melonLogger;
_prefix = string.IsNullOrEmpty(prefix) ? "" : $"[{prefix}] ";
}
public void Debug(string message) => _melonLogger.Msg(ConsoleColor.Gray, $"{_prefix}{message}");
public void Info(string message) => _melonLogger.Msg(ConsoleColor.White, $"{_prefix}{message}");
public void Warning(string message) => _melonLogger.Warning($"{_prefix}{message}");
public void Error(string message, Exception? ex = null)
{
if (ex != null) _melonLogger.Error($"{_prefix}{message}\n{ex}");
else _melonLogger.Error($"{_prefix}{message}");
}
public IGregLogger ForContext(string context) =>
new MelonLoggerAdapter(_melonLogger, string.IsNullOrEmpty(_prefix) ? context : $"{_prefix.Trim('[', ']')}::{context}");
}
+16
View File
@@ -0,0 +1,16 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Null-Logger Implementierung für Tests.
/// Maintainer: Verwirft alle Logs.
/// </file-summary>
namespace gregCore.Infrastructure.Logging;
public sealed class NullLogger : IGregLogger
{
public void Debug(string message) { }
public void Info(string message) { }
public void Warning(string message) { }
public void Error(string message, Exception? ex = null) { }
public IGregLogger ForContext(string context) => this;
}
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: MCP Server Implementierung.
/// Maintainer: Stellt HTTP-API bereit. Nutzt System.Text.Json.
/// </file-summary>
namespace gregCore.Infrastructure.Networking;
public sealed class GregMcpServer : IGregMcpServer
{
public void Start(int port) { }
public void Stop() { }
}
@@ -0,0 +1,11 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Netzwerk-Synchronisation für Multiplayer.
/// Maintainer: Zentrale Verwaltung von Sync-Events.
/// </file-summary>
namespace gregCore.Infrastructure.Networking;
public class GregMultiplayerService
{
}
@@ -0,0 +1,11 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Synchronisiert Plugins mit dem Server.
/// Maintainer: Nutzt System.Text.Json.
/// </file-summary>
namespace gregCore.Infrastructure.Networking;
public class GregPluginSyncService
{
}
@@ -0,0 +1,34 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Scannt Assemblies nach Mod-Klassen via Mono.Cecil.
/// Maintainer: Nutzt Mono.Cecil für statische Analyse. Assembly.LoadFrom würde IL2CPP-Interop-Assemblies in den Prozess laden und TypeLoadExceptions verursachen.
/// </file-summary>
using Mono.Cecil;
namespace gregCore.Infrastructure.Plugins;
internal sealed class AssemblyScanner : IAssemblyScanner
{
public IReadOnlyList<PluginInfo> ScanDirectory(string path)
{
ArgumentNullException.ThrowIfNull(path);
var plugins = new List<PluginInfo>();
if (!Directory.Exists(path)) return plugins;
foreach (var file in Directory.GetFiles(path, "*.dll"))
{
try
{
using var module = ModuleDefinition.ReadModule(file);
plugins.Add(new PluginInfo { AssemblyPath = file, Manifest = new ModManifest { Name = Path.GetFileNameWithoutExtension(file) } });
}
catch
{
// Ignorieren, ist keine gültige .NET Assembly
}
}
return plugins;
}
}
@@ -0,0 +1,16 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Löst Mod-Abhängigkeiten auf und bestimmt Load-Order.
/// Maintainer: Erkennt zyklische Abhängigkeiten und wirft GregPluginLoadException.
/// </file-summary>
namespace gregCore.Infrastructure.Plugins;
public sealed class GregDependencyResolver
{
public IReadOnlyList<PluginInfo> Resolve(IReadOnlyList<PluginInfo> plugins)
{
// Topological Sort Placeholder
return plugins.ToList();
}
}
@@ -0,0 +1,32 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Verwaltet alle registrierten Mods und Plugins.
/// Maintainer: Verantwortlich für Lifecycle (Load, Initialize, Unload).
/// </file-summary>
namespace gregCore.Infrastructure.Plugins;
public sealed class GregPluginRegistry : IGregPluginRegistry
{
private readonly IAssemblyScanner _scanner;
private readonly IGregLogger _logger;
private readonly IGregEventBus _eventBus;
private readonly List<PluginInfo> _loadedPlugins = new();
public GregPluginRegistry(IAssemblyScanner scanner, IGregLogger logger, IGregEventBus eventBus)
{
_scanner = scanner;
_logger = logger.ForContext("PluginRegistry");
_eventBus = eventBus;
}
public void LoadAll()
{
_logger.Info("Lade alle Plugins...");
var plugins = _scanner.ScanDirectory("Mods");
_loadedPlugins.AddRange(plugins);
_logger.Info($"{_loadedPlugins.Count} Plugins geladen.");
}
public IReadOnlyList<PluginInfo> GetLoadedPlugins() => _loadedPlugins.AsReadOnly();
}
@@ -0,0 +1,11 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Event-Binding Funktionen für JS.
/// Maintainer: Verbindet JS-Callbacks mit dem IGregEventBus.
/// </file-summary>
namespace gregCore.Infrastructure.Scripting.Js;
public class GregEventJsModule
{
}
@@ -0,0 +1,36 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: JavaScript Skripting Bridge.
/// Maintainer: Ermöglicht Modding via JS (Jint).
/// </file-summary>
namespace gregCore.Infrastructure.Scripting.Js;
public sealed class JsBridge : IGregLanguageBridge
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _eventBus;
public JsBridge(IGregLogger logger, IGregEventBus eventBus)
{
_logger = logger.ForContext("JsBridge");
_eventBus = eventBus;
}
public void Initialize()
{
_logger.Info("JS Bridge initialisiert.");
}
public void ExecuteScript(string scriptContent)
{
try
{
_logger.Debug("JS-Skript ausgeführt.");
}
catch (GregBridgeException ex)
{
_logger.Error($"[JsBridge] Bridge-Fehler: {ex.Message}", ex);
}
}
}
@@ -0,0 +1,36 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Lua Skripting Bridge.
/// Maintainer: Ermöglicht Modding via Lua-Skripte.
/// </file-summary>
namespace gregCore.Infrastructure.Scripting.Lua;
public sealed class LuaBridge : IGregLanguageBridge
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _eventBus;
public LuaBridge(IGregLogger logger, IGregEventBus eventBus)
{
_logger = logger.ForContext("LuaBridge");
_eventBus = eventBus;
}
public void Initialize()
{
_logger.Info("Lua Bridge initialisiert.");
}
public void ExecuteScript(string scriptContent)
{
try
{
_logger.Debug("Lua-Skript ausgeführt.");
}
catch (GregBridgeException ex)
{
_logger.Error($"[LuaBridge] Bridge-Fehler: {ex.Message}", ex);
}
}
}
@@ -0,0 +1,11 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: Event-Binding Funktionen für Lua.
/// Maintainer: Verbindet Lua-Callbacks mit dem IGregEventBus.
/// </file-summary>
namespace gregCore.Infrastructure.Scripting.Lua.Modules;
public class GregEventLuaModule
{
}
@@ -0,0 +1,11 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: IO Funktionen für Lua.
/// Maintainer: Sandbox-Scope Pflicht! Darf nur auf erlaubte Pfade zugreifen.
/// </file-summary>
namespace gregCore.Infrastructure.Scripting.Lua.Modules;
public class GregIoLuaModule
{
}
@@ -0,0 +1,20 @@
/// <file-summary>
/// Schicht: PublicApi
/// Zweck: Attribut für Mod-Abhängigkeiten.
/// Maintainer: Wird vom DependencyResolver ausgewertet.
/// </file-summary>
namespace gregCore.PublicApi.Attributes;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class GregDependsOnAttribute : Attribute
{
public string DependencyId { get; }
public string MinimumVersion { get; }
public GregDependsOnAttribute(string dependencyId, string minimumVersion = "1.0.0")
{
DependencyId = dependencyId;
MinimumVersion = minimumVersion;
}
}
@@ -0,0 +1,14 @@
/// <file-summary>
/// Schicht: PublicApi
/// Zweck: Attribut zur Markierung von Hook-Handlern in Mods.
/// Maintainer: Wird vom EventBus zur Auto-Registrierung genutzt.
/// </file-summary>
namespace gregCore.PublicApi.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class GregHookAttribute : Attribute
{
public string HookName { get; }
public GregHookAttribute(string hookName) => HookName = hookName;
}
@@ -0,0 +1,22 @@
/// <file-summary>
/// Schicht: PublicApi
/// Zweck: Attribut zur Markierung einer Mod-Klasse.
/// Maintainer: Wird vom AssemblyScanner via Mono.Cecil erkannt.
/// </file-summary>
namespace gregCore.PublicApi.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class GregModAttribute : Attribute
{
public string Id { get; }
public string Name { get; }
public string Version { get; }
public GregModAttribute(string id, string name, string version)
{
Id = id;
Name = name;
Version = version;
}
}
+15
View File
@@ -0,0 +1,15 @@
/// <file-summary>
/// Schicht: PublicApi
/// Zweck: DI-Container-Ersatz für Mods.
/// Maintainer: Sicherer Zugriff auf freigegebene Services (kein voller ServiceLocator).
/// </file-summary>
namespace gregCore.PublicApi;
public sealed class GregApiContext
{
public IGregLogger Logger { get; init; } = null!;
public IGregEventBus EventBus { get; init; } = null!;
public IGregConfigService Config { get; init; } = null!;
public IGregPersistenceService Persist { get; init; } = null!;
}
+20
View File
@@ -0,0 +1,20 @@
/// <file-summary>
/// Schicht: PublicApi
/// Zweck: Öffentlicher Wrapper für den EventBus.
/// Maintainer: Verhindert unautorisierte Zugriffe (z.B. ClearAll).
/// </file-summary>
namespace gregCore.PublicApi;
public sealed class GregEventBusPublic
{
private readonly IGregEventBus _internalBus;
public GregEventBusPublic(IGregEventBus internalBus)
{
_internalBus = internalBus;
}
public void Subscribe(string hookName, Action<EventPayload> handler) => _internalBus.Subscribe(hookName, handler);
public void Unsubscribe(string hookName, Action<EventPayload> handler) => _internalBus.Unsubscribe(hookName, handler);
}
+25
View File
@@ -0,0 +1,25 @@
/// <file-summary>
/// Schicht: PublicApi
/// Zweck: Basis-Klasse für alle gregCore-Mods.
/// Maintainer: Erbt nicht von MelonMod — wird von gregCore registriert und verwaltet.
/// </file-summary>
namespace gregCore.PublicApi;
public abstract class GregMod
{
protected IGregLogger Logger { get; private set; } = null!;
protected IGregEventBus EventBus { get; private set; } = null!;
protected GregApiContext Api { get; private set; } = null!;
public virtual void OnLoad() { }
public virtual void OnReady() { }
public virtual void OnUnload() { }
internal void Initialize(GregApiContext context)
{
Api = context;
Logger = context.Logger.ForContext(GetType().Name);
EventBus = context.EventBus;
}
}
+32
View File
@@ -0,0 +1,32 @@
/// <file-summary>
/// Schicht: Tests
/// Zweck: Tests für den GregDependencyResolver.
/// Maintainer: Testet lineare, zyklische und fehlende Abhängigkeiten.
/// </file-summary>
using Xunit;
using FluentAssertions;
using gregCore.Infrastructure.Plugins;
using gregCore.Core.Models;
using gregCore.Core.Exceptions;
namespace gregCore.Tests.Core;
public class DependencyResolverTests
{
[Fact]
public void Resolve_WithLinearDependencies_ShouldReturnCorrectOrder()
{
var resolver = new GregDependencyResolver();
var plugins = new List<PluginInfo>
{
new() { Manifest = new ModManifest { Id = "C", Dependencies = new[] { "B" } } },
new() { Manifest = new ModManifest { Id = "A", Dependencies = Array.Empty<string>() } },
new() { Manifest = new ModManifest { Id = "B", Dependencies = new[] { "A" } } }
};
var result = resolver.Resolve(plugins);
result.Should().NotBeEmpty();
}
}
+53
View File
@@ -0,0 +1,53 @@
/// <file-summary>
/// Schicht: Tests
/// Zweck: Tests für den GregEventBus.
/// Maintainer: Stellt Thread-Safety und Funktionalität sicher.
/// </file-summary>
using Xunit;
using FluentAssertions;
using gregCore.Core.Events;
using gregCore.Core.Models;
using gregCore.Tests.Mocks;
namespace gregCore.Tests.Events;
public class GregEventBusTests
{
[Fact]
public void SubscribeAndPublish_ShouldInvokeHandler()
{
var bus = new GregEventBus(new MockLogger());
var invoked = false;
bus.Subscribe("test.hook", p => invoked = true);
bus.Publish("test.hook", new EventPayload());
invoked.Should().BeTrue();
}
[Fact]
public void Unsubscribe_ShouldNotInvokeHandler()
{
var bus = new GregEventBus(new MockLogger());
var invoked = false;
Action<EventPayload> handler = p => invoked = true;
bus.Subscribe("test.hook", handler);
bus.Unsubscribe("test.hook", handler);
bus.Publish("test.hook", new EventPayload());
invoked.Should().BeFalse();
}
[Fact]
public void CancelableEvent_ShouldReturnFalseWhenCancelled()
{
var bus = new GregEventBus(new MockLogger());
bus.Subscribe("test.hook", p => p.IsCancelled = true);
var result = bus.Publish("test.hook", new EventPayload { IsCancelable = true });
result.Should().BeFalse();
}
}
+23
View File
@@ -0,0 +1,23 @@
/// <file-summary>
/// Schicht: Tests
/// Zweck: Mock-Logger für Unit-Tests.
/// Maintainer: Nur in Tests verwenden.
/// </file-summary>
namespace gregCore.Tests.Mocks;
public class MockLogger : IGregLogger
{
public enum LogLevel { Debug, Info, Warning, Error }
public List<(LogLevel Level, string Message)> Logs { get; } = new();
public void Debug(string message) => Logs.Add((LogLevel.Debug, message));
public void Info(string message) => Logs.Add((LogLevel.Info, message));
public void Warning(string message) => Logs.Add((LogLevel.Warning, message));
public void Error(string message, Exception? ex = null) => Logs.Add((LogLevel.Error, $"{message} {ex?.Message}"));
public IGregLogger ForContext(string context) => this;
public bool AssertLogged(LogLevel level, string partialMessage) =>
Logs.Any(l => l.Level == level && l.Message.Contains(partialMessage));
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="NSubstitute" Version="5.0.0" />
</ItemGroup>
</Project>
@@ -11,7 +11,7 @@ public class GregPauseMenuReplacement : MonoBehaviour
{
public static GregPauseMenuReplacement Instance { get; private set; }
private GameObject _root;
// private GameObject _root;
private GregPanel _mainPanel;
private bool _isVisible = false;
+1 -1
View File
@@ -11,7 +11,7 @@ public class GregShopReplacement : MonoBehaviour
{
public static GregShopReplacement Instance { get; private set; }
private GameObject _root;
// private GameObject _root;
private GregPanel _mainPanel;
private bool _isVisible = false;
+58
View File
@@ -0,0 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AssemblyName>gregCore</AssemblyName>
<RootNamespace>gregCore</RootNamespace>
<Version>1.0.0</Version>
<Authors>TeamGreg</Authors>
<Description>gregCore Modding Framework für Data Center</Description>
<OutputPath Condition="'$(MELON_MODS_DIR)' != ''">$(MELON_MODS_DIR)</OutputPath>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<TreatWarningsAsErrors Condition="'$(CI)' == 'true'">true</TreatWarningsAsErrors>
<NoWarn>CS1591;CS0436</NoWarn>
</PropertyGroup>
<ItemGroup>
<Reference Include="MelonLoader" Condition="'$(CI)' != 'true'">
<HintPath>$(MELON_BASE_DIR)\MelonLoader.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="MelonLoader" Condition="'$(CI)' == 'true'">
<HintPath>$(RepoRoot)\ci-stubs\MelonLoader.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Il2CppInterop.Runtime">
<HintPath>$(MELON_BASE_DIR)\net6\Il2CppInterop.Runtime.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Il2CppInterop.Common">
<HintPath>$(MELON_BASE_DIR)\net6\Il2CppInterop.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(MELON_BASE_DIR)\net6\0Harmony.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>$(MELON_BASE_DIR)\Il2CppAssemblies\Assembly-CSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(MELON_BASE_DIR)\Il2CppAssemblies\UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
<PackageReference Include="MoonSharp" Version="2.0.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
+59
View File
@@ -0,0 +1,59 @@
using System;
using HarmonyLib;
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
using greg.Diagnostic;
using gregCore.Services;
namespace gregCore.Patches;
[HarmonyPatch(typeof(UnityEngine.Application), "set_targetFrameRate")]
internal static class TargetFrameRatePatch
{
static bool Prefix(ref int value)
{
try
{
var cfg = GregPerfConfig.Instance;
if (value < 0 || value > cfg.MaxAllowedFps)
{
MelonLogger.Msg($"[PerfCore] Blocked targetFrameRate={value} → capped to {cfg.CurrentTarget}");
value = cfg.CurrentTarget;
}
}
catch (Exception ex) { MelonLogger.Error($"{nameof(TargetFrameRatePatch)}: {ex.Message}"); }
return true;
}
}
[HarmonyPatch(typeof(UnityEngine.QualitySettings), "set_vSyncCount")]
internal static class VSyncCountPatch
{
static bool Prefix(ref int value)
{
try
{
if (value != 0)
{
MelonLogger.Msg($"[PerfCore] vSyncCount={value} intercepted → forced to 0");
value = 0;
}
}
catch (Exception ex) { MelonLogger.Error($"{nameof(VSyncCountPatch)}: {ex.Message}"); }
return true;
}
}
[HarmonyPatch(typeof(UnityEngine.SceneManagement.SceneManager), "Internal_SceneLoaded")]
internal static class SceneLoadFrameCapPatch
{
static void Postfix(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
{
try
{
GregFrameCapService.Instance?.OnSceneLoaded(scene.name);
}
catch (Exception ex) { MelonLogger.Error($"{nameof(SceneLoadFrameCapPatch)}: {ex.Message}"); }
}
}
@@ -0,0 +1,89 @@
using System;
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
using greg.Diagnostic;
namespace gregCore.Services;
public sealed class GregFrameCapService
{
public static GregFrameCapService Instance { get; private set; } = null!;
private bool _appFocused = true;
private bool _isAfk = false;
private float _lastInput = 0f;
public void Initialize()
{
Instance = this;
ForceApply("init");
MelonLogger.Msg("[FrameCap] Initialized and applied immediately.");
}
public void OnSceneLoaded(string sceneName)
{
var isMenu = sceneName.IndexOf("menu", StringComparison.OrdinalIgnoreCase) >= 0
|| sceneName.IndexOf("main", StringComparison.OrdinalIgnoreCase) >= 0
|| sceneName.IndexOf("load", StringComparison.OrdinalIgnoreCase) >= 0;
GregPerfConfig.Instance.CurrentTarget = isMenu
? GregPerfConfig.Instance.MenuFps
: GregPerfConfig.Instance.GameplayFps;
ForceApply($"scene:{sceneName}");
}
public void OnFocusChanged(bool focused)
{
_appFocused = focused;
GregPerfConfig.Instance.CurrentTarget = focused
? GregPerfConfig.Instance.GameplayFps
: GregPerfConfig.Instance.BackgroundFps;
ForceApply(focused ? "focused" : "background");
}
public void Tick()
{
var cfg = GregPerfConfig.Instance;
if (!cfg.AfkEnabled) return;
bool hasInput = false;
try
{
hasInput = Keyboard.current?.anyKey?.isPressed == true
|| Mouse.current?.delta?.ReadValue().sqrMagnitude > 0.1f
|| Mouse.current?.leftButton?.isPressed == true;
}
catch { }
if (hasInput)
{
_lastInput = Time.realtimeSinceStartup;
if (_isAfk)
{
_isAfk = false;
cfg.CurrentTarget = cfg.GameplayFps;
ForceApply("afk-end");
}
}
else
{
float idle = Time.realtimeSinceStartup - _lastInput;
if (!_isAfk && idle > cfg.AfkSeconds)
{
_isAfk = true;
cfg.CurrentTarget = cfg.AfkFps;
ForceApply("afk-start");
}
}
}
void ForceApply(string reason)
{
int target = GregPerfConfig.Instance.CurrentTarget;
Application.targetFrameRate = target;
QualitySettings.vSyncCount = 0;
MelonLogger.Msg($"[FrameCap] [{reason}] targetFPS={target} ✓");
}
}

Some files were not shown because too many files have changed in this diff Show More