chore: sync gregCore workspace updates
This commit is contained in:
@@ -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.
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.
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
<Project>
|
||||
</Project>
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace gregAssetExporter
|
||||
{
|
||||
internal static class MainCiBuildMarker
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user