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/),
|
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).
|
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
|
## [v1.0.0.7] - 2026-04-12
|
||||||
### Added
|
### Added
|
||||||
- **gregUI**: Complete UI manipulation layer for UGUI (`src/UI/`).
|
- **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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<!-- ── Build ──────────────────────────────────────────────────── -->
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<AssemblyName>gregCore</AssemblyName>
|
<Nullable>enable</Nullable>
|
||||||
<Version>1.0.0.30-pre</Version>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<AssemblyName>gregCore</AssemblyName>
|
||||||
<NoWarn>CS1701;CS1702</NoWarn>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
<Import Project="../shared/gregFramework.props" />
|
<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>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jint" Version="4.1.0" />
|
<PackageReference Include="Jint" Version="4.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -20,4 +98,4 @@
|
|||||||
<EmbeddedResource Remove="tests\**" />
|
<EmbeddedResource Remove="tests\**" />
|
||||||
<None Remove="tests\**" />
|
<None Remove="tests\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
+1
-1
@@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gregCore", "gregCore.csproj
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gregFramework", "gregFramework", "{9A0CCAB9-4303-13B4-2371-F1B97FF5B728}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gregFramework", "gregFramework", "{9A0CCAB9-4303-13B4-2371-F1B97FF5B728}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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'
|
$ErrorActionPreference = 'Stop'
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
$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'
|
$harmonyPatches = Join-Path $repoRoot 'gregCore\framework\ModLoader\HarmonyPatches.cs'
|
||||||
$outJsonRoot = Join-Path $repoRoot 'greg_hooks.json'
|
$outJsonRoot = Join-Path $repoRoot 'greg_hooks.json'
|
||||||
$outJsonFramework = Join-Path $repoRoot 'gregCore\framework\greg_hooks.json'
|
$outJsonFramework = Join-Path $repoRoot 'gregCore\framework\greg_hooks.json'
|
||||||
$outHooksDir = Join-Path $repoRoot 'gregCore\framework\harmony'
|
$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
|
New-Item -ItemType Directory -Force -Path $outHooksDir | Out-Null
|
||||||
|
|
||||||
function Get-HarmonyExclusions([string]$path) {
|
function Get-HarmonyExclusions([string]$path) {
|
||||||
$set = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
$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
|
$text = Get-Content $path -Raw
|
||||||
foreach ($m in [regex]::Matches($text, 'typeof\((\w+)\)\s*,\s*nameof\(\w+\.(\w+)\)')) {
|
foreach ($m in [regex]::Matches($text, 'typeof\((\w+)\)\s*,\s*nameof\(\w+\.(\w+)\)')) {
|
||||||
[void]$set.Add("$($m.Groups[1].Value)|$($m.Groups[2].Value)")
|
[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+)"')) {
|
foreach ($m in [regex]::Matches($text, 'typeof\((\w+)\)\s*,\s*"(\w+)"')) {
|
||||||
[void]$set.Add("$($m.Groups[1].Value)|$($m.Groups[2].Value)")
|
[void]$set.Add("$($m.Groups[1].Value)|$($m.Groups[2].Value)")
|
||||||
}
|
}
|
||||||
return $set
|
return ,$set
|
||||||
}
|
}
|
||||||
|
|
||||||
$excluded = Get-HarmonyExclusions $harmonyPatches
|
$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).
|
# Curated game surface for stable compile-time Harmony class generation.
|
||||||
$gameHookClasses = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
$harmonyEmitClasses = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||||
@(
|
@(
|
||||||
'Player', 'PlayerManager', 'PlayerHit', 'PlayerData',
|
'Player', 'PlayerManager', 'PlayerHit', 'PlayerData',
|
||||||
'Server', 'MainGameManager', 'ComputerShop', 'HRSystem', 'SaveSystem', 'CustomerBase',
|
'Server', 'MainGameManager', 'ComputerShop', 'HRSystem', 'SaveSystem', 'CustomerBase',
|
||||||
'CablePositions', 'CableLink', 'Rack', 'NetworkMap', 'BalanceSheet', 'MainMenu',
|
'CablePositions', 'CableLink', 'Rack', 'NetworkMap', 'BalanceSheet', 'MainMenu',
|
||||||
'TimeController', 'TechnicianManager', 'Technician', 'Objectives',
|
'TimeController', 'TechnicianManager', 'Technician', 'Objectives',
|
||||||
'PacketSpawnerSystem', 'NetworkSwitch', 'SFPModule', 'SFPBox', 'PatchPanel'
|
'PacketSpawnerSystem', 'NetworkSwitch', 'SFPModule', 'SFPBox', 'PatchPanel'
|
||||||
) | ForEach-Object { [void]$gameHookClasses.Add($_) }
|
) | ForEach-Object { [void]$harmonyEmitClasses.Add($_) }
|
||||||
|
|
||||||
function Test-SkipTypeName([string]$n) {
|
function Test-SkipTypeName([string]$n) {
|
||||||
if ($n -match 'd__\d+$') { return $true }
|
if ($n -match 'd__\d+$') { return $true }
|
||||||
@@ -120,6 +123,34 @@ function Test-SkipClass([string]$className, [string]$baseClause) {
|
|||||||
return $false
|
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) {
|
function Test-SkipInteropSignature([string]$returnType, [string]$argList) {
|
||||||
$blob = "$returnType $argList"
|
$blob = "$returnType $argList"
|
||||||
if ($blob -match 'EntityCommandBuffer|SystemState|BlobArray|ComponentLookup|BufferLookup') { return $true }
|
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) {
|
function Split-MethodLine([string]$line, [ref]$isStatic) {
|
||||||
$isStatic.Value = $false
|
$isStatic.Value = $false
|
||||||
$m = [regex]::Match($line, '^\t{2}public unsafe static (?<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 ($m.Success) { $isStatic.Value = $true }
|
|
||||||
else {
|
|
||||||
$m = [regex]::Match($line, '^\t{2}public unsafe (?<sig>.+?)\s*\((?<args>[^\)]*)\)\s*$')
|
|
||||||
}
|
|
||||||
if (-not $m.Success) { return $null }
|
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()
|
$sig = $m.Groups['sig'].Value.Trim()
|
||||||
if ($sig.StartsWith('static ')) { $sig = $sig.Substring(7).Trim() }
|
|
||||||
$argList = $m.Groups['args'].Value.Trim()
|
$argList = $m.Groups['args'].Value.Trim()
|
||||||
$idx = $sig.LastIndexOf(' ')
|
$idx = $sig.LastIndexOf(' ')
|
||||||
if ($idx -lt 0) { return $null }
|
if ($idx -lt 0) { return $null }
|
||||||
@@ -235,22 +265,22 @@ function Build-EmitAnonymousBody([string]$className, [bool]$isStatic) {
|
|||||||
|
|
||||||
$hooks = [System.Collections.Generic.List[object]]::new()
|
$hooks = [System.Collections.Generic.List[object]]::new()
|
||||||
$byDomain = @{}
|
$byDomain = @{}
|
||||||
|
$scanDirectories = Get-SourceDirectories $assemblyRoot
|
||||||
|
$sourceFiles = $scanDirectories | ForEach-Object { Get-ChildItem -Path $_ -Filter '*.cs' -File -Recurse }
|
||||||
|
|
||||||
Get-ChildItem $il2cppDir -Filter '*.cs' -File | ForEach-Object {
|
foreach ($sourceFile in $sourceFiles) {
|
||||||
$lines = Get-Content $_.FullName
|
$assemblyName = Get-AssemblyNameFromPath $sourceFile.FullName $assemblyRoot
|
||||||
$overloadFirst = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
$lines = Get-Content $sourceFile.FullName
|
||||||
|
$harmonyOverloadFirst = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||||
$currentClass = $null
|
$currentClass = $null
|
||||||
$currentBase = ''
|
$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) {
|
foreach ($line in $lines) {
|
||||||
$cm = $classRegex.Match($line)
|
$cm = $classRegex.Match($line)
|
||||||
if ($cm.Success) {
|
if ($cm.Success) {
|
||||||
$cn = $cm.Groups['name'].Value
|
$cn = $cm.Groups['name'].Value
|
||||||
$cb = $cm.Groups['base'].Value.Trim()
|
$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
|
$currentClass = $cn
|
||||||
$currentBase = $cb
|
$currentBase = $cb
|
||||||
continue
|
continue
|
||||||
@@ -265,18 +295,9 @@ Get-ChildItem $il2cppDir -Filter '*.cs' -File | ForEach-Object {
|
|||||||
$mn = $parsed.Name
|
$mn = $parsed.Name
|
||||||
$argList = $parsed.ArgList
|
$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
|
$domain = Get-GregDomain $currentClass
|
||||||
$action = Get-SemanticAction $currentClass $mn
|
$action = Get-SemanticAction $currentClass $mn
|
||||||
$strategy = Get-HookStrategy $mn $ret
|
$strategy = 'Postfix'
|
||||||
if ($strategy -eq 'None') { continue }
|
|
||||||
|
|
||||||
$hookName = "greg.$($domain.ToUpperInvariant()).$action"
|
$hookName = "greg.$($domain.ToUpperInvariant()).$action"
|
||||||
$patchTarget = Get-Il2CppPatchSignature $currentClass $mn $argList
|
$patchTarget = Get-Il2CppPatchSignature $currentClass $mn $argList
|
||||||
@@ -314,29 +335,41 @@ Get-ChildItem $il2cppDir -Filter '*.cs' -File | ForEach-Object {
|
|||||||
name = $hookName
|
name = $hookName
|
||||||
legacy = $null
|
legacy = $null
|
||||||
patchTarget = $patchTarget
|
patchTarget = $patchTarget
|
||||||
|
assembly = $assemblyName
|
||||||
strategy = $(if ($strategy -eq 'PrefixPostfix') { 'Prefix+Postfix' } else { 'Postfix' })
|
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
|
payloadSchema = $payload
|
||||||
})
|
})
|
||||||
|
|
||||||
if (-not $byDomain.ContainsKey($domain)) { $byDomain[$domain] = [System.Collections.Generic.List[object]]::new() }
|
$harmonySignatureCompatible = $line -match '^\s+public unsafe(?:\s+static)?\s+'
|
||||||
[void]$byDomain[$domain].Add([ordered]@{
|
$allowHarmonyEmit = $harmonyEmitClasses.Contains($currentClass) -and -not (Test-SkipTypeName $currentClass) -and -not (Test-SkipClass $currentClass $currentBase) -and $harmonySignatureCompatible
|
||||||
Class = $currentClass
|
if ($allowHarmonyEmit -and (Test-ShouldEmitHook $mn $ret) -and -not (Test-SkipInteropSignature $ret $argList) -and -not $excluded.Contains("$currentClass|$mn")) {
|
||||||
Method = $mn
|
$harmonyOvKey = "$currentClass|$mn"
|
||||||
Ret = $ret
|
if ($harmonyOverloadFirst.Contains($harmonyOvKey)) { continue }
|
||||||
Args = $argList
|
[void]$harmonyOverloadFirst.Add($harmonyOvKey)
|
||||||
Action = $action
|
|
||||||
Strategy = $strategy
|
$harmonyStrategy = Get-HookStrategy $mn $ret
|
||||||
HookName = $hookName
|
if ($harmonyStrategy -eq 'None') { continue }
|
||||||
IsStatic = $staticFlag
|
|
||||||
})
|
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 = @{}
|
$seen = @{}
|
||||||
$unique = [System.Collections.Generic.List[object]]::new()
|
$unique = [System.Collections.Generic.List[object]]::new()
|
||||||
foreach ($h in $hooks) {
|
foreach ($h in $hooks) {
|
||||||
$k = [string]$h.patchTarget
|
$k = "$($h.assembly)|$([string]$h.patchTarget)"
|
||||||
if ($seen.ContainsKey($k)) { continue }
|
if ($seen.ContainsKey($k)) { continue }
|
||||||
$seen[$k] = $true
|
$seen[$k] = $true
|
||||||
[void]$unique.Add($h)
|
[void]$unique.Add($h)
|
||||||
@@ -345,7 +378,7 @@ foreach ($h in $hooks) {
|
|||||||
$doc = [ordered]@{
|
$doc = [ordered]@{
|
||||||
version = 2
|
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.'
|
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 = @()
|
legacyPrefixes = @()
|
||||||
hooks = $unique
|
hooks = $unique
|
||||||
}
|
}
|
||||||
@@ -370,7 +403,8 @@ foreach ($kv in $byDomain.GetEnumerator()) {
|
|||||||
$sb = [System.Text.StringBuilder]::new()
|
$sb = [System.Text.StringBuilder]::new()
|
||||||
[void]$sb.AppendLine('using System;')
|
[void]$sb.AppendLine('using System;')
|
||||||
[void]$sb.AppendLine('using HarmonyLib;')
|
[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 Il2Cpp;')
|
||||||
[void]$sb.AppendLine('using Il2CppSystem.Collections.Generic;')
|
[void]$sb.AppendLine('using Il2CppSystem.Collections.Generic;')
|
||||||
[void]$sb.AppendLine('using Il2CppInterop.Runtime.InteropTypes.Arrays;')
|
[void]$sb.AppendLine('using Il2CppInterop.Runtime.InteropTypes.Arrays;')
|
||||||
@@ -420,8 +454,8 @@ foreach ($kv in $byDomain.GetEnumerator()) {
|
|||||||
[void]$sb.AppendLine(' {')
|
[void]$sb.AppendLine(' {')
|
||||||
[void]$sb.AppendLine(' try')
|
[void]$sb.AppendLine(' try')
|
||||||
[void]$sb.AppendLine(' {')
|
[void]$sb.AppendLine(' {')
|
||||||
[void]$sb.AppendLine(' GregEventDispatcher.Emit(')
|
[void]$sb.AppendLine(' gregEventDispatcher.Emit(')
|
||||||
[void]$sb.AppendLine(" GregHookName.Create(GregDomain.$gregDomain, `"$action`"),")
|
[void]$sb.AppendLine(" gregHookName.Create(GregDomain.$gregDomain, `"$action`"),")
|
||||||
[void]$sb.AppendLine(' new')
|
[void]$sb.AppendLine(' new')
|
||||||
[void]$sb.AppendLine(' {')
|
[void]$sb.AppendLine(' {')
|
||||||
[void]$sb.AppendLine($emitBody)
|
[void]$sb.AppendLine($emitBody)
|
||||||
@@ -461,3 +495,4 @@ internal static class GregPowerHooks
|
|||||||
Write-Host "Wrote stub $powerFile"
|
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;
|
/// <file-summary>
|
||||||
global using TextAlignmentOptions = Il2CppTMPro.TextAlignmentOptions;
|
/// Schicht: Core
|
||||||
global using FontStyles = Il2CppTMPro.FontStyles;
|
/// Zweck: Globale Usings und Projekt-Regeln
|
||||||
global using TMP_FontAsset = Il2CppTMPro.TMP_FontAsset;
|
/// 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; }
|
public static GregPauseMenuReplacement Instance { get; private set; }
|
||||||
|
|
||||||
private GameObject _root;
|
// private GameObject _root;
|
||||||
private GregPanel _mainPanel;
|
private GregPanel _mainPanel;
|
||||||
private bool _isVisible = false;
|
private bool _isVisible = false;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class GregShopReplacement : MonoBehaviour
|
|||||||
{
|
{
|
||||||
public static GregShopReplacement Instance { get; private set; }
|
public static GregShopReplacement Instance { get; private set; }
|
||||||
|
|
||||||
private GameObject _root;
|
// private GameObject _root;
|
||||||
private GregPanel _mainPanel;
|
private GregPanel _mainPanel;
|
||||||
private bool _isVisible = false;
|
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