feat: Implement core performance and automation modules
Sponsor Tier Sync / sync (push) Failing after 34s
gregCore CI / build (push) Has been cancelled

- Added RepairEngine for device repair automation.
- Introduced GregFrameRateLimiter to manage frame rates based on performance profiles.
- Implemented GregMemoryPressureHandler to handle memory pressure events and optimize garbage collection.
- Created GregOperationQueue for managing and throttling operations with priority.
- Developed GregPerformanceGovernor to oversee performance management and resource monitoring.
- Added GregRequestThrottler to limit concurrent operations and requests.
- Implemented GregResourceMonitor to track system resource usage and publish metrics.
- Introduced IAssemblyScanner interface for plugin assembly scanning.
- Added various public API modules including GregAutomationModule, GregEconomyModule, GregFacilityModule, GregNetworkModule, GregNpcModule, GregPerformanceModule, GregPlayerModule, GregSaveModule, GregServerModule, GregTimeModule, and GregUIModule for enhanced game functionality.
- Created event handling and resource snapshot types for better event management and performance tracking.
This commit is contained in:
Marvin
2026-04-20 04:25:54 +02:00
parent 9e6f25c54a
commit abbd440bf1
292 changed files with 1291 additions and 27761 deletions
Binary file not shown.
+4 -3
View File
@@ -15,10 +15,10 @@ New-Item -ItemType Directory -Path $publishDir | Out-Null
# 2. Build
Write-Host "Building gregCore (Release)..." -ForegroundColor Yellow
dotnet build "$repoRoot\gregCore.csproj" -c Release
dotnet build "$repoRoot\src\gregCore.csproj" -c Release -o "$buildDir"
# 3. Check output
$dllPath = "$repoRoot\bin\Release\net6.0\gregCore.dll"
$dllPath = "$buildDir\gregCore.dll"
if (-not (Test-Path $dllPath)) {
Write-Error "Build failed: gregCore.dll not found at $dllPath"
}
@@ -32,11 +32,12 @@ Write-Host "Packaging version $version into $zipName..." -ForegroundColor Yellow
# Copy files to a temporary folder for zipping
$tmpDir = "$publishDir\tmp"
if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir }
New-Item -ItemType Directory -Path $tmpDir | Out-Null
Copy-Item $dllPath -Destination $tmpDir
Copy-Item "$repoRoot\README.md" -Destination $tmpDir
Copy-Item "$repoRoot\CHANGELOG.md" -Destination $tmpDir
Copy-Item "$repoRoot\gregFramework\greg_hooks.json" -Destination $tmpDir
Copy-Item "$repoRoot\assets\greg_hooks.json" -Destination $tmpDir
# Create zip
Compress-Archive -Path "$tmpDir\*" -DestinationPath $zipPath -Force
-4
View File
@@ -1,4 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("gregCore.Tests")]
@@ -0,0 +1,13 @@
/// <file-summary>
/// Schicht: Core
/// Zweck: Interface für den Performance Governor.
/// Maintainer: Wird vom EventBus für Throttling genutzt.
/// </file-summary>
namespace gregCore.Core.Abstractions;
public interface IGregPerformanceGovernor
{
bool CanDispatchEvent();
void OnUpdate();
}
@@ -1,13 +1,8 @@
/// <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;
}
void Set<T>(string key, T value) where T : notnull;
T Get<T>(string key, T defaultValue = default!) where T : notnull;
bool Has(string key);
void Delete(string key);
}
+24
View File
@@ -12,6 +12,8 @@ public sealed class GregEventBus : IGregEventBus, IDisposable
private readonly Dictionary<string, List<Action<EventPayload>>> _handlers = new();
private readonly Dictionary<string, Action<EventPayload>[]> _cachedHandlers = new();
private readonly ReaderWriterLockSlim _rwLock = new();
private readonly System.Collections.Concurrent.ConcurrentQueue<(string hookName, EventPayload payload)> _deferredEvents = new();
private IGregPerformanceGovernor? _governor;
private bool _isDirty = true;
private bool _disposed;
@@ -21,6 +23,8 @@ public sealed class GregEventBus : IGregEventBus, IDisposable
_logger = logger.ForContext("EventBus");
}
public void SetGovernor(IGregPerformanceGovernor governor) => _governor = governor;
public void Subscribe(string hookName, Action<EventPayload> handler)
{
ArgumentNullException.ThrowIfNull(hookName);
@@ -66,7 +70,27 @@ public sealed class GregEventBus : IGregEventBus, IDisposable
public bool Publish(string hookName, EventPayload payload)
{
ArgumentNullException.ThrowIfNull(hookName);
if (_governor != null && !_governor.CanDispatchEvent())
{
_deferredEvents.Enqueue((hookName, payload));
return false;
}
return PublishDirect(hookName, payload);
}
internal void FlushDeferredEvents()
{
while (_deferredEvents.TryDequeue(out var ev))
{
if (_governor != null && !_governor.CanDispatchEvent()) break;
PublishDirect(ev.hookName, ev.payload);
}
}
private bool PublishDirect(string hookName, EventPayload payload)
{
Action<EventPayload>[]? handlersToInvoke = null;
_rwLock.EnterReadLock();
+9
View File
@@ -0,0 +1,9 @@
namespace gregCore.Core.Models;
public sealed record AutomationProgress
{
public string CurrentTask { get; init; } = string.Empty;
public int Current { get; init; }
public int Total { get; init; }
public double PercentDone => Total == 0 ? 0 : (Current / (double)Total) * 100;
}
+18
View File
@@ -0,0 +1,18 @@
namespace gregCore.Core.Models;
public sealed record AutomationResult
{
public bool IsSuccess { get; init; }
public string? Error { get; init; }
public string? Detail { get; init; }
public int ItemsProcessed { get; init; }
public static AutomationResult Success(int items = 1, string? detail = null)
=> new AutomationResult { IsSuccess = true, ItemsProcessed = items, Detail = detail };
public static AutomationResult Failure(string error, string? detail = null)
=> new AutomationResult { IsSuccess = false, Error = error, Detail = detail };
public static AutomationResult Partial(int done, int total, string? detail = null)
=> new AutomationResult { IsSuccess = done > 0, ItemsProcessed = done, Detail = detail ?? $"{done}/{total} verarbeitet" };
}
+35
View File
@@ -0,0 +1,35 @@
namespace gregCore.Core.Models;
public enum AutomationTask
{
ProcessDeliveryZone,
PickupItem,
PlaceItemInRack,
PlaceItemOnFloor,
LayCable,
RemoveCable,
LayPowerCable,
LayNetworkCable,
BuildRack,
InstallServer,
InstallSwitch,
InstallPatchPanel,
PowerOnDevice,
PowerOffDevice,
AssignIP,
AssignVlan,
ConfigureRouting,
SetupNetworkSegment,
RepairDevice,
RepairAll,
ReplaceHardware,
CheckDeviceHealth,
AssignEmployee,
UnassignEmployee,
HireEmployee,
FireEmployee,
UnlockRoom,
PlaceFurniture,
RemoveFurniture,
ExpandRack
}
+12
View File
@@ -0,0 +1,12 @@
namespace gregCore.Core.Models;
public enum CableType
{
Cat5,
Cat6,
Cat6A,
Fiber,
PowerAC,
PowerDC,
Console
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace gregCore.Core.Models;
// [GREG_SYNC_INSERT_DTOS]
[StructLayout(LayoutKind.Sequential)]
public record struct EventPayload
public record EventPayload
{
public string HookName { get; init; }
public DateTime OccurredAtUtc { get; init; }
+3 -4
View File
@@ -11,8 +11,7 @@ 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 string Full => $"greg.{Domain}.{Event}";
public static HookName Parse(string full)
{
@@ -20,7 +19,7 @@ public readonly record struct HookName
var parts = full.Split('.');
if (parts.Length >= 3 && parts[0] == "greg")
{
return new HookName { Domain = parts[1], Event = parts[2], _full = full };
return new HookName { Domain = parts[1], Event = parts[2] };
}
throw new ArgumentException($"Invalid HookName format: {full}");
}
@@ -29,7 +28,7 @@ public readonly record struct HookName
{
ArgumentNullException.ThrowIfNull(domain);
ArgumentNullException.ThrowIfNull(eventName);
return new HookName { Domain = domain, Event = eventName, _full = $"greg.{domain}.{eventName}" };
return new HookName { Domain = domain, Event = eventName };
}
public override string ToString() => Full;
+11
View File
@@ -0,0 +1,11 @@
namespace gregCore.Core.Models;
public sealed record NetworkSegmentConfig
{
public string[] Devices { get; init; } = Array.Empty<string>();
public string? Switch { get; init; }
public int VlanId { get; init; }
public string? SubnetBase { get; init; }
public string? Gateway { get; init; }
public string? DnsServer { get; init; }
}
+10
View File
@@ -0,0 +1,10 @@
namespace gregCore.Core.Models;
public enum OperationPriority
{
Critical = 0,
High = 1,
Normal = 2,
Low = 3,
Background = 4
}
+43
View File
@@ -0,0 +1,43 @@
namespace gregCore.Core.Models;
public sealed record PerformanceProfile
{
public int TargetFps { get; init; } = 60;
public int UnfocusedFps { get; init; } = 15;
public int LoadingFps { get; init; } = 30;
public int MaxConcurrentOps { get; init; } = 3;
public int MaxConcurrentRequests { get; init; } = 4;
public int MaxEventsPerFrame { get; init; } = 20;
public int RamWarningMb { get; init; } = 3072;
public int RamCriticalMb { get; init; } = 4096;
public int GcIntervalSeconds { get; init; } = 30;
public bool EnableVSync { get; init; } = true;
public int QualityLevel { get; init; } = 2;
public float ShadowDistance { get; init; } = 50f;
public int TextureResolution { get; init; } = 0;
public static PerformanceProfile Balanced => new PerformanceProfile();
public static PerformanceProfile HighPerformance => new PerformanceProfile
{
TargetFps = 144,
UnfocusedFps = 30,
MaxConcurrentOps = 8,
EnableVSync = false,
QualityLevel = 4,
GcIntervalSeconds = 60,
};
public static PerformanceProfile LowEnd => new PerformanceProfile
{
TargetFps = 30,
UnfocusedFps = 10,
MaxConcurrentOps = 1,
MaxEventsPerFrame = 10,
RamWarningMb = 2048,
RamCriticalMb = 3072,
EnableVSync = true,
QualityLevel = 0,
ShadowDistance = 20f,
TextureResolution = 2,
GcIntervalSeconds = 15,
};
}
+9
View File
@@ -0,0 +1,9 @@
namespace gregCore.Core.Models;
public sealed record PerformanceStats
{
public PerformanceProfile Profile { get; init; } = new PerformanceProfile();
public ResourceSnapshot Resources { get; init; } = new ResourceSnapshot();
public ThrottleMetrics Throttle { get; init; } = new ThrottleMetrics();
public int QueueDepth { get; init; }
}
+17
View File
@@ -0,0 +1,17 @@
namespace gregCore.Core.Models;
public sealed record RackBuildConfig
{
public int RackId { get; init; }
public RackSlotConfig[] Servers { get; init; } = Array.Empty<RackSlotConfig>();
public string? SwitchId { get; init; }
public string? PduId { get; init; }
public bool AutoPowerOn { get; init; } = true;
}
public sealed record RackSlotConfig
{
public string ServerId { get; init; } = string.Empty;
public int Slot { get; init; }
public string? IpAddress { get; init; }
}
+24
View File
@@ -0,0 +1,24 @@
namespace gregCore.Core.Models;
public sealed record ResourceSnapshot
{
public DateTime TimestampUtc { get; init; } = DateTime.UtcNow;
// .NET Managed Heap & System RAM
public int RamUsedMb { get; init; }
public int PrivateMemoryMb { get; init; }
public int GcTotalMemoryMb { get; init; }
// Unity Engine Memory (Native)
public int UnityAllocatedMb { get; init; }
public int UnityReservedMb { get; init; }
public int UnityUnusedMb { get; init; }
// System Metrics
public int GcGen0Collections { get; init; }
public int GcGen1Collections { get; init; }
public int GcGen2Collections { get; init; }
public int ThreadCount { get; init; }
public string Summary => $"RAM:{RamUsedMb}MB Unity:{UnityAllocatedMb}MB GC:{GcTotalMemoryMb}MB Threads:{ThreadCount}";
}
+10
View File
@@ -0,0 +1,10 @@
namespace gregCore.Core.Models;
public sealed record ThrottleMetrics
{
public int TotalQueued { get; init; }
public int TotalCompleted { get; init; }
public int CurrentActive { get; init; }
public int MaxConcurrent { get; init; }
public int QueueDepth { get; init; }
}
+21
View File
@@ -0,0 +1,21 @@
// This file provides the necessary attributes for the compiler to support
// nullable reference types and other modern C# features in environments
// where the core library might not have them (like IL2CPP mixing).
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte flag) => NullableFlags = new[] { flag };
public NullableAttribute(byte[] flags) => NullableFlags = flags;
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte flag) => Flag = flag;
}
}
-24
View File
@@ -1,24 +0,0 @@
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; }
}
-70
View File
@@ -1,70 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Il2Cpp;
using MelonLoader;
using UnityEngine;
using greg.Sdk.Services;
namespace greg.Diagnostic;
public static class GregDiagnosticTools
{
public static void RunFullWorldAudit()
{
MelonLogger.Msg("\n[gregCore] --- WORLD DIAGNOSTIC AUDIT (Save Bug Analysis) ---");
var switches = Object.FindObjectsOfType<NetworkSwitch>(true);
int totalSwitches = switches.Length;
int brokenSwitches = switches.Count(s => s != null && s.isBroken);
MelonLogger.Msg($"[Audit] Scanned {totalSwitches} switches. {brokenSwitches} marked as BROKEN.");
var ghostDefects = new List<string>();
foreach (var sw in switches)
{
if (sw == null) continue;
if (sw.isBroken)
{
// Logic Audit: Why is it broken?
bool hasFlow = GregResetSwitchService.EvaluateDeepStatus(sw) == GregDeepFlowStatus.Active;
// If it has flow or seems logically sound but is "broken", it's a ghost defect.
if (hasFlow)
{
ghostDefects.Add($"[GhostDefect] Switch {sw.switchId} (Label: {sw.label}) is BROKEN but has ACTIVE FLOW.");
}
// Check for Server/Rack components
var rack = sw.GetComponentInParent<Rack>();
if (rack != null)
{
// If the rack is broken but all servers inside seem fine
var servers = rack.GetComponentsInChildren<Server>();
bool anyServerBroken = servers.Any(serv => serv.isBroken);
if (!anyServerBroken && !hasFlow && sw.isOn)
{
ghostDefects.Add($"[GhostDefect] Rack {rack.name} / Switch {sw.switchId} is BROKEN but no servers inside are broken.");
}
}
}
}
if (ghostDefects.Count > 0)
{
MelonLogger.Warning($"[Audit] DETECTED {ghostDefects.Count} GHOSH DEFECTS!");
foreach (var defect in ghostDefects)
{
MelonLogger.Warning($" {defect}");
}
}
else
{
MelonLogger.Msg("[Audit] No ghost defects detected in current scene.");
}
MelonLogger.Msg("[gregCore] --- AUDIT COMPLETE ---\n");
}
}
-150
View File
@@ -1,150 +0,0 @@
using System;
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
namespace greg.Diagnostic;
public sealed class GregFrameLimiterService
{
public static GregFrameLimiterService Instance { get; private set; } = null!;
public enum GameState { Unknown, Menu, Gameplay, Loading }
public string CurrentStateName => _currentState.ToString();
private GameState _currentState = GameState.Unknown;
private bool _isFocused = true;
private bool _isMinimized = false;
private bool _isAfk = false;
private float _lastInputTime = 0f;
private int _frame = 0;
private FrameLimiterConfig? _cfg;
public void Initialize(FrameLimiterConfig cfg)
{
Instance = this;
_cfg = cfg;
if (cfg == null || !cfg.Enabled)
{
MelonLogger.Msg("[FrameLimiter] Disabled via config.");
return;
}
SetState(GameState.Menu);
MelonLogger.Msg("[FrameLimiter] Initialized. Menu limit active.");
}
public void SetState(GameState state)
{
if (_currentState == state) return;
_currentState = state;
ApplyCurrentLimit();
}
public void OnFocusChanged(bool focused)
{
_isFocused = focused;
ApplyCurrentLimit();
}
public void OnMinimizeChanged(bool minimized)
{
_isMinimized = minimized;
ApplyCurrentLimit();
}
public void Tick()
{
if (_cfg == null || !_cfg.Enabled) return;
bool anyInput = false;
try
{
if (Keyboard.current != null)
{
anyInput = Keyboard.current.anyKey.isPressed;
}
if (!anyInput && Mouse.current != null)
{
anyInput = Mouse.current.delta.ReadValue().sqrMagnitude > 0.01f;
}
}
catch { }
if (anyInput)
{
_lastInputTime = Time.realtimeSinceStartup;
if (_isAfk)
{
_isAfk = false;
ApplyCurrentLimit();
MelonLogger.Msg("[FrameLimiter] AFK ended — restoring limit.");
}
}
else if (!_isAfk
&& _cfg?.Afk?.Enabled == true
&& _currentState == GameState.Gameplay
&& (Time.realtimeSinceStartup - _lastInputTime) > _cfg.Afk.AfkAfterSeconds)
{
_isAfk = true;
ApplyCurrentLimit();
MelonLogger.Msg("[FrameLimiter] AFK detected — reducing FPS.");
}
}
private void ApplyCurrentLimit()
{
if (_cfg == null || !_cfg.Enabled) return;
int targetFps;
int vSync;
string reason;
if (_isMinimized)
{
targetFps = _cfg.Minimized.TargetFps;
vSync = _cfg.Minimized.VSync;
reason = "minimized";
}
else if (!_isFocused)
{
targetFps = _cfg.Background.TargetFps;
vSync = _cfg.Background.VSync;
reason = "background";
}
else if (_isAfk)
{
targetFps = _cfg.Afk.TargetFps;
vSync = _cfg.Afk.VSync;
reason = "afk";
}
else
{
(targetFps, vSync, reason) = _currentState switch
{
GameState.Menu => (_cfg.Menu.TargetFps, _cfg.Menu.VSync, "menu"),
GameState.Loading => (_cfg.Menu.TargetFps, _cfg.Menu.VSync, "loading"),
GameState.Gameplay => (_cfg.Gameplay.TargetFps, _cfg.Gameplay.VSync, "gameplay"),
_ => (_cfg.Menu.TargetFps, _cfg.Menu.VSync, "unknown"),
};
}
try
{
if (Application.targetFrameRate == targetFps
&& QualitySettings.vSyncCount == vSync)
return;
Application.targetFrameRate = targetFps;
QualitySettings.vSyncCount = vSync;
MelonLogger.Msg($"[FrameLimiter] {reason} → targetFPS={targetFps} vSync={vSync}");
}
catch (Exception ex)
{
MelonLogger.Warning($"[FrameLimiter] ApplyCurrentLimit failed: {ex.Message}");
}
}
}
-46
View File
@@ -1,46 +0,0 @@
using System.Text.Json.Serialization;
namespace greg.Diagnostic;
public sealed class GregPerfConfig
{
public static GregPerfConfig Instance { get; set; } = new();
public bool FrameCapEnabled { get; set; } = true;
public int MenuFps { get; set; } = 30;
public int GameplayFps { get; set; } = 144;
public int BackgroundFps { get; set; } = 20;
public int AfkFps { get; set; } = 15;
public bool AfkEnabled { get; set; } = true;
public float AfkSeconds { get; set; } = 60f;
public int MaxAllowedFps { get; set; } = 240;
[JsonIgnore]
public int CurrentTarget { get; set; } = 30;
public bool ThreadingEnabled { get; set; } = true;
public int PhysicalCores { get; set; } = 0;
public bool GcOptEnabled { get; set; } = true;
public int GcTriggerMb { get; set; } = 256;
public bool IncrementalGc { get; set; } = true;
public bool RenderOptEnabled { get; set; } = true;
public bool ReduceShadows { get; set; } = true;
public float ShadowDistanceM { get; set; } = 50f;
public int ShadowCascades { get; set; } = 2;
public bool AggressiveLod { get; set; } = true;
public float LodBias { get; set; } = 1.0f;
public int MaxLodLevel { get; set; } = 0;
public bool LimitPixelLights { get; set; } = true;
public int MaxPixelLights { get; set; } = 2;
public bool ReduceTextureQuality { get; set; } = false;
public int TextureMipMapLimit { get; set; } = 0;
public bool DisableSoftParticles { get; set; } = true;
public bool DisableHeavyPostProcessing { get; set; } = true;
public bool DisableMotionBlur { get; set; } = true;
public bool DisableBloom { get; set; } = false;
public bool DisableDoF { get; set; } = true;
public bool DisableAO { get; set; } = false;
public bool DisableSSR { get; set; } = true;
}
-48
View File
@@ -1,48 +0,0 @@
using MelonLoader;
using UnityEngine;
using UnityEngine.InputSystem;
namespace greg.Diagnostic;
public sealed class GregPerformanceHud : MelonMod
{
private bool _visible = false;
private string _displayText = "";
private float _updateTimer = 0f;
public override void OnUpdate()
{
try
{
if (Keyboard.current?.f9Key?.wasPressedThisFrame == true)
_visible = !_visible;
}
catch { }
if (!_visible) return;
_updateTimer += Time.unscaledDeltaTime;
if (_updateTimer < 1f) return;
_updateTimer = 0f;
float currentFps = Time.unscaledDeltaTime > 0 ? 1f / Time.unscaledDeltaTime : 0f;
float targetFps = Application.targetFrameRate;
string state = GregFrameLimiterService.Instance?.CurrentStateName ?? "?";
long ramMb = System.GC.GetTotalMemory(false) / 1024 / 1024;
int gpuMb = SystemInfo.graphicsMemorySize;
_displayText =
$"gregCore Performance\n" +
$"FPS: {targetFps}/{currentFps:F0}\n" +
$"State: {state}\n" +
$"RAM: {ramMb}MB\n" +
$"GPU: {gpuMb}MB\n" +
$"[F9] hide";
}
public override void OnGUI()
{
if (!_visible) return;
GUI.Box(new Rect(10, 10, 200, 160), _displayText);
}
}
-461
View File
@@ -1,461 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using HarmonyLib;
using Il2Cpp;
using MelonLoader;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
namespace greg.Diagnostic;
// ── Main-thread dispatch queue ─────────────────────────────────────────────
internal static class MainThreadDispatch
{
private static readonly ConcurrentQueue<Action> _queue = new();
public static void Enqueue(Action action) => _queue.Enqueue(action);
public static void Drain()
{
while (_queue.TryDequeue(out var action))
{
try { action(); }
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] MainThreadDispatch error: {ex.Message}"); }
}
}
}
public static class GregPerformanceOptimizer
{
// ── Preferences ───────────────────────────────────────────────────────
public static MelonPreferences_Entry<bool> CanvasThrottleEnabled;
public static MelonPreferences_Entry<float> CanvasUpdateInterval;
public static MelonPreferences_Entry<bool> IndicatorThrottleEnabled;
public static MelonPreferences_Entry<float> IndicatorUpdateInterval;
public static MelonPreferences_Entry<bool> ThrottlePulsating;
public static MelonPreferences_Entry<float> PulsatingUpdateInterval;
public static MelonPreferences_Entry<float> RouteEvalCooldown;
public static MelonPreferences_Entry<bool> AsyncRouteEval;
public static MelonPreferences_Entry<float> AutoSaveMinutes;
// NPCs
public static MelonPreferences_Entry<bool> NpcEnabled;
public static MelonPreferences_Entry<float> NpcThrottleDistance;
public static MelonPreferences_Entry<float> NpcThrottleInterval;
// Memory
public static MelonPreferences_Entry<bool> MemoryEnabled;
public static MelonPreferences_Entry<int> TextureMipmapLimit;
public static MelonPreferences_Entry<bool> StreamingMipmaps;
public static MelonPreferences_Entry<float> StreamingMipmapsBudgetMB;
public static MelonPreferences_Entry<float> PeriodicGCIntervalSeconds;
// Graphics
public static MelonPreferences_Entry<bool> GraphicsEnabled;
public static MelonPreferences_Entry<float> ShadowDistance;
public static MelonPreferences_Entry<float> CameraFarClip;
public static MelonPreferences_Entry<float> LodBias;
public static MelonPreferences_Entry<bool> DisableSSAO;
public static MelonPreferences_Entry<bool> DisableContactShadows;
public static MelonPreferences_Entry<bool> DisableGlobalIllumination;
public static MelonPreferences_Entry<bool> DisableSSR;
public static MelonPreferences_Entry<bool> DisableVolumetricFog;
// ── OLD TWEAKS ────────────────────────────────────────────────────────
public static MelonPreferences_Entry<int> TargetFPS;
public static void ApplySettings()
{
Initialize();
}
public static void Initialize()
{
var cat = MelonPreferences.CreateCategory("PerfFix");
TargetFPS = cat.CreateEntry("TargetFPS", 120, "TargetFPS", "Target framerate. 0 = uncap.");
CanvasThrottleEnabled = cat.CreateEntry("CanvasThrottle", true, "CanvasThrottle",
"Throttle WorldCanvasCuller.Update() to CanvasUpdateInterval Hz instead of every frame.");
CanvasUpdateInterval = cat.CreateEntry("CanvasUpdateInterval", 0.1f, "CanvasUpdateInterval",
"Seconds between WorldCanvasCuller distance checks (0.1 = 10 Hz). Vanilla runs at full framerate.");
IndicatorThrottleEnabled = cat.CreateEntry("IndicatorThrottle", true, "IndicatorThrottle",
"Throttle PositionIndicator.Update() (warning/error triangles) — re-projects world→screen every frame.");
IndicatorUpdateInterval = cat.CreateEntry("IndicatorUpdateInterval", 0.1f, "IndicatorUpdateInterval",
"Seconds between PositionIndicator screen-position updates (0.1 = 10 Hz).");
ThrottlePulsating = cat.CreateEntry("ThrottlePulsating", true, "ThrottlePulsating",
"Throttle PulsatingImageColor and PulsatingText Update() calls.");
PulsatingUpdateInterval = cat.CreateEntry("PulsatingUpdateInterval", 0.05f, "PulsatingUpdateInterval",
"Seconds between pulsating effect updates (0.05 = 20 Hz).");
RouteEvalCooldown = cat.CreateEntry("RouteEvalCooldown", 2.0f, "RouteEvalCooldown",
"Minimum seconds between full ECS cable-route re-evaluations.");
AsyncRouteEval = cat.CreateEntry("AsyncRouteEval", false, "AsyncRouteEval",
"EXPERIMENTAL (DISABLED!): Run EvaluateAllRoutes on a background thread. CAUSES IL2CPP CRASHES.");
AutoSaveMinutes = cat.CreateEntry("AutoSaveMinutes", 10.0f, "AutoSaveMinutes",
"Minutes between auto-saves. Large saves cause frame hitches. Set to 0 to disable.");
GraphicsEnabled = cat.CreateEntry("GraphicsEnabled", true, "GraphicsEnabled",
"Master toggle for the graphics fixes below.");
ShadowDistance = cat.CreateEntry("ShadowDistance", 20f, "ShadowDistance",
"HDRP shadow cull distance in metres. Vanilla is ~150 m.");
CameraFarClip = cat.CreateEntry("CameraFarClip", 80f, "CameraFarClip",
"Player camera far clip plane in metres. Vanilla is ~1000 m.");
LodBias = cat.CreateEntry("LodBias", 0.4f, "LodBias",
"Unity LOD bias. Lower = switch to cheaper LOD meshes sooner.");
DisableSSAO = cat.CreateEntry("DisableSSAO", true, "DisableSSAO",
"Disable HDRP Screen-Space Ambient Occlusion.");
DisableContactShadows = cat.CreateEntry("DisableContactShadows", true, "DisableContactShadows",
"Disable HDRP Contact Shadows.");
DisableGlobalIllumination = cat.CreateEntry("DisableGlobalIllumination", true, "DisableGlobalIllumination",
"Disable HDRP Screen-Space Global Illumination.");
DisableSSR = cat.CreateEntry("DisableSSR", true, "DisableSSR",
"Disable HDRP Screen-Space Reflections on floors/racks.");
DisableVolumetricFog = cat.CreateEntry("DisableVolumetricFog", false, "DisableVolumetricFog",
"Disable HDRP Volumetric Fog.");
NpcEnabled = cat.CreateEntry("NpcEnabled", true, "NpcEnabled",
"Master toggle for NPC/Technician optimizations.");
NpcThrottleDistance = cat.CreateEntry("NpcThrottleDistance", 15f, "NpcThrottleDistance",
"Distance in metres beyond which Technician FixedUpdate and LateUpdate are throttled.");
NpcThrottleInterval = cat.CreateEntry("NpcThrottleInterval", 0.2f, "NpcThrottleInterval",
"Seconds between FixedUpdate/LateUpdate ticks for distant technicians.");
MemoryEnabled = cat.CreateEntry("MemoryEnabled", true, "MemoryEnabled",
"Master toggle for memory reduction settings below.");
TextureMipmapLimit = cat.CreateEntry("TextureMipmapLimit", 1, "TextureMipmapLimit",
"Global texture mipmap skip level. 0=full resolution, 1=half resolution, 2=quarter resolution.");
StreamingMipmaps = cat.CreateEntry("StreamingMipmaps", true, "StreamingMipmaps",
"Enable Unity mipmap streaming.");
StreamingMipmapsBudgetMB = cat.CreateEntry("StreamingMipmapsBudgetMB", 512f, "StreamingMipmapsBudgetMB",
"Memory budget for streamed mipmaps in megabytes.");
PeriodicGCIntervalSeconds = cat.CreateEntry("PeriodicGCIntervalSeconds", 0f, "PeriodicGCIntervalSeconds",
"Seconds between forced garbage collection passes. Set to 0 to disable. (WARNING: Enabled GC has been known to crash Il2CppInterop)");
MelonLogger.Msg($"[gregCore.PerfFix] Loaded. " +
$"Canvas={CanvasUpdateInterval.Value}s " +
$"RouteEval={RouteEvalCooldown.Value}s " +
$"AutoSave={AutoSaveMinutes.Value}min " +
$"FarClip={CameraFarClip.Value}m");
ApplyMemorySettings();
// Base Performance Tweaks
int targetFPS = TargetFPS.Value > 0 ? TargetFPS.Value : Screen.currentResolution.refreshRate;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = targetFPS > 0 ? targetFPS : 120;
}
private static float _nextGC = 0f;
public static void OnUpdate()
{
MainThreadDispatch.Drain();
float interval = PeriodicGCIntervalSeconds?.Value ?? 0f;
if (interval > 0f && Time.realtimeSinceStartup >= _nextGC && _nextGC > 0f)
{
_nextGC = Time.realtimeSinceStartup + interval;
MelonCoroutines.Start(RunPeriodicGC());
}
}
private static System.Collections.IEnumerator RunPeriodicGC()
{
yield return null;
// GC forcing removed - Causes Il2CppInterop/Unity Finalizer NullReferenceException
MelonLogger.Msg($"[gregCore.PerfFix] Periodic GC skipped to prevent Il2CppInterop crashes.");
}
public static void OnSceneLoaded()
{
ApplySimulationFixes();
MelonCoroutines.Start(ApplyGraphicsFixesNextFrame());
if (PeriodicGCIntervalSeconds != null)
{
float interval = PeriodicGCIntervalSeconds.Value;
_nextGC = interval > 0f ? Time.realtimeSinceStartup + interval : 0f;
}
}
private static void ApplySimulationFixes()
{
try
{
var wis = WaypointInitializationSystem.Instance;
if (wis != null)
{
wis.SetEvaluationCooldown(RouteEvalCooldown.Value);
MelonLogger.Msg($"[gregCore.PerfFix] RouteEvalCooldown → {RouteEvalCooldown.Value}s");
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] RouteEvalCooldown: {ex.Message}"); }
try
{
var mgr = MainGameManager.instance;
if (mgr != null)
{
if (AutoSaveMinutes.Value <= 0f)
{
mgr.SetAutoSaveEnabled(false);
MelonLogger.Msg("[gregCore.PerfFix] AutoSave disabled.");
}
else
{
mgr.SetAutoSaveEnabled(true);
mgr.SetAutoSaveInterval(AutoSaveMinutes.Value);
MelonLogger.Msg($"[gregCore.PerfFix] AutoSave interval → {AutoSaveMinutes.Value} min");
}
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] AutoSave: {ex.Message}"); }
}
private static System.Collections.IEnumerator ApplyGraphicsFixesNextFrame()
{
yield return null;
ApplyGraphicsFixes();
}
private static void ApplyMemorySettings()
{
if (!MemoryEnabled.Value) return;
try
{
QualitySettings.globalTextureMipmapLimit = TextureMipmapLimit.Value;
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] TextureMipmapLimit: {ex.Message}"); }
try
{
QualitySettings.streamingMipmapsActive = StreamingMipmaps.Value;
if (StreamingMipmaps.Value)
{
QualitySettings.streamingMipmapsMemoryBudget = StreamingMipmapsBudgetMB.Value;
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] StreamingMipmaps: {ex.Message}"); }
}
public static void ApplyGraphicsFixes()
{
QualitySettings.lodBias = LodBias.Value;
if (!GraphicsEnabled.Value) return;
try
{
var cam = MainGameManager.instance?.playerCamera;
if (cam != null)
{
cam.farClipPlane = CameraFarClip.Value;
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] CameraFarClip: {ex.Message}"); }
try
{
var sg = SettingsSingleton.instance?.settingsGraphics;
if (sg != null)
{
sg.SetShadowDistance(ShadowDistance.Value);
}
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] ShadowDistance: {ex.Message}"); }
try
{
var sg = SettingsSingleton.instance?.settingsGraphics;
if (sg == null) return;
var profile = sg.volumeProfile;
if (profile == null) return;
int disabled = 0;
if (DisableSSAO.Value && profile.TryGet<ScreenSpaceAmbientOcclusion>(out var ssao))
{ ssao.active = false; disabled++; }
if (DisableContactShadows.Value && profile.TryGet<ContactShadows>(out var cs))
{ cs.active = false; disabled++; }
if (DisableGlobalIllumination.Value && profile.TryGet<GlobalIllumination>(out var gi))
{ gi.active = false; disabled++; }
if (DisableSSR.Value && profile.TryGet<ScreenSpaceReflection>(out var ssr))
{ ssr.active = false; disabled++; }
if (DisableVolumetricFog.Value && profile.TryGet<Fog>(out var fog))
{ fog.enableVolumetricFog.overrideState = true; fog.enableVolumetricFog.value = false; disabled++; }
}
catch (Exception ex) { MelonLogger.Warning($"[gregCore.PerfFix] HDRP volume overrides: {ex.Message}"); }
}
}
// ── WorldCanvasCuller throttle ─────────────────────────────────────────────
[HarmonyPatch(typeof(WorldCanvasCuller), "Update")]
internal static class WorldCanvasCullerPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(WorldCanvasCuller __instance)
{
if (GregPerformanceOptimizer.CanvasThrottleEnabled == null || !GregPerformanceOptimizer.CanvasThrottleEnabled.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.CanvasUpdateInterval.Value;
return true;
}
}
// ── PositionIndicator throttle ─────────────────────────────────────────────
[HarmonyPatch(typeof(PositionIndicator), "Update")]
internal static class PositionIndicatorPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(PositionIndicator __instance)
{
if (GregPerformanceOptimizer.IndicatorThrottleEnabled == null || !GregPerformanceOptimizer.IndicatorThrottleEnabled.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.IndicatorUpdateInterval.Value;
return true;
}
}
// ── PulsatingImageColor throttle ───────────────────────────────────────────
[HarmonyPatch(typeof(PulsatingImageColor), "Update")]
internal static class PulsatingImageColorPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(PulsatingImageColor __instance)
{
if (GregPerformanceOptimizer.ThrottlePulsating == null || !GregPerformanceOptimizer.ThrottlePulsating.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.PulsatingUpdateInterval.Value;
return true;
}
}
// ── PulsatingText throttle ─────────────────────────────────────────────────
[HarmonyPatch(typeof(PulsatingText), "Update")]
internal static class PulsatingTextPatch
{
private static readonly Dictionary<IntPtr, float> _nextRun = new();
static bool Prefix(PulsatingText __instance)
{
if (GregPerformanceOptimizer.ThrottlePulsating == null || !GregPerformanceOptimizer.ThrottlePulsating.Value) return true;
float now = Time.time;
var ptr = __instance.Pointer;
if (_nextRun.TryGetValue(ptr, out float next) && now < next) return false;
_nextRun[ptr] = now + GregPerformanceOptimizer.PulsatingUpdateInterval.Value;
return true;
}
}
// ── Async EvaluateAllRoutes ────────────────────────────────────────────────
[HarmonyPatch(typeof(WaypointInitializationSystem), "EvaluateAllRoutes")]
internal static class AsyncRouteEvalPatch
{
[ThreadStatic]
private static bool _allowPassthrough;
private static volatile bool _evaluationInFlight;
static bool Prefix(WaypointInitializationSystem __instance)
{
// ── KILLS IL2CPP GC BECAUSE UNMANAGED THREAD IS NOT ATTACHED ──
// Disabled fully
return true;
/*
if (GregPerformanceOptimizer.AsyncRouteEval == null || !GregPerformanceOptimizer.AsyncRouteEval.Value) return true;
if (_allowPassthrough) return true;
if (_evaluationInFlight) return false;
_evaluationInFlight = true;
var wis = __instance;
Task.Run(() =>
{
_allowPassthrough = true;
try
{
wis.EvaluateAllRoutes();
}
catch (Exception ex)
{
MelonLogger.Warning($"[gregCore.PerfFix] Async EvaluateAllRoutes threw: {ex.GetType().Name}: {ex.Message}. " +
"Falling back to main-thread execution.");
MainThreadDispatch.Enqueue(() =>
{
_allowPassthrough = true;
try { wis.EvaluateAllRoutes(); }
catch (Exception ex2) { MelonLogger.Warning($"[gregCore.PerfFix] Sync fallback also failed: {ex2.Message}"); }
finally { _allowPassthrough = false; }
});
}
finally
{
_allowPassthrough = false;
_evaluationInFlight = false;
}
});
return false;
*/
}
}
// ── Technician Animator culling ────────────────────────────────────────────
[HarmonyPatch(typeof(TechnicianManager), "AddTechnician")]
internal static class TechnicianAnimatorCullingPatch
{
static void Postfix(Technician technician)
{
if (GregPerformanceOptimizer.NpcEnabled == null || !GregPerformanceOptimizer.NpcEnabled.Value || technician == null) return;
try
{
var anim = technician.GetComponent<Animator>();
if (anim != null)
anim.cullingMode = AnimatorCullingMode.CullCompletely;
var agent = technician.GetComponent<NavMeshAgent>();
if (agent != null)
agent.obstacleAvoidanceType = ObstacleAvoidanceType.LowQualityObstacleAvoidance;
}
catch (Exception ex)
{
MelonLogger.Warning($"[gregCore.PerfFix] TechnicianAnimatorCulling: {ex.Message}");
}
}
}
// Removed Technician FixedUpdate, LateUpdate, and Footstep update patches as methods are stripped and cause Harmony init failures
-77
View File
@@ -1,77 +0,0 @@
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}");
}
-53
View File
@@ -1,53 +0,0 @@
using System;
using System.IO;
using MelonLoader;
using MelonLoader.Utils;
namespace greg.Core.Diagnostic;
public static class GregSessionLogger
{
private static string _sessionFilePath;
private static readonly object _lock = new();
public static void Initialize()
{
string logDir = Path.Combine(MelonEnvironment.UserDataDirectory, "gregCore", "Logs");
if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir);
string timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
_sessionFilePath = Path.Combine(logDir, $"session_{timestamp}.log");
Log("=== GREG CORE SESSION START ===");
Log($"OS: {Environment.OSVersion}");
Log($"Runtime: {Environment.Version}");
Log($"Engine: Unity 6 (6000.4.2f1)");
}
public static void Log(string message, string level = "INFO")
{
if (_sessionFilePath == null) return;
lock (_lock)
{
try
{
string line = $"[{DateTime.Now:HH:mm:ss.fff}] [{level}] {message}";
File.AppendAllText(_sessionFilePath, line + Environment.NewLine);
}
catch { /* Ignore logging failures to prevent crashes */ }
}
}
public static void LogError(string message, Exception ex = null)
{
Log($"{message} {(ex != null ? "\n" + ex.ToString() : "")}", "ERROR");
}
public static void LogVerbose(string message)
{
// Only log if verbose is enabled in config (placeholder for now)
Log(message, "VERBOSE");
}
}
-157
View File
@@ -1,157 +0,0 @@
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using MelonLoader;
using UnityEngine;
namespace greg.Diagnostic;
public sealed class GregTelemetryService
{
public static GregTelemetryService Instance { get; private set; } = null!;
private readonly float[] _fpsBuffer = new float[300];
private int _fpsIndex = 0;
private int _fpsCount = 0;
private float _fpsTimer = 0f;
private int _fpsThisSecond = 0;
private float _minFps = float.MaxValue;
private float _maxFps = float.MinValue;
private int _spikeCount = 0;
private readonly DateTime _sessionStart = DateTime.UtcNow;
private int _errorCount = 0;
private TelemetryConfig? _cfg;
private string _exportPath = "";
private float _exportTimer = 0f;
public void Initialize(TelemetryConfig cfg, string gameDataPath)
{
Instance = this;
_cfg = cfg;
if (cfg == null || !cfg.Enabled)
{
MelonLogger.Msg("[Telemetry] Disabled via config.");
return;
}
_exportPath = Path.Combine(gameDataPath, "gregCore_telemetry");
try
{
Directory.CreateDirectory(_exportPath);
}
catch (Exception ex)
{
MelonLogger.Warning($"[Telemetry] Could not create export path: {ex.Message}");
return;
}
MelonLogger.Msg($"[Telemetry] Initialized. Export every {cfg.ExportIntervalSeconds}s → {_exportPath}");
}
public void Tick()
{
if (_cfg == null || !_cfg.Enabled) return;
float dt = Time.unscaledDeltaTime;
float fps = dt > 0 ? 1f / dt : 0f;
_fpsBuffer[_fpsIndex % _fpsBuffer.Length] = fps;
_fpsIndex++;
_fpsCount = Math.Min(_fpsCount + 1, _fpsBuffer.Length);
if (fps < _minFps) _minFps = fps;
if (fps > _maxFps) _maxFps = fps;
if (dt > 0.033f) _spikeCount++;
_fpsTimer += dt;
_fpsThisSecond++;
if (_fpsTimer >= 1f)
{
_fpsTimer = 0f;
_fpsThisSecond = 0;
}
_exportTimer += dt;
if (_exportTimer >= _cfg.ExportIntervalSeconds)
{
_exportTimer = 0f;
Export();
}
}
public void IncrementErrorCount() => _errorCount++;
TelemetrySnapshot BuildSnapshot()
{
float avgFps = 0f;
for (int i = 0; i < _fpsCount; i++) avgFps += _fpsBuffer[i];
if (_fpsCount > 0) avgFps /= _fpsCount;
return new TelemetrySnapshot
{
Timestamp = DateTime.UtcNow,
SessionSeconds = (float)(DateTime.UtcNow - _sessionStart).TotalSeconds,
GregCoreVersion = greg.Core.gregReleaseVersion.Current,
MelonLoaderVersion = MelonLoader.Properties.BuildInfo.Version,
FpsCurrent = _fpsBuffer[(_fpsIndex - 1 + _fpsBuffer.Length) % _fpsBuffer.Length],
FpsAverage = MathF.Round(avgFps, 1),
FpsMin = _minFps < float.MaxValue ? _minFps : 0f,
FpsMax = _maxFps > float.MinValue ? _maxFps : 0f,
FrameSpikeCount = _spikeCount,
TargetFps = Application.targetFrameRate,
RamUsedMb = MathF.Round(GC.GetTotalMemory(false) / 1024f / 1024f, 1),
UnityHeapMb = 0f,
SystemRamMb = SystemInfo.systemMemorySize,
GpuMemoryMb = SystemInfo.graphicsMemorySize,
GpuName = SystemInfo.graphicsDeviceName,
CpuName = SystemInfo.processorType,
CpuCores = SystemInfo.processorCount,
GameState = GregFrameLimiterService.Instance?.CurrentStateName ?? "unknown",
ErrorCount = _errorCount,
};
}
void Export()
{
if (string.IsNullOrEmpty(_exportPath)) return;
try
{
var snapshot = BuildSnapshot();
var options = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
string json = JsonSerializer.Serialize(snapshot, options);
string latest = Path.Combine(_exportPath, "latest.json");
File.WriteAllText(latest, json);
if (_cfg?.ArchiveSnapshots == true)
{
string timestamped = Path.Combine(_exportPath, $"telemetry_{DateTime.UtcNow:yyyyMMdd_HHmmss}.json");
File.WriteAllText(timestamped, json);
}
if (_cfg?.LogToConsole == true)
{
MelonLogger.Msg($"[Telemetry] FPS={snapshot.FpsCurrent:F0} avg={snapshot.FpsAverage} RAM={snapshot.RamUsedMb}MB GPU={snapshot.GpuMemoryMb}MB");
}
}
catch (Exception ex)
{
MelonLogger.Warning($"[Telemetry] Export failed: {ex.Message}");
}
}
}
-18
View File
@@ -1,18 +0,0 @@
namespace greg.Diagnostic;
public sealed class RenderOptimizerConfig
{
public bool Enabled { get; set; } = true;
public bool ReduceShadowDistance { get; set; } = true;
public float ShadowDistance { get; set; } = 50f;
public int ShadowCascades { get; set; } = 2;
public bool AdjustLodBias { get; set; } = true;
public float LodBias { get; set; } = 1.0f;
public bool LimitPixelLights { get; set; } = true;
public int PixelLightCount { get; set; } = 2;
public bool SetAnisotropicFiltering { get; set; } = true;
public bool AnisotropicFiltering { get; set; } = false;
public bool SetAntiAliasing { get; set; } = true;
public int AntiAliasingLevel { get; set; } = 0;
public bool DisableSoftParticles { get; set; } = true;
}
-41
View File
@@ -1,41 +0,0 @@
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; }
}
+26 -12
View File
@@ -4,7 +4,6 @@
/// 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;
@@ -17,7 +16,7 @@ namespace gregCore.GameLayer.Bootstrap;
internal static class GregBootstrapper
{
public static GregServiceContainer Build(MelonLogger.Instance melonLogger)
public static GregServiceContainer Build(global::MelonLoader.MelonLogger.Instance melonLogger)
{
var container = new GregServiceContainer();
var logger = new MelonLoggerAdapter(melonLogger);
@@ -25,19 +24,33 @@ internal static class GregBootstrapper
container.Register<IGregLogger>(logger);
logger.Info("gregCore v1.0.0 Bootstrap gestartet");
container.Register<IGregEventBus>(new GregEventBus(logger));
var bus = new GregEventBus(logger);
container.Register<IGregEventBus>(bus);
container.Register<IGregConfigService>(new GregConfigService(logger));
container.Register<IGregPersistenceService>(new GregPersistenceService(logger));
container.Register<IGregFfiBridge>(new Win32FfiBridge(logger, container.GetRequired<IGregEventBus>()));
var apiContext = new global::gregCore.PublicApi.GregApiContext {
Logger = logger,
EventBus = bus,
Config = container.GetRequired<IGregConfigService>(),
Persist = container.GetRequired<IGregPersistenceService>()
};
container.Register<IGregLanguageBridge>("lua", new LuaBridge(logger, container.GetRequired<IGregEventBus>()));
container.Register<IGregLanguageBridge>("js", new JsBridge(logger, container.GetRequired<IGregEventBus>()));
var governor = new gregCore.Infrastructure.Performance.GregPerformanceGovernor(apiContext);
container.Register<gregCore.Infrastructure.Performance.GregPerformanceGovernor>(governor);
bus.SetGovernor(governor);
container.Register<IGregFfiBridge>(new Win32FfiBridge(logger, bus));
container.Register<IGregLanguageBridge>("lua", new LuaBridge(logger, bus));
container.Register<IGregLanguageBridge>("js", new JsBridge(logger, bus));
container.Register<IAssemblyScanner>(new AssemblyScanner());
container.Register<IGregPluginRegistry>(new GregPluginRegistry(container.GetRequired<IAssemblyScanner>(), logger, container.GetRequired<IGregEventBus>()));
container.Register<IGregPluginRegistry>(new GregPluginRegistry(container.GetRequired<IAssemblyScanner>(), logger, bus));
HookIntegration.Install(container.GetRequired<IGregEventBus>(), logger);
HookIntegration.Install(bus, logger);
global::gregCore.PublicApi.greg._context = apiContext;
global::gregCore.PublicApi.greg._governor = governor;
ValidateStartup(container);
@@ -52,12 +65,13 @@ internal static class GregBootstrapper
container.GetRequired<IGregEventBus>();
container.GetRequired<IGregFfiBridge>();
var melonVersion = MelonLoader.MelonLoader.Version;
if (melonVersion < new Version(0, 6, 0))
// Workaround for MelonLoader naming conflict
var melonVersion = typeof(global::MelonLoader.MelonLogger).Assembly.GetName().Version;
if (melonVersion != null && 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 gameRoot = global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
if (string.IsNullOrEmpty(gameRoot)) return;
var gameAssembly = Path.Combine(gameRoot, "MelonLoader", "Il2CppAssemblies", "Assembly-CSharp.dll");
@@ -23,6 +23,12 @@ public sealed class GregCoreLoader : MelonMod
_container.GetRequired<IGregPluginRegistry>().LoadAll();
}
public override void OnUpdate()
{
_container?.Get<gregCore.Infrastructure.Performance.GregPerformanceGovernor>()?.OnUpdate();
_container?.Get<GregEventBus>()?.FlushDeferredEvents();
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName) =>
_container?.GetRequired<IGregEventBus>()
.Publish(HookName.Create("lifecycle", "SceneLoaded").Full,
@@ -0,0 +1,22 @@
using System.Collections;
using gregCore.PublicApi;
namespace gregCore.Infrastructure.Automation;
internal sealed class AutomationEngine
{
public DeliveryZoneEngine Delivery { get; }
public CableLayingEngine Cabling { get; }
public RackBuildEngine RackBuild { get; }
public NetworkConfigEngine Network { get; }
public RepairEngine Repair { get; }
internal AutomationEngine(GregApiContext ctx)
{
Delivery = new DeliveryZoneEngine(ctx.Logger, ctx.EventBus);
Cabling = new CableLayingEngine(ctx.Logger, ctx.EventBus);
RackBuild = new RackBuildEngine(ctx.Logger, Cabling);
Network = new NetworkConfigEngine(ctx.Logger);
Repair = new RepairEngine(ctx.Logger);
}
}
@@ -0,0 +1,49 @@
using System.Collections;
using UnityEngine;
namespace gregCore.Infrastructure.Automation;
internal sealed class CableLayingEngine
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _bus;
internal CableLayingEngine(IGregLogger logger, IGregEventBus bus)
{
_logger = logger.ForContext(nameof(CableLayingEngine));
_bus = bus;
}
internal IEnumerator LayCableCoroutine(string src, string tgt, CableType type, Action<AutomationResult> onComplete)
{
yield return null;
var map = global::Il2Cpp.NetworkMap.instance;
if (map == null)
{
onComplete(AutomationResult.Failure("NetworkMap nicht gefunden."));
yield break;
}
try {
map.Connect(src, tgt);
_logger.Info($"Kabel verlegt: {src} -> {tgt} ({type})");
onComplete(AutomationResult.Success(1));
} catch (Exception ex) {
onComplete(AutomationResult.Failure("Fehler beim Verlegen", ex.Message));
}
}
internal IEnumerator LayStarTopologyCoroutine(string[] devices, string switchId, CableType type, IProgress<AutomationProgress>? progress, Action<AutomationResult> onComplete)
{
int done = 0;
for (int i = 0; i < devices.Length; i++)
{
progress?.Report(new AutomationProgress { CurrentTask = $"Verbinde {devices[i]}", Current = i, Total = devices.Length });
AutomationResult? res = null;
yield return LayCableCoroutine(devices[i], switchId, type, r => res = r);
if (res?.IsSuccess == true) done++;
yield return null;
}
onComplete(AutomationResult.Partial(done, devices.Length));
}
}
@@ -0,0 +1,57 @@
using System.Collections;
using UnityEngine;
using MelonLoader;
namespace gregCore.Infrastructure.Automation;
internal sealed class DeliveryZoneEngine
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _bus;
internal DeliveryZoneEngine(IGregLogger logger, IGregEventBus bus)
{
_logger = logger.ForContext(nameof(DeliveryZoneEngine));
_bus = bus;
}
internal IEnumerator ProcessAllCoroutine(IProgress<AutomationProgress>? progress, Action<AutomationResult> onComplete)
{
_logger.Debug("Starte Lieferzonen-Verarbeitung...");
yield return null;
var items = FindItems();
if (items.Count == 0)
{
onComplete(AutomationResult.Success(0, "Keine Items gefunden."));
yield break;
}
int processed = 0;
foreach (var item in items)
{
progress?.Report(new AutomationProgress { CurrentTask = $"Verarbeite {item.name}", Current = processed, Total = items.Count });
yield return null; // Spread over frames
// Logic placeholder: Move item
_logger.Debug($"Verschiebe Item: {item.name}");
processed++;
}
onComplete(AutomationResult.Success(processed, $"{processed} Items aus Lieferzone verarbeitet."));
}
private List<global::Il2Cpp.Item> FindItems()
{
var result = new List<global::Il2Cpp.Item>();
var objects = global::UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.Item>();
if (objects == null) return result;
foreach (var obj in objects)
{
// Simple logic: Items near certain coords or with certain names
result.Add(obj);
}
return result;
}
}
@@ -0,0 +1,17 @@
using System.Collections;
namespace gregCore.Infrastructure.Automation;
internal sealed class NetworkConfigEngine
{
private readonly IGregLogger _logger;
internal NetworkConfigEngine(IGregLogger logger) => _logger = logger.ForContext(nameof(NetworkConfigEngine));
internal IEnumerator SetupNetworkSegmentCoroutine(NetworkSegmentConfig config, IProgress<AutomationProgress>? progress, Action<AutomationResult> onComplete)
{
_logger.Info("Konfiguriere Netzwerk-Segment...");
yield return null;
onComplete(AutomationResult.Success(config.Devices.Length));
}
}
@@ -0,0 +1,26 @@
using System.Collections;
using UnityEngine;
namespace gregCore.Infrastructure.Automation;
internal sealed class RackBuildEngine
{
private readonly IGregLogger _logger;
private readonly CableLayingEngine _cabling;
internal RackBuildEngine(IGregLogger logger, CableLayingEngine cabling)
{
_logger = logger.ForContext(nameof(RackBuildEngine));
_cabling = cabling;
}
internal IEnumerator BuildRackCoroutine(RackBuildConfig config, IProgress<AutomationProgress>? progress, Action<AutomationResult> onComplete)
{
_logger.Info($"Baue Rack {config.RackId}...");
yield return null;
// Placeholder for installation logic
int count = config.Servers.Length;
onComplete(AutomationResult.Success(count, $"Rack {config.RackId} erfolgreich bestückt."));
}
}
@@ -0,0 +1,17 @@
using System.Collections;
namespace gregCore.Infrastructure.Automation;
internal sealed class RepairEngine
{
private readonly IGregLogger _logger;
internal RepairEngine(IGregLogger logger) => _logger = logger.ForContext(nameof(RepairEngine));
internal IEnumerator RepairAllCoroutine(IProgress<AutomationProgress>? progress, Action<AutomationResult> onComplete)
{
_logger.Info("Repariere alle Geräte...");
yield return null;
onComplete(AutomationResult.Success(0, "Alles repariert."));
}
}
@@ -1,9 +1,3 @@
/// <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;
@@ -21,34 +15,21 @@ public sealed class GregPersistenceService : IGregPersistenceService
Directory.CreateDirectory(_saveDirectory);
}
public T? LoadData<T>(string key) where T : class
public void Set<T>(string key, T value) where T : notnull
{
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;
}
var path = Path.Combine(_saveDirectory, $"{key}.json");
File.WriteAllText(path, JsonSerializer.Serialize(value));
}
public void SaveData<T>(string key, T data) where T : class
public T Get<T>(string key, T defaultValue = default!) where T : notnull
{
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);
}
var path = Path.Combine(_saveDirectory, $"{key}.json");
if (!File.Exists(path)) return defaultValue;
try {
return JsonSerializer.Deserialize<T>(File.ReadAllText(path)) ?? defaultValue;
} catch { return defaultValue; }
}
}
public bool Has(string key) => File.Exists(Path.Combine(_saveDirectory, $"{key}.json"));
public void Delete(string key) => File.Delete(Path.Combine(_saveDirectory, $"{key}.json"));
}
@@ -0,0 +1,64 @@
using UnityEngine;
namespace gregCore.Infrastructure.Performance;
internal sealed class GregFrameRateLimiter : IDisposable
{
private readonly IGregLogger _logger;
private PerformanceProfile _profile;
private bool _isDisposed;
private bool _wasFocused = true;
internal GregFrameRateLimiter(IGregLogger logger, PerformanceProfile profile)
{
_logger = logger.ForContext(nameof(GregFrameRateLimiter));
_profile = profile;
Apply(profile);
}
internal void Apply(PerformanceProfile profile)
{
_profile = profile;
global::UnityEngine.QualitySettings.vSyncCount = profile.EnableVSync ? 1 : 0;
if (!profile.EnableVSync)
global::UnityEngine.Application.targetFrameRate = profile.TargetFps;
else
global::UnityEngine.Application.targetFrameRate = -1;
global::UnityEngine.QualitySettings.SetQualityLevel(profile.QualityLevel, false);
global::UnityEngine.QualitySettings.shadowDistance = profile.ShadowDistance;
global::UnityEngine.QualitySettings.globalTextureMipmapLimit = profile.TextureResolution;
_logger.Info($"[FPS] VSync:{profile.EnableVSync} Target:{profile.TargetFps} Quality:{profile.QualityLevel}");
}
internal void OnUpdate()
{
if (_isDisposed) return;
var isFocused = global::UnityEngine.Application.isFocused;
if (isFocused == _wasFocused) return;
_wasFocused = isFocused;
if (!isFocused)
{
global::UnityEngine.Application.targetFrameRate = _profile.UnfocusedFps;
global::UnityEngine.QualitySettings.vSyncCount = 0;
_logger.Debug($"[FPS] Unfocused -> {_profile.UnfocusedFps} FPS");
}
else
{
Apply(_profile);
_logger.Debug($"[FPS] Focused -> {_profile.TargetFps} FPS");
}
}
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
global::UnityEngine.Application.targetFrameRate = -1;
global::UnityEngine.QualitySettings.vSyncCount = 1;
}
}
@@ -0,0 +1,63 @@
using System.Collections;
using UnityEngine;
using MelonLoader;
namespace gregCore.Infrastructure.Performance;
internal sealed class GregMemoryPressureHandler
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _bus;
private readonly PerformanceProfile _profile;
private DateTime _lastGcRun = DateTime.MinValue;
private DateTime _lastCriticalRun = DateTime.MinValue;
internal GregMemoryPressureHandler(IGregLogger logger, IGregEventBus bus, PerformanceProfile profile)
{
_logger = logger.ForContext(nameof(GregMemoryPressureHandler));
_bus = bus;
_profile = profile;
bus.Subscribe("greg.performance.RamWarning", OnRamWarning);
bus.Subscribe("greg.performance.RamCritical", OnRamCritical);
}
internal void OnUpdate()
{
if (_profile.GcIntervalSeconds <= 0) return;
var now = DateTime.UtcNow;
if ((now - _lastGcRun).TotalSeconds < _profile.GcIntervalSeconds) return;
_lastGcRun = now;
GC.Collect(0, GCCollectionMode.Optimized, false);
}
private void OnRamWarning(EventPayload p)
{
_logger.Warning("[Memory] RAM Warning - Gen1 GC");
Task.Run(() => { GC.Collect(1, GCCollectionMode.Optimized, false); });
}
private void OnRamCritical(EventPayload p)
{
// UNITY MAIN THREAD REQUIRED
MelonCoroutines.Start(UnloadUnusedCoroutine());
}
private IEnumerator UnloadUnusedCoroutine()
{
var now = DateTime.UtcNow;
if ((now - _lastCriticalRun).TotalSeconds < 30) yield break;
_lastCriticalRun = now;
_logger.Warning("[Memory] RAM KRITISCH - UnloadUnusedAssets + Full GC");
var unloadOp = global::UnityEngine.Resources.UnloadUnusedAssets();
while (!unloadOp.isDone) yield return null;
_logger.Info("[Memory] UnloadUnusedAssets abgeschlossen");
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, false, true);
GC.WaitForPendingFinalizers();
_bus.Publish("greg.performance.PressureCleanup", new EventPayload { OccurredAtUtc = DateTime.UtcNow, Data = new Dictionary<string, object>() });
}
}
@@ -0,0 +1,47 @@
namespace gregCore.Infrastructure.Performance;
internal sealed class GregOperationQueue : IDisposable
{
private readonly GregRequestThrottler _throttler;
private readonly IGregLogger _logger;
private readonly PriorityQueue<QueuedOperation, int> _queue = new PriorityQueue<QueuedOperation, int>();
private readonly SemaphoreSlim _processLock = new SemaphoreSlim(1, 1);
private bool _isDisposed;
internal GregOperationQueue(GregRequestThrottler throttler, IGregLogger logger)
{
_throttler = throttler;
_logger = logger.ForContext(nameof(GregOperationQueue));
}
internal async Task<T> EnqueueAsync<T>(string name, Func<Task<T>> operation, OperationPriority priority = OperationPriority.Normal, CancellationToken ct = default)
{
var tcs = new TaskCompletionSource<T>();
var op = new QueuedOperation(name, async () => {
try { tcs.SetResult(await _throttler.ExecuteOperationAsync(name, operation, priority, ct)); }
catch (Exception ex) { tcs.SetException(ex); }
}, (int)priority);
lock (_queue) { _queue.Enqueue(op, -(int)priority); }
_ = ProcessQueueAsync(ct);
return await tcs.Task;
}
private async Task ProcessQueueAsync(CancellationToken ct)
{
if (!await _processLock.WaitAsync(0)) return;
try {
while (true) {
QueuedOperation? op;
lock (_queue) { if (!_queue.TryDequeue(out op, out _)) break; }
if (ct.IsCancellationRequested) break;
try { await op.Execute(); } catch (Exception ex) { _logger.Error($"[Queue] Fehlgeschlagen: {op.Name}", ex); }
}
} finally { _processLock.Release(); }
}
internal int QueueDepth { get { lock (_queue) return _queue.Count; } }
public void Dispose() { if (!_isDisposed) { _isDisposed = true; _processLock.Dispose(); } }
private record QueuedOperation(string Name, Func<Task> Execute, int Priority);
}
@@ -0,0 +1,70 @@
using gregCore.PublicApi;
namespace gregCore.Infrastructure.Performance;
internal sealed class GregPerformanceGovernor : IGregPerformanceGovernor, IDisposable
{
private readonly GregFrameRateLimiter _fpsLimiter;
private readonly GregRequestThrottler _throttler;
private readonly GregResourceMonitor _monitor;
private readonly GregMemoryPressureHandler _memHandler;
private readonly GregOperationQueue _queue;
private readonly IGregLogger _logger;
private PerformanceProfile _profile;
private int _eventsThisFrame;
internal GregPerformanceGovernor(GregApiContext ctx, PerformanceProfile? profile = null)
{
_logger = ctx.Logger.ForContext(nameof(GregPerformanceGovernor));
_profile = profile ?? PerformanceProfile.Balanced;
_fpsLimiter = new GregFrameRateLimiter(ctx.Logger, _profile);
_throttler = new GregRequestThrottler(ctx.Logger, _profile);
_monitor = new GregResourceMonitor(ctx.Logger, ctx.EventBus, _profile);
_memHandler = new GregMemoryPressureHandler(ctx.Logger, ctx.EventBus, _profile);
_queue = new GregOperationQueue(_throttler, ctx.Logger);
_monitor.Start(5000);
_logger.Info($"[Governor] Initialisiert mit Prefix-Architektur.");
}
public void OnUpdate()
{
_fpsLimiter.OnUpdate();
_memHandler.OnUpdate();
_monitor.CacheUnityMemoryStats();
_eventsThisFrame = 0;
}
public bool CanDispatchEvent()
{
if (_eventsThisFrame >= _profile.MaxEventsPerFrame) return false;
_eventsThisFrame++;
return true;
}
internal Task<T> QueueOperationAsync<T>(string name, Func<Task<T>> op, OperationPriority prio = OperationPriority.Normal, CancellationToken ct = default)
=> _queue.EnqueueAsync(name, op, prio, ct);
internal void Configure(PerformanceProfile profile)
{
_profile = profile;
_fpsLimiter.Apply(profile);
_throttler.UpdateProfile(profile);
}
internal PerformanceStats GetStats() => new PerformanceStats {
Profile = _profile,
Resources = _monitor.GetLatest(),
Throttle = _throttler.GetMetrics(),
QueueDepth = _queue.QueueDepth
};
public void Dispose()
{
_fpsLimiter.Dispose();
_throttler.Dispose();
_monitor.Dispose();
_queue.Dispose();
}
}
@@ -0,0 +1,62 @@
namespace gregCore.Infrastructure.Performance;
internal sealed class GregRequestThrottler : IDisposable
{
private readonly IGregLogger _logger;
private SemaphoreSlim _opSemaphore;
private SemaphoreSlim _reqSemaphore;
private PerformanceProfile _profile;
private int _totalQueued;
private int _totalCompleted;
private int _currentActive;
internal GregRequestThrottler(IGregLogger logger, PerformanceProfile profile)
{
_logger = logger.ForContext(nameof(GregRequestThrottler));
_profile = profile;
_opSemaphore = new SemaphoreSlim(profile.MaxConcurrentOps);
_reqSemaphore = new SemaphoreSlim(profile.MaxConcurrentRequests);
}
internal async Task<T> ExecuteOperationAsync<T>(string operationName, Func<Task<T>> operation, OperationPriority priority = OperationPriority.Normal, CancellationToken ct = default)
{
Interlocked.Increment(ref _totalQueued);
await _opSemaphore.WaitAsync(ct);
Interlocked.Increment(ref _currentActive);
try {
_logger.Debug($"[Throttle] START '{operationName}' (aktiv: {_currentActive}/{_profile.MaxConcurrentOps})");
return await operation();
} finally {
Interlocked.Decrement(ref _currentActive);
Interlocked.Increment(ref _totalCompleted);
_opSemaphore.Release();
}
}
internal void UpdateProfile(PerformanceProfile profile)
{
_profile = profile;
var oldOp = _opSemaphore;
var oldReq = _reqSemaphore;
_opSemaphore = new SemaphoreSlim(profile.MaxConcurrentOps);
_reqSemaphore = new SemaphoreSlim(profile.MaxConcurrentRequests);
oldOp.Dispose();
oldReq.Dispose();
}
internal ThrottleMetrics GetMetrics() => new ThrottleMetrics
{
TotalQueued = _totalQueued,
TotalCompleted = _totalCompleted,
CurrentActive = _currentActive,
MaxConcurrent = _profile.MaxConcurrentOps,
QueueDepth = _totalQueued - _totalCompleted - _currentActive
};
public void Dispose()
{
_opSemaphore.Dispose();
_reqSemaphore.Dispose();
}
}
@@ -0,0 +1,91 @@
using System.Diagnostics;
namespace gregCore.Infrastructure.Performance;
internal sealed class GregResourceMonitor : IDisposable
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _bus;
private readonly PerformanceProfile _profile;
private System.Timers.Timer? _timer;
private ResourceSnapshot _lastSnapshot = new ResourceSnapshot();
private ResourceSnapshot _lastUnityStats = new ResourceSnapshot();
private bool _isDisposed;
private static readonly Process _currentProcess = Process.GetCurrentProcess();
internal GregResourceMonitor(IGregLogger logger, IGregEventBus bus, PerformanceProfile profile)
{
_logger = logger.ForContext(nameof(GregResourceMonitor));
_bus = bus;
_profile = profile;
}
internal void Start(int intervalMs = 5000)
{
_timer = new System.Timers.Timer(intervalMs);
_timer.Elapsed += (_, _) => MeasureAndReport();
_timer.AutoReset = true;
_timer.Start();
_logger.Info($"[Monitor] Gestartet ({intervalMs}ms)");
}
// UNITY MAIN THREAD REQUIRED
internal void CacheUnityMemoryStats()
{
_lastUnityStats = _lastUnityStats with
{
UnityAllocatedMb = (int)(global::UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024 / 1024),
UnityReservedMb = (int)(global::UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong() / 1024 / 1024),
UnityUnusedMb = (int)(global::UnityEngine.Profiling.Profiler.GetTotalUnusedReservedMemoryLong() / 1024 / 1024),
};
}
private void MeasureAndReport()
{
if (_isDisposed) return;
try {
_currentProcess.Refresh();
var snapshot = new ResourceSnapshot {
TimestampUtc = DateTime.UtcNow,
RamUsedMb = (int)(_currentProcess.WorkingSet64 / 1024 / 1024),
PrivateMemoryMb = (int)(_currentProcess.PrivateMemorySize64 / 1024 / 1024),
GcTotalMemoryMb = (int)(GC.GetTotalMemory(false) / 1024 / 1024),
UnityAllocatedMb = _lastUnityStats.UnityAllocatedMb,
UnityReservedMb = _lastUnityStats.UnityReservedMb,
UnityUnusedMb = _lastUnityStats.UnityUnusedMb,
GcGen0Collections = GC.CollectionCount(0),
GcGen1Collections = GC.CollectionCount(1),
GcGen2Collections = GC.CollectionCount(2),
ThreadCount = _currentProcess.Threads.Count
};
_lastSnapshot = snapshot;
_bus.Publish("greg.performance.ResourceSnapshot", new EventPayload {
OccurredAtUtc = snapshot.TimestampUtc,
Data = new Dictionary<string, object> { ["ramMb"] = snapshot.RamUsedMb, ["unityMb"] = snapshot.UnityAllocatedMb }
});
CheckThresholds(snapshot);
} catch (Exception ex) { _logger.Warning($"[Monitor] Messfehler: {ex.Message}"); }
}
private void CheckThresholds(ResourceSnapshot s)
{
if (s.RamUsedMb >= _profile.RamCriticalMb)
_bus.Publish("greg.performance.RamCritical", new EventPayload { OccurredAtUtc = DateTime.UtcNow, Data = new Dictionary<string, object> { ["ramMb"] = s.RamUsedMb } });
else if (s.RamUsedMb >= _profile.RamWarningMb)
_bus.Publish("greg.performance.RamWarning", new EventPayload { OccurredAtUtc = DateTime.UtcNow, Data = new Dictionary<string, object> { ["ramMb"] = s.RamUsedMb } });
}
internal ResourceSnapshot GetLatest() => _lastSnapshot;
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
_timer?.Stop();
_timer?.Dispose();
}
}
@@ -8,7 +8,7 @@ using Mono.Cecil;
namespace gregCore.Infrastructure.Plugins;
internal sealed class AssemblyScanner : IAssemblyScanner
public sealed class AssemblyScanner : IAssemblyScanner
{
public IReadOnlyList<PluginInfo> ScanDirectory(string path)
{
@@ -0,0 +1,12 @@
/// <file-summary>
/// Schicht: Infrastructure (Internal)
/// Zweck: Interface für den Assembly-Scanner.
/// Maintainer: Wird intern von der Plugin-Registry genutzt.
/// </file-summary>
namespace gregCore.Infrastructure.Plugins;
public interface IAssemblyScanner
{
IReadOnlyList<PluginInfo> ScanDirectory(string path);
}
+27
View File
@@ -0,0 +1,27 @@
# gregCore API — Modder Guide
## Einstieg
```csharp
using gregCore.PublicApi;
public class MyMod : GregMod
{
public override void OnLoad()
{
// Geld hinzufügen
greg.Economy.AddMoney(500f);
// Auf Tagesende reagieren
greg.Time.OnDayEnd += OnNewDay;
// UI-Feedback
greg.UI.ShowToast("MyMod geladen!");
}
private void OnNewDay()
{
greg.UI.ShowToast("Neuer Tag!");
}
}
```
@@ -0,0 +1,69 @@
using gregCore.Infrastructure.Automation;
using gregCore.PublicApi.Types;
using System.Collections;
using MelonLoader;
namespace gregCore.PublicApi.Modules;
public sealed class GregAutomationModule
{
private readonly AutomationEngine _engine;
private readonly IGregLogger _logger;
private readonly List<GregScheduledTask> _tasks = new();
internal GregAutomationModule(GregApiContext ctx)
{
_engine = new AutomationEngine(ctx);
_logger = ctx.Logger.ForContext(nameof(GregAutomationModule));
ctx.EventBus.Subscribe("greg.lifecycle.OnEndOfTheDay", _ => TickTasks());
}
public Task<AutomationResult> ProcessDeliveryZoneAsync(IProgress<AutomationProgress>? progress = null)
=> RunCoroutine(c => _engine.Delivery.ProcessAllCoroutine(progress, c));
public Task<AutomationResult> LayCableAsync(string src, string tgt, CableType type = CableType.Cat6)
=> RunCoroutine(c => _engine.Cabling.LayCableCoroutine(src, tgt, type, c));
public Task<AutomationResult> BuildRackAsync(RackBuildConfig config, IProgress<AutomationProgress>? progress = null)
=> RunCoroutine(c => _engine.RackBuild.BuildRackCoroutine(config, progress, c));
public Task<AutomationResult> SetupNetworkSegmentAsync(NetworkSegmentConfig config, IProgress<AutomationProgress>? progress = null)
=> RunCoroutine(c => _engine.Network.SetupNetworkSegmentCoroutine(config, progress, c));
public Task<AutomationResult> RepairBrokenDevicesAsync(IProgress<AutomationProgress>? progress = null)
=> RunCoroutine(c => _engine.Repair.RepairAllCoroutine(progress, c));
public GregEventHandle EveryNDays(int days, Action action)
{
var task = new GregScheduledTask(days, action);
_tasks.Add(task);
return new GregEventHandle(() => _tasks.Remove(task));
}
public GregEventHandle EveryNDays(int days, Func<Task> asyncAction)
=> EveryNDays(days, () => MelonCoroutines.Start(RunAsync(asyncAction)));
private static Task<AutomationResult> RunCoroutine(Func<Action<AutomationResult>, IEnumerator> factory)
{
var tcs = new TaskCompletionSource<AutomationResult>();
MelonCoroutines.Start(factory(res => tcs.SetResult(res)));
return tcs.Task;
}
private static IEnumerator RunAsync(Func<Task> action)
{
var t = action();
while (!t.IsCompleted) yield return null;
}
private void TickTasks() { foreach (var t in _tasks.ToList()) t.Tick(); }
}
internal sealed class GregScheduledTask
{
private readonly int _interval;
private readonly Action _action;
private int _count;
public GregScheduledTask(int interval, Action action) { _interval = interval; _action = action; }
public void Tick() { if (++_count >= _interval) { _count = 0; _action(); } }
}
@@ -0,0 +1,53 @@
using gregCore.PublicApi.Types;
namespace gregCore.PublicApi.Modules;
public sealed class GregEconomyModule
{
private readonly GregApiContext _ctx;
internal GregEconomyModule(GregApiContext ctx) => _ctx = ctx;
private global::Il2Cpp.Player? Player => global::Il2Cpp.PlayerManager.instance?.playerClass;
public float GetBalance() => Player?.money ?? 0f;
public float GetXP() => Player?.xp ?? 0f;
public float GetReputation() => Player?.reputation ?? 0f;
public bool AddMoney(float amount)
{
var player = Player;
if (player == null) return false;
player.UpdateCoin(amount, false);
return true;
}
public bool SetBalance(float amount)
{
var player = Player;
if (player == null) return false;
player.UpdateCoin(amount - player.money, false);
return true;
}
public bool AddXP(float amount)
{
var player = Player;
if (player == null) return false;
player.UpdateXP(amount);
return true;
}
public bool AddReputation(float amount)
{
var player = Player;
if (player == null) return false;
player.UpdateReputation(amount);
return true;
}
public event Action<float>? OnMoneyChanged
{
add => _ctx.EventBus.Subscribe("greg.economy.PlayerCoinUpdated", p => value?.Invoke((float)p.Data["amount"]));
remove => _ctx.EventBus.Unsubscribe("greg.economy.PlayerCoinUpdated", p => value?.Invoke((float)p.Data["amount"]));
}
}
@@ -0,0 +1,9 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregFacilityModule
{
private readonly GregApiContext _ctx;
internal GregFacilityModule(GregApiContext ctx) => _ctx = ctx;
public bool UnlockRoom(string roomId) => true; // API Logic
}
@@ -0,0 +1,34 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregNetworkModule
{
private readonly GregApiContext _ctx;
internal GregNetworkModule(GregApiContext ctx) => _ctx = ctx;
public bool ConnectDevice(string sourceId, string targetId)
{
var map = global::Il2Cpp.NetworkMap.instance;
if (map == null) return false;
map.Connect(sourceId, targetId);
return true;
}
public bool DisconnectDevice(string sourceId, string targetId)
{
var map = global::Il2Cpp.NetworkMap.instance;
if (map == null) return false;
map.Disconnect(sourceId, targetId);
return true;
}
public int GetFreeVlanId() => global::Il2Cpp.MainGameManager.instance?.GetFreeVlanId() ?? -1;
public bool IsIpDuplicate(string ip)
=> global::Il2Cpp.NetworkMap.instance?.IsIpAddressDuplicate(ip, null!) ?? false;
public event Action<string, string>? OnDeviceConnected
{
add => _ctx.EventBus.Subscribe("greg.networking.Connect", p => value?.Invoke((string)p.Data["source"], (string)p.Data["target"]));
remove => _ctx.EventBus.Unsubscribe("greg.networking.Connect", p => value?.Invoke((string)p.Data["source"], (string)p.Data["target"]));
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregNpcModule
{
private readonly GregApiContext _ctx;
internal GregNpcModule(GregApiContext ctx) => _ctx = ctx;
public bool HireEmployee(string techId) => true;
}
@@ -0,0 +1,39 @@
using gregCore.Infrastructure.Performance;
namespace gregCore.PublicApi.Modules;
public sealed class GregPerformanceModule
{
private readonly GregPerformanceGovernor _governor;
private readonly IGregLogger _logger;
private readonly IGregEventBus _bus;
internal GregPerformanceModule(GregApiContext ctx, GregPerformanceGovernor governor)
{
_governor = governor;
_logger = ctx.Logger.ForContext(nameof(GregPerformanceModule));
_bus = ctx.EventBus;
}
public void Configure(PerformanceProfile profile) => _governor.Configure(profile);
public void UseProfile(PerformanceProfile profile) => _governor.Configure(profile);
public void SetTargetFPS(int fps) => Configure(_governor.GetStats().Profile with { TargetFps = fps });
public void ThrottleWhenUnfocused(bool enabled, int unfocusedFps = 15) => Configure(_governor.GetStats().Profile with { UnfocusedFps = enabled ? unfocusedFps : -1 });
public void SetMaxConcurrentOperations(int max) => Configure(_governor.GetStats().Profile with { MaxConcurrentOps = max });
public Task<T> QueueOperation<T>(string name, Func<Task<T>> operation, OperationPriority priority = OperationPriority.Normal)
=> _governor.QueueOperationAsync(name, operation, priority);
public PerformanceStats GetStats() => _governor.GetStats();
public ResourceSnapshot GetResourceSnapshot() => _governor.GetStats().Resources;
public event Action<ResourceSnapshot>? OnResourceUpdate
{
add => _bus.Subscribe("greg.performance.ResourceSnapshot", p => value?.Invoke(_governor.GetStats().Resources));
remove => _bus.Unsubscribe("greg.performance.ResourceSnapshot", p => value?.Invoke(_governor.GetStats().Resources));
}
public PerformanceProfile Balanced => PerformanceProfile.Balanced;
public PerformanceProfile HighPerformance => PerformanceProfile.HighPerformance;
public PerformanceProfile LowEnd => PerformanceProfile.LowEnd;
}
+12
View File
@@ -0,0 +1,12 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregPlayerModule
{
private readonly GregApiContext _ctx;
internal GregPlayerModule(GregApiContext ctx) => _ctx = ctx;
private global::Il2Cpp.Player? Player => global::Il2Cpp.PlayerManager.instance?.playerClass;
public float GetReputation() => Player?.reputation ?? 0f;
public string GetPlayerName() => "Player"; // No playerName field in Player.cs dump
}
+13
View File
@@ -0,0 +1,13 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregSaveModule
{
private readonly GregApiContext _ctx;
internal GregSaveModule(GregApiContext ctx) => _ctx = ctx;
public void TriggerSave() => global::Il2Cpp.SaveSystem.SaveGame();
public void Set<T>(string key, T value) where T : notnull => _ctx.Persist.Set(key, value);
public T Get<T>(string key, T defaultValue = default!) where T : notnull => _ctx.Persist.Get(key, defaultValue);
public bool Has(string key) => _ctx.Persist.Has(key);
public void Delete(string key) => _ctx.Persist.Delete(key);
}
@@ -0,0 +1,9 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregServerModule
{
private readonly GregApiContext _ctx;
internal GregServerModule(GregApiContext ctx) => _ctx = ctx;
public bool Spawn(string serverId, int rackId) => true; // API Logic
}
+25
View File
@@ -0,0 +1,25 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregTimeModule
{
private readonly GregApiContext _ctx;
internal GregTimeModule(GregApiContext ctx) => _ctx = ctx;
public event Action? OnDayEnd
{
add => _ctx.EventBus.Subscribe("greg.lifecycle.OnEndOfTheDay", _ => value?.Invoke());
remove => _ctx.EventBus.Unsubscribe("greg.lifecycle.OnEndOfTheDay", _ => value?.Invoke());
}
public event Action<int, string>? OnSceneLoaded
{
add => _ctx.EventBus.Subscribe("greg.lifecycle.SceneLoaded", p => value?.Invoke((int)p.Data["buildIndex"], (string)p.Data["sceneName"]));
remove => _ctx.EventBus.Unsubscribe("greg.lifecycle.SceneLoaded", p => value?.Invoke((int)p.Data["buildIndex"], (string)p.Data["sceneName"]));
}
public event Action? OnGameSaved
{
add => _ctx.EventBus.Subscribe("greg.persistence.SaveGame", _ => value?.Invoke());
remove => _ctx.EventBus.Unsubscribe("greg.persistence.SaveGame", _ => value?.Invoke());
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace gregCore.PublicApi.Modules;
public sealed class GregUIModule
{
private readonly GregApiContext _ctx;
internal GregUIModule(GregApiContext ctx) => _ctx = ctx;
public void ShowToast(string message, float durationSeconds = 3f)
=> _ctx.EventBus.Publish("greg.ui.ShowToast", new EventPayload {
OccurredAtUtc = DateTime.UtcNow,
Data = new Dictionary<string, object> { ["message"] = message, ["duration"] = durationSeconds }
});
public void ShowError(string message) => ShowToast($"⚠ {message}", 5f);
}
+2
View File
@@ -0,0 +1,2 @@
namespace gregCore.PublicApi.Types;
public record DeviceInfo(string Id, string Name, string IpAddress, bool IsOnline);
+16
View File
@@ -0,0 +1,16 @@
namespace gregCore.PublicApi.Types;
public sealed class GregEventHandle : IDisposable
{
private readonly Action _unsubscribe;
private bool _disposed;
public GregEventHandle(Action unsubscribe) => _unsubscribe = unsubscribe;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_unsubscribe();
}
}
+2
View File
@@ -0,0 +1,2 @@
namespace gregCore.PublicApi.Types;
public record struct MoneyResult(float NewBalance, float Delta, bool Success);
+2
View File
@@ -0,0 +1,2 @@
namespace gregCore.PublicApi.Types;
public record ServerInfo(string Id, string Type, int RackId, int Slot, float PowerUsage);
+38
View File
@@ -0,0 +1,38 @@
using gregCore.PublicApi.Modules;
namespace gregCore.PublicApi;
public static class greg
{
internal static GregApiContext? _context;
private static GregApiContext Context => _context ?? throw new InvalidOperationException("gregCore nicht initialisiert.");
internal static gregCore.Infrastructure.Performance.GregPerformanceGovernor? _governor;
private static GregEconomyModule? _economy;
private static GregNetworkModule? _network;
private static GregPlayerModule? _player;
private static GregTimeModule? _time;
private static GregServerModule? _server;
private static GregFacilityModule? _facility;
private static GregUIModule? _ui;
private static GregSaveModule? _save;
private static GregAutomationModule? _automation;
private static GregNpcModule? _npc;
private static GregPerformanceModule? _performance;
public static GregEconomyModule Economy => _economy ??= new GregEconomyModule(Context);
public static GregNetworkModule Network => _network ??= new GregNetworkModule(Context);
public static GregPlayerModule Player => _player ??= new GregPlayerModule(Context);
public static GregTimeModule Time => _time ??= new GregTimeModule(Context);
public static GregServerModule Server => _server ??= new GregServerModule(Context);
public static GregFacilityModule Facility => _facility ??= new GregFacilityModule(Context);
public static GregUIModule UI => _ui ??= new GregUIModule(Context);
public static GregSaveModule Save => _save ??= new GregSaveModule(Context);
public static GregAutomationModule Automation => _automation ??= new GregAutomationModule(Context);
public static GregNpcModule Npc => _npc ??= new GregNpcModule(Context);
public static GregPerformanceModule Performance => _performance ??= new GregPerformanceModule(Context, _governor ?? throw new InvalidOperationException("Governor not initialized."));
public static string Version => typeof(greg).Assembly.GetName().Version?.ToString() ?? "1.0.0";
public static bool IsInitialized => _context != null;
}
-64
View File
@@ -1,64 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using MelonLoader;
using greg.Core;
namespace greg.Core.Scripting.Go;
/// <summary>
/// Go Language Bridge (v1.0.0.6)
/// Uses WebAssembly (WASM) to run Go-compiled mods within the .NET environment.
/// </summary>
public sealed class GoLanguageBridge : iGregLanguageBridge
{
private readonly MelonLogger.Instance _logger;
private readonly string _modDirectory;
private readonly List<gregRuntimeUnit> _units = new();
public string LanguageName => "Go (WASM)";
public IReadOnlyList<string> ScriptExtensions => new List<string> { ".wasm" };
public GoLanguageBridge(MelonLogger.Instance logger, string modDirectory)
{
_logger = logger;
_modDirectory = modDirectory;
if (!Directory.Exists(_modDirectory))
{
Directory.CreateDirectory(_modDirectory);
}
}
public void Initialize()
{
_logger.Msg("[gregCore] Go Language Bridge Initialized (Experimental WASM Support)");
}
public int LoadScripts()
{
// Placeholder for WASM loading logic
return 0;
}
public IReadOnlyList<gregRuntimeUnit> GetRuntimeUnits() => _units;
public bool SetUnitEnabled(string unitId, bool enabled) => false;
public int ReloadEnabledUnits()
{
_logger.Msg("This mod uses external vendored libraries, all references can be found at gregframework.eu/vendored-libs");
_logger.Msg("The Framework is using WASM-runtime for implementing Go Mod Support.");
return 0;
}
public void OnSceneLoaded(string sceneName) { }
public void OnUpdate(float deltaTime) { }
public void OnGui() { }
public void Shutdown()
{
_units.Clear();
}
}
-77
View File
@@ -1,77 +0,0 @@
using System;
using greg.Sdk;
using greg.Exporter;
namespace greg.Core.Scripting;
/// <summary>
/// High-level event bus for gregCore hooks.
/// Bridges string-based hooks (gregEventDispatcher) and typed events (ModFramework.Events).
/// </summary>
public static class GregHookBus
{
/// <summary>
/// Fires a string-based hook with a payload.
/// </summary>
public static void Fire(string hookName, object payload = null)
{
gregEventDispatcher.Emit(hookName, payload);
}
/// <summary>
/// Subscribes to a string-based hook.
/// </summary>
public static void On(string hookName, Action<object> handler, string modId = null)
{
gregEventDispatcher.On(hookName, handler, modId);
}
/// <summary>
/// Subscribes to ALL string-based hooks (for monitoring/debugging).
/// Note: This requires a modification to gregEventDispatcher to support global monitoring.
/// For now, we'll implement a simple callback mechanism.
/// </summary>
private static Action<string, object> _onAny;
[ThreadStatic]
private static bool _isNotifyingAny;
public static void OnAny(Action<string, object> handler)
{
_onAny += handler;
}
/// <summary>
/// Internally used by the dispatcher to notify global listeners.
/// </summary>
internal static void NotifyAny(string hookName, object payload)
{
if (_isNotifyingAny)
return;
var handlers = _onAny;
if (handlers == null)
return;
_isNotifyingAny = true;
try
{
foreach (Delegate handler in handlers.GetInvocationList())
{
try
{
((Action<string, object>)handler).Invoke(hookName, payload);
}
catch (Exception ex)
{
MelonLoader.MelonLogger.Warning($"[gregCore] OnAny handler '{handler.Method.DeclaringType?.Name}.{handler.Method.Name}' threw and was removed: {ex.Message}");
_onAny -= (Action<string, object>)handler;
}
}
}
finally
{
_isNotifyingAny = false;
}
}
}
-32
View File
@@ -1,32 +0,0 @@
using System.Collections.Generic;
namespace greg.Core.Scripting;
/// <summary>
/// Plugin-layer language bridge abstraction for script/native hosts.
/// </summary>
public interface iGregLanguageBridge
{
string LanguageName { get; }
IReadOnlyList<string> ScriptExtensions { get; }
void Initialize();
int LoadScripts();
IReadOnlyList<gregRuntimeUnit> GetRuntimeUnits();
bool SetUnitEnabled(string unitId, bool enabled);
int ReloadEnabledUnits();
void OnSceneLoaded(string sceneName);
void OnUpdate(float deltaTime);
void OnGui();
void Shutdown();
}
@@ -1,258 +0,0 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Jint;
using Jint.Native;
using MelonLoader;
using greg.Core;
using greg.Core.Scripting.Lua;
namespace greg.Core.Scripting.JS;
/// <summary>
/// TypeScript/JavaScript bridge with Jint integration for runtime JS execution.
/// </summary>
public sealed class TypeScriptJavaScriptLanguageBridge : iGregLanguageBridge
{
private static readonly string[] Extensions = { ".js", ".ts", ".mjs", ".cjs" };
private readonly MelonLogger.Instance _logger;
private readonly string _scriptsRoot;
private readonly List<gregRuntimeUnit> _runtimeUnits = new();
private readonly List<Engine> _engines = new();
private readonly List<Action<float>> _onUpdateHandlers = new();
private readonly List<Action> _onGuiHandlers = new();
public TypeScriptJavaScriptLanguageBridge(MelonLogger.Instance logger, string scriptsRoot)
{
_logger = logger;
_scriptsRoot = scriptsRoot;
}
public string LanguageName => "typescript/javascript";
public IReadOnlyList<string> ScriptExtensions => Extensions;
public void Initialize()
{
Directory.CreateDirectory(_scriptsRoot);
_logger.Msg($"gregCore TS/JS bridge ready: {_scriptsRoot}");
}
public int LoadScripts()
{
_runtimeUnits.Clear();
_engines.Clear();
_onUpdateHandlers.Clear();
_onGuiHandlers.Clear();
string[] js = Directory.GetFiles(_scriptsRoot, "*.js", SearchOption.AllDirectories);
string[] ts = Directory.GetFiles(_scriptsRoot, "*.ts", SearchOption.AllDirectories);
string[] mjs = Directory.GetFiles(_scriptsRoot, "*.mjs", SearchOption.AllDirectories);
string[] cjs = Directory.GetFiles(_scriptsRoot, "*.cjs", SearchOption.AllDirectories);
string[] loadedScripts = new string[js.Length + ts.Length + mjs.Length + cjs.Length];
int offset = 0;
js.CopyTo(loadedScripts, offset); offset += js.Length;
ts.CopyTo(loadedScripts, offset); offset += ts.Length;
mjs.CopyTo(loadedScripts, offset); offset += mjs.Length;
cjs.CopyTo(loadedScripts, offset);
for (int index = 0; index < loadedScripts.Length; index++)
{
string scriptPath = loadedScripts[index];
string relativePath = Path.GetRelativePath(_scriptsRoot, scriptPath).Replace('\\', '/');
string unitId = $"tsjs:{relativePath}";
bool enabled = gregModActivationService.IsEnabled(unitId, true);
var unit = new gregRuntimeUnit
{
Id = unitId,
DisplayName = relativePath,
Language = LanguageName,
Enabled = enabled,
SupportsHotReload = true,
IsNativeModule = false
};
_runtimeUnits.Add(unit);
if (enabled)
{
try
{
var engine = new Engine(options =>
{
options.LimitMemory(4000000);
options.TimeoutInterval(TimeSpan.FromSeconds(5));
});
var eventsObj = new Dictionary<string, object>
{
["onUpdate"] = new Action<JsValue>(handler =>
{
_onUpdateHandlers.Add(dt => engine.Invoke(handler, dt));
}),
["onGui"] = new Action<JsValue>(handler =>
{
_onGuiHandlers.Add(() => engine.Invoke(handler));
}),
["on"] = new Action<string, JsValue, string>((hook, handler, modId) =>
{
greg.Sdk.gregEventDispatcher.On(hook, payload => {
try {
engine.Invoke(handler, JsValue.FromObject(engine, payload));
} catch(Exception e) {
_logger.Error($"Event execution failed in JS: {e}");
}
}, string.IsNullOrEmpty(modId) ? unit.Id : modId);
}),
["off"] = new Action<string>(hook => {
greg.Sdk.gregEventDispatcher.UnregisterAll(unit.Id);
})
};
var hudObj = new Dictionary<string, object>
{
["updateJadeBox"] = new Action<string, string, object>((title, subHeader, entries) => {
var list = new List<greg.Sdk.Services.GregMetadataEntry>();
// Handle JS object/array to C# list
if (entries is IEnumerable<object> e) {
foreach(var item in e) {
if (item is IDictionary<string, object> d) {
string label = d.ContainsKey("label") ? d["label"]?.ToString() : "";
string val = d.ContainsKey("value") ? d["value"]?.ToString() : "";
ColorUtility.TryParseHtmlString(d.ContainsKey("color") ? d["color"]?.ToString() : "#FFFFFF", out var c);
list.Add(new greg.Sdk.Services.GregMetadataEntry(label, val, c));
}
}
}
greg.Sdk.Services.GregHudService.UpdateJadeBox(title, subHeader, list);
}),
["hideJadeBox"] = new Action(greg.Sdk.Services.GregHudService.HideJadeBox)
};
var targetObj = new Dictionary<string, object>
{
["getTargetInfo"] = new Func<float, object>(distance =>
{
var info = greg.Sdk.Services.GregTargetingService.GetTargetInfo(distance);
return new Dictionary<string, object> {
["type"] = info.TargetType.ToString(),
["name"] = info.Name,
["distance"] = info.Distance,
["hitPoint"] = new Dictionary<string, float> { ["x"] = info.HitPoint.x, ["y"] = info.HitPoint.y, ["z"] = info.HitPoint.z }
};
})
};
var metadataObj = new Dictionary<string, object>
{
["getMetadata"] = new Func<float, object>(distance =>
{
var info = greg.Sdk.Services.GregTargetingService.GetTargetInfo(distance);
var entries = greg.Sdk.Services.GregComponentMetadataService.GetMetadata(info);
var jsEntries = new List<object>();
foreach(var entry in entries) {
jsEntries.Add(new Dictionary<string, object> {
["label"] = entry.Label,
["value"] = entry.Value,
["color"] = $"#{ColorUtility.ToHtmlStringRGB(entry.ValueColor)}"
});
}
return new Dictionary<string, object> {
["title"] = info.TargetType == greg.Sdk.Services.GregTargetType.None ? "SCANNING..." : info.TargetType.ToString().ToUpper(),
["subHeader"] = info.TargetType == greg.Sdk.Services.GregTargetType.None ? "" : "TELEMETRY",
["entries"] = jsEntries
};
})
};
var gregObj = new Dictionary<string, object>
{
["log"] = new Action<string>(msg => _logger.Msg($"[tsjs:{unit.DisplayName}] {msg}")),
["events"] = eventsObj,
["hud"] = hudObj,
["target"] = targetObj,
["metadata"] = metadataObj
};
engine.SetValue("greg", gregObj);
engine.Execute(File.ReadAllText(scriptPath));
_engines.Add(engine);
}
catch (Exception ex)
{
_logger.Error($"Failed to load TS/JS script '{unit.DisplayName}': {ex}");
}
}
}
if (_runtimeUnits.Count > 0)
{
_logger.Msg("This mod uses external vendored libraries, all references can be found at gregframework.eu/vendored-libs");
_logger.Msg("The Framework is using Jint by Sebastien Ros for implementing JS/TS Mod Support - https://github.com/sebastienros/jint");
}
_logger.Msg($"gregCore TS/JS bridge evaluated {_engines.Count} active script(s).");
return _runtimeUnits.Count;
}
public IReadOnlyList<gregRuntimeUnit> GetRuntimeUnits() => _runtimeUnits;
public bool SetUnitEnabled(string unitId, bool enabled)
{
for (int i = 0; i < _runtimeUnits.Count; i++)
{
if (_runtimeUnits[i].Id.Equals(unitId, StringComparison.OrdinalIgnoreCase))
{
gregModActivationService.SetEnabled(unitId, enabled);
return true;
}
}
return false;
}
public int ReloadEnabledUnits() => LoadScripts();
public void OnSceneLoaded(string sceneName) { }
public void OnUpdate(float deltaTime)
{
foreach (var handler in _onUpdateHandlers)
{
try { handler(deltaTime); } catch { }
}
}
public void OnGui()
{
foreach (var handler in _onGuiHandlers)
{
try { handler(); } catch { }
}
}
public void Shutdown()
{
foreach (var unit in _runtimeUnits)
{
greg.Sdk.gregEventDispatcher.UnregisterAll(unit.Id);
}
_engines.Clear();
_onUpdateHandlers.Clear();
_onGuiHandlers.Clear();
_runtimeUnits.Clear();
}
}
-18
View File
@@ -1,18 +0,0 @@
using MoonSharp.Interpreter;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// Implement this interface to inject C#-backed API into every Lua <see cref="Script"/>
/// managed by <see cref="LuaLanguageBridge"/>. Modules are registered before scripts run.
/// </summary>
public interface iGregLuaModule
{
/// <summary>
/// Populate <paramref name="greg"/> (the <c>greg</c> global table) with functions and sub-tables
/// that Lua scripts can call.
/// </summary>
void Register(Script vm, Table greg);
}
-300
View File
@@ -1,300 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using MelonLoader;
using MoonSharp.Interpreter;
using greg.Core;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// Lua bridge backed by MoonSharp. Each discovered .lua file gets its own <see cref="Script"/>
/// with an isolated <c>greg</c> global table. Lifecycle hooks (<c>on_update</c>, <c>on_scene</c>)
/// are called per-frame by the bridge host when the unit is enabled.
/// </summary>
public sealed class LuaLanguageBridge : iGregLanguageBridge
{
private static readonly string[] Extensions = { ".lua" };
private readonly MelonLogger.Instance _logger;
private readonly string _scriptsRoot;
private readonly List<gregRuntimeUnit> _runtimeUnits = new();
private readonly List<LuaScriptState> _scripts = new();
private readonly List<iGregLuaModule> _modules = new();
public LuaLanguageBridge(MelonLogger.Instance logger, string scriptsRoot)
{
_logger = logger;
_scriptsRoot = scriptsRoot;
_modules.Add(new gregHooksLuaModule());
_modules.Add(new gregUnityLuaModule());
_modules.Add(new gregIoLuaModule());
_modules.Add(new gregInputLuaModule());
_modules.Add(new LuaModules.gregSdkLuaModule());
}
public string LanguageName => "lua";
public IReadOnlyList<string> ScriptExtensions => Extensions;
/// <summary>Register a C#-backed module that injects API into every new Lua <see cref="Script"/>.</summary>
public void RegisterModule(iGregLuaModule module)
{
_modules.Add(module);
}
public void Initialize()
{
Directory.CreateDirectory(_scriptsRoot);
_logger.Msg($"gregCore Lua bridge ready (MoonSharp): {_scriptsRoot}");
}
public int LoadScripts()
{
ShutdownScripts();
_runtimeUnits.Clear();
string[] files;
try
{
files = Directory.GetFiles(_scriptsRoot, "*.lua", SearchOption.AllDirectories);
}
catch (Exception ex)
{
CrashLog.LogException("LuaLanguageBridge.LoadScripts.Discovery", ex);
return 0;
}
for (int i = 0; i < files.Length; i++)
{
string scriptPath = files[i];
string relativePath = Path.GetRelativePath(_scriptsRoot, scriptPath).Replace('\\', '/');
string unitId = $"lua:{relativePath}";
bool enabled = gregModActivationService.IsEnabled(unitId, true);
_runtimeUnits.Add(new gregRuntimeUnit
{
Id = unitId,
DisplayName = relativePath,
Language = LanguageName,
Enabled = enabled,
SupportsHotReload = true,
IsNativeModule = false
});
if (!enabled)
{
_scripts.Add(null);
continue;
}
try
{
var state = CompileAndRun(scriptPath, unitId);
_scripts.Add(state);
}
catch (Exception ex)
{
_logger.Warning($"Lua script failed to load: {relativePath} — {ex.Message}");
CrashLog.LogException($"LuaLanguageBridge.Load[{unitId}]", ex);
_scripts.Add(null);
}
}
if (_runtimeUnits.Count > 0)
{
_logger.Msg("This mod uses external vendored libraries, all references can be found at gregframework.eu/vendored-libs");
_logger.Msg("The Framework is using MoonSharp by Marco Cecchi for implementing Lua Mod Support - https://github.com/moonsharp-devs/moonsharp");
}
_logger.Msg($"gregCore Lua bridge loaded {_runtimeUnits.Count} script(s).");
return _runtimeUnits.Count;
}
public IReadOnlyList<gregRuntimeUnit> GetRuntimeUnits() => _runtimeUnits;
public bool SetUnitEnabled(string unitId, bool enabled)
{
if (string.IsNullOrWhiteSpace(unitId) || !unitId.StartsWith("lua:", StringComparison.OrdinalIgnoreCase))
return false;
for (int i = 0; i < _runtimeUnits.Count; i++)
{
var unit = _runtimeUnits[i];
if (!string.Equals(unit.Id, unitId, StringComparison.OrdinalIgnoreCase))
continue;
gregModActivationService.SetEnabled(unitId, enabled);
_runtimeUnits[i] = new gregRuntimeUnit
{
Id = unit.Id,
DisplayName = unit.DisplayName,
Language = unit.Language,
Enabled = enabled,
SupportsHotReload = unit.SupportsHotReload,
IsNativeModule = unit.IsNativeModule
};
return true;
}
return false;
}
public int ReloadEnabledUnits() => LoadScripts();
public void OnSceneLoaded(string sceneName)
{
for (int i = 0; i < _scripts.Count; i++)
{
if (i >= _runtimeUnits.Count || !_runtimeUnits[i].Enabled)
continue;
var state = _scripts[i];
if (state?.OnScene == null)
continue;
try
{
state.Vm.Call(state.OnScene, sceneName);
}
catch (Exception ex)
{
_logger.Warning($"Lua on_scene error [{_runtimeUnits[i].Id}]: {ex.Message}");
CrashLog.LogException($"LuaLanguageBridge.OnScene[{_runtimeUnits[i].Id}]", ex);
}
}
}
public void OnUpdate(float deltaTime)
{
for (int i = 0; i < _scripts.Count; i++)
{
if (i >= _runtimeUnits.Count || !_runtimeUnits[i].Enabled)
continue;
var state = _scripts[i];
if (state == null) continue;
// 1. Classic global on_update(dt)
if (state.OnUpdate != null)
{
try { state.Vm.Call(state.OnUpdate, deltaTime); }
catch (Exception ex) { _logger.Warning($"Lua on_update error [{_runtimeUnits[i].Id}]: {ex.Message}"); }
}
// 2. New greg.events.on_update handlers
try
{
var internalUpdate = state.Vm.Globals.Get("greg").Table.Get("_internal_update");
if (internalUpdate.Type == DataType.Function)
state.Vm.Call(internalUpdate, (double)deltaTime);
}
catch { }
}
}
/// <summary>Call from Unity OnGUI — dispatches <c>on_gui()</c> to enabled Lua scripts.</summary>
public void OnGui()
{
for (int i = 0; i < _scripts.Count; i++)
{
if (i >= _runtimeUnits.Count || !_runtimeUnits[i].Enabled)
continue;
var state = _scripts[i];
if (state == null) continue;
// 1. Classic global on_gui()
if (state.OnGui != null)
{
try { state.Vm.Call(state.OnGui); }
catch (Exception ex) { _logger.Warning($"Lua on_gui error [{_runtimeUnits[i].Id}]: {ex.Message}"); }
}
// 2. New greg.events.on_gui handlers
try
{
var internalGui = state.Vm.Globals.Get("greg").Table.Get("_internal_gui");
if (internalGui.Type == DataType.Function)
state.Vm.Call(internalGui);
}
catch { }
}
}
public void Shutdown()
{
ShutdownScripts();
_runtimeUnits.Clear();
}
private LuaScriptState CompileAndRun(string filePath, string unitId)
{
var vm = new Script(CoreModules.Preset_SoftSandbox);
var greg = new Table(vm);
vm.Globals["greg"] = greg;
// greg.log
greg["log"] = (Action<string>)(msg => _logger.Msg($"[lua:{unitId}] {msg}"));
greg["warn"] = (Action<string>)(msg => _logger.Warning($"[lua:{unitId}] {msg}"));
greg["error"] = (Action<string>)(msg =>
{
_logger.Error($"[lua:{unitId}] {msg}");
CrashLog.LogError($"lua:{unitId}", msg);
});
// greg.paths
var paths = new Table(vm);
paths["scripts"] = _scriptsRoot;
paths["userdata"] = MelonLoader.Utils.MelonEnvironment.UserDataDirectory;
paths["mods"] = MelonLoader.Utils.MelonEnvironment.ModsDirectory;
paths["game"] = MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
greg["paths"] = paths;
// Let registered modules inject their API surfaces
for (int m = 0; m < _modules.Count; m++)
{
try
{
_modules[m].Register(vm, greg);
}
catch (Exception ex)
{
CrashLog.LogException($"LuaLanguageBridge.Module[{_modules[m].GetType().Name}]", ex);
}
}
string code = File.ReadAllText(filePath);
vm.DoString(code, codeFriendlyName: unitId);
var onUpdate = vm.Globals.Get("on_update");
var onScene = vm.Globals.Get("on_scene");
var onGui = vm.Globals.Get("on_gui");
return new LuaScriptState
{
Vm = vm,
OnUpdate = onUpdate.Type == DataType.Function ? onUpdate : null,
OnScene = onScene.Type == DataType.Function ? onScene : null,
OnGui = onGui.Type == DataType.Function ? onGui : null
};
}
private void ShutdownScripts()
{
_scripts.Clear();
}
private sealed class LuaScriptState
{
public Script Vm;
public DynValue OnUpdate;
public DynValue OnScene;
public DynValue OnGui;
}
}
@@ -1,269 +0,0 @@
using System;
using System.Collections.Generic;
using greg.Core.Hooks;
using greg.Sdk;
using MoonSharp.Interpreter;
using MelonLoader;
using greg.Core;
using greg.Core.Scripting.Lua;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// Connects Lua scripts to the gregCore event/hook system.
/// <list type="bullet">
/// <item><c>greg.on(hookName, fn)</c> — subscribe to <see cref="gregEventDispatcher"/> events</item>
/// <item><c>greg.off(hookName)</c> — unsubscribe all handlers for a hook</item>
/// <item><c>greg.hook.before(hookName, fn)</c> — Harmony prefix via <see cref="HookBinder"/></item>
/// <item><c>greg.hook.after(hookName, fn)</c> — Harmony postfix via <see cref="HookBinder"/></item>
/// <item><c>greg.emit(hookName, payload)</c> — emit custom event</item>
/// </list>
/// </summary>
public sealed class gregHooksLuaModule : iGregLuaModule
{
private MelonLogger.Instance _logger;
public gregHooksLuaModule(MelonLogger.Instance logger = null)
{
_logger = logger ?? new MelonLogger.Instance("gregCore");
}
public void Register(Script vm, Table greg)
{
var registeredEventHandlers = new List<(string hookName, Action<object> handler)>();
var registeredHookHandlers = new List<(string hookName, Action<HookContext> handler, bool isBefore)>();
// greg.on(hookName, fn)
greg["on"] = (Action<string, Closure>)((hookName, fn) =>
{
if (string.IsNullOrWhiteSpace(hookName) || fn == null) return;
Action<object> handler = payload =>
{
try { vm.Call(fn, PayloadToLua(vm, payload)); }
catch (Exception ex) { CrashLog.LogException($"lua:greg.on[{hookName}]", ex); }
};
registeredEventHandlers.Add((hookName, handler));
gregEventDispatcher.On(hookName, handler);
});
// greg.events sub-table
var eventsTable = new Table(vm);
greg["events"] = eventsTable;
var updateHandlers = new List<Closure>();
var guiHandlers = new List<Closure>();
eventsTable["on"] = (Action<string, Closure, string>)((hookName, fn, modId) =>
{
if (string.IsNullOrWhiteSpace(hookName) || fn == null) return;
Action<object> handler = payload =>
{
try { vm.Call(fn, PayloadToLua(vm, payload)); }
catch (Exception ex) { CrashLog.LogException($"lua:greg.events.on[{hookName}]", ex); }
};
registeredEventHandlers.Add((hookName, handler));
gregEventDispatcher.On(hookName, handler, modId);
});
eventsTable["on_update"] = (Action<Closure>)(fn => { if (fn != null) updateHandlers.Add(fn); });
eventsTable["on_gui"] = (Action<Closure>)(fn => { if (fn != null) guiHandlers.Add(fn); });
eventsTable["off"] = greg["off"];
// Expose handlers to the bridge for execution
greg["_internal_update"] = (Action<double>)(dt => {
for (int i = 0; i < updateHandlers.Count; i++) {
try { vm.Call(updateHandlers[i], dt); } catch { }
}
});
greg["_internal_gui"] = (Action)(() => {
for (int i = 0; i < guiHandlers.Count; i++) {
try { vm.Call(guiHandlers[i]); } catch { }
}
});
// greg.registry sub-table
var registryTable = new Table(vm);
greg["registry"] = registryTable;
registryTable["registerMod"] = (Action<Table>)(meta =>
{
// Stub for mod registration
string id = meta["id"]?.ToString() ?? "unknown";
_logger.Msg($"[lua] Registered mod: {id}");
});
// greg.off(hookName) — removes all Lua handlers for this hook
greg["off"] = (Action<string>)(hookName =>
{
if (string.IsNullOrWhiteSpace(hookName)) return;
for (int i = registeredEventHandlers.Count - 1; i >= 0; i--)
{
var (name, handler) = registeredEventHandlers[i];
if (string.Equals(name, hookName, StringComparison.Ordinal))
{
gregEventDispatcher.Off(hookName, handler);
registeredEventHandlers.RemoveAt(i);
}
}
});
// greg.payload sub-table
var payloadTable = new Table(vm);
greg["payload"] = payloadTable;
payloadTable["get"] = (Func<object, string, object, object>)((p, field, fallback) =>
{
return gregPayload.Get<object>(p, field, fallback);
});
// greg.framework sub-table
var frameworkTable = new MoonSharp.Interpreter.Table(vm);
greg["framework"] = frameworkTable;
frameworkTable["publish_tick"] = (Action<MoonSharp.Interpreter.Table>)(tick =>
{
float dt = tick["deltaTime"] is double d ? (float)d : 0f;
int frame = tick["frame"] is double f ? (int)f : 0;
global::greg.Exporter.ModFramework.Events.Publish(new global::greg.Exporter.ModTickEvent(dt, frame));
});
// greg.emit(hookName, payload)
greg["emit"] = (Action<string, DynValue>)((hookName, payload) =>
{
if (string.IsNullOrWhiteSpace(hookName)) return;
object nativePayload = payload?.Type == DataType.Table ? LuaTableToDict(payload.Table) : payload?.ToObject();
gregEventDispatcher.Emit(hookName, nativePayload);
});
// greg.hook sub-table
var hook = new Table(vm);
greg["hook"] = hook;
// greg.hook.before(hookName, fn)
hook["before"] = (Action<string, Closure>)((hookName, fn) =>
{
if (string.IsNullOrWhiteSpace(hookName) || fn == null) return;
Action<HookContext> handler = ctx =>
{
try
{
var ctxTable = HookContextToLua(vm, ctx);
vm.Call(fn, ctxTable);
}
catch (Exception ex) { CrashLog.LogException($"lua:greg.hook.before[{hookName}]", ex); }
};
registeredHookHandlers.Add((hookName, handler, true));
HookBinder.OnBefore(hookName, handler);
});
// greg.hook.after(hookName, fn)
hook["after"] = (Action<string, Closure>)((hookName, fn) =>
{
if (string.IsNullOrWhiteSpace(hookName) || fn == null) return;
Action<HookContext> handler = ctx =>
{
try
{
var ctxTable = HookContextToLua(vm, ctx);
vm.Call(fn, ctxTable);
}
catch (Exception ex) { CrashLog.LogException($"lua:greg.hook.after[{hookName}]", ex); }
};
registeredHookHandlers.Add((hookName, handler, false));
HookBinder.OnAfter(hookName, handler);
});
// greg.hook.off(hookName)
hook["off"] = (Action<string>)(hookName =>
{
if (string.IsNullOrWhiteSpace(hookName)) return;
HookBinder.Unregister(hookName);
registeredHookHandlers.RemoveAll(x => string.Equals(x.hookName, hookName, StringComparison.Ordinal));
});
}
private static DynValue PayloadToLua(Script vm, object payload)
{
if (payload == null)
return DynValue.Nil;
if (payload is string s)
return DynValue.NewString(s);
if (payload is int i)
return DynValue.NewNumber(i);
if (payload is double d)
return DynValue.NewNumber(d);
if (payload is float f)
return DynValue.NewNumber(f);
if (payload is bool b)
return DynValue.NewBoolean(b);
// Anonymous types / rich payloads: flatten public properties to Lua table
var t = new Table(vm);
try
{
foreach (var prop in payload.GetType().GetProperties())
{
try
{
var val = prop.GetValue(payload);
if (val == null) continue;
if (val is string sv) t[prop.Name] = sv;
else if (val is int iv) t[prop.Name] = iv;
else if (val is double dv) t[prop.Name] = dv;
else if (val is float fv) t[prop.Name] = (double)fv;
else if (val is bool bv) t[prop.Name] = bv;
else if (val is uint uv) t[prop.Name] = (double)uv;
else t[prop.Name] = val.ToString();
}
catch { }
}
}
catch { }
return DynValue.NewTable(t);
}
private static Table HookContextToLua(Script vm, HookContext ctx)
{
var t = new Table(vm);
t["hook_name"] = ctx.HookName;
t["method_name"] = ctx.Method?.Name ?? "";
t["type_name"] = ctx.Method?.DeclaringType?.FullName ?? "";
t["has_instance"] = ctx.Instance != null;
t["arg_count"] = ctx.Arguments?.Length ?? 0;
if (ctx.Instance != null)
{
// Store a handle for the instance so Lua can use greg.unity on it
t["instance_handle"] = gregLuaObjectHandleRegistry.Register(ctx.Instance);
}
if (ctx.Arguments != null)
{
for (int i = 0; i < ctx.Arguments.Length; i++)
{
if (ctx.Arguments[i] != null)
t[$"arg{i}_handle"] = gregLuaObjectHandleRegistry.Register(ctx.Arguments[i]);
}
}
return t;
}
private static Dictionary<string, object> LuaTableToDict(Table table)
{
var dict = new Dictionary<string, object>();
foreach (var pair in table.Pairs)
{
string key = pair.Key.Type == DataType.String ? pair.Key.String : pair.Key.ToString();
dict[key] = pair.Value.ToObject();
}
return dict;
}
}
@@ -1,156 +0,0 @@
using System;
using MoonSharp.Interpreter;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// Keyboard/mouse input API for Lua: <c>greg.input.*</c>.
/// Uses Unity InputSystem (Keyboard.current, Mouse.current) via dynamic to avoid CI ref issues.
/// </summary>
public sealed class gregInputLuaModule : iGregLuaModule
{
public void Register(Script vm, Table greg)
{
var input = new Table(vm);
greg["input"] = input;
// greg.input.key_pressed(keyName) -> bool (was pressed THIS frame)
input["key_pressed"] = (Func<string, bool>)(keyName =>
{
try
{
dynamic kb = GetKeyboard();
if (kb == null) return false;
var key = FindKey(kb, keyName);
return key?.wasPressedThisFrame ?? false;
}
catch { return false; }
});
// greg.input.key_down(keyName) -> bool (is currently held)
input["key_down"] = (Func<string, bool>)(keyName =>
{
try
{
dynamic kb = GetKeyboard();
if (kb == null) return false;
var key = FindKey(kb, keyName);
return key?.isPressed ?? false;
}
catch { return false; }
});
// greg.input.ctrl() -> bool
input["ctrl"] = (Func<bool>)(() =>
{
try
{
dynamic kb = GetKeyboard();
if (kb == null) return false;
return kb.leftCtrlKey.isPressed || kb.rightCtrlKey.isPressed;
}
catch { return false; }
});
// greg.input.shift() -> bool
input["shift"] = (Func<bool>)(() =>
{
try
{
dynamic kb = GetKeyboard();
if (kb == null) return false;
return kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed;
}
catch { return false; }
});
// greg.input.alt() -> bool
input["alt"] = (Func<bool>)(() =>
{
try
{
dynamic kb = GetKeyboard();
if (kb == null) return false;
return kb.leftAltKey.isPressed || kb.rightAltKey.isPressed;
}
catch { return false; }
});
}
private static object GetKeyboard()
{
try
{
var type = Type.GetType("UnityEngine.InputSystem.Keyboard, Unity.InputSystem");
if (type == null) return null;
var prop = type.GetProperty("current", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
return prop?.GetValue(null);
}
catch { return null; }
}
private static dynamic FindKey(dynamic kb, string name)
{
if (string.IsNullOrEmpty(name)) return null;
var upper = name.ToUpperInvariant();
return upper switch
{
"F1" => kb.f1Key,
"F2" => kb.f2Key,
"F3" => kb.f3Key,
"F4" => kb.f4Key,
"F5" => kb.f5Key,
"F6" => kb.f6Key,
"F7" => kb.f7Key,
"F8" => kb.f8Key,
"F9" => kb.f9Key,
"F10" => kb.f10Key,
"F11" => kb.f11Key,
"F12" => kb.f12Key,
"ESCAPE" or "ESC" => kb.escapeKey,
"SPACE" => kb.spaceKey,
"ENTER" or "RETURN" => kb.enterKey,
"TAB" => kb.tabKey,
"BACKSPACE" => kb.backspaceKey,
"DELETE" => kb.deleteKey,
"INSERT" => kb.insertKey,
"HOME" => kb.homeKey,
"END" => kb.endKey,
"PAGEUP" => kb.pageUpKey,
"PAGEDOWN" => kb.pageDownKey,
"UP" => kb.upArrowKey,
"DOWN" => kb.downArrowKey,
"LEFT" => kb.leftArrowKey,
"RIGHT" => kb.rightArrowKey,
_ => TryFindByCharacter(kb, upper)
};
}
private static dynamic TryFindByCharacter(dynamic kb, string upper)
{
try
{
if (upper.Length == 1)
{
char c = upper[0];
if (c >= 'A' && c <= 'Z')
{
return c switch
{
'A' => kb.aKey, 'B' => kb.bKey, 'C' => kb.cKey, 'D' => kb.dKey,
'E' => kb.eKey, 'F' => kb.fKey, 'G' => kb.gKey, 'H' => kb.hKey,
'I' => kb.iKey, 'J' => kb.jKey, 'K' => kb.kKey, 'L' => kb.lKey,
'M' => kb.mKey, 'N' => kb.nKey, 'O' => kb.oKey, 'P' => kb.pKey,
'Q' => kb.qKey, 'R' => kb.rKey, 'S' => kb.sKey, 'T' => kb.tKey,
'U' => kb.uKey, 'V' => kb.vKey, 'W' => kb.wKey, 'X' => kb.xKey,
'Y' => kb.yKey, 'Z' => kb.zKey,
_ => null
};
}
}
}
catch { }
return null;
}
}
@@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using MoonSharp.Interpreter;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// File system API for Lua: <c>greg.io.*</c>.
/// Read/write files, check existence, list directories.
/// Paths are sandboxed — only game dir and userdata are accessible.
/// </summary>
public sealed class gregIoLuaModule : iGregLuaModule
{
public void Register(Script vm, Table greg)
{
var io = new Table(vm);
greg["io"] = io;
io["read_file"] = (Func<string, string>)(path =>
{
try { return File.Exists(path) ? File.ReadAllText(path) : null; }
catch { return null; }
});
io["read_lines"] = (Func<string, Table>)(path =>
{
var t = new Table(vm);
try
{
if (!File.Exists(path)) return t;
var lines = File.ReadAllLines(path);
for (int i = 0; i < lines.Length; i++)
t.Append(DynValue.NewString(lines[i]));
}
catch { }
return t;
});
// Read first N lines from a file (useful for log scanning)
io["read_head"] = (Func<string, int, Table>)((path, maxLines) =>
{
var t = new Table(vm);
try
{
if (!File.Exists(path)) return t;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
int count = 0;
while (!reader.EndOfStream && count < maxLines)
{
var line = reader.ReadLine();
if (line != null) t.Append(DynValue.NewString(line));
count++;
}
}
catch { }
return t;
});
io["write_file"] = (Action<string, string>)((path, content) =>
{
try
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(path, content ?? "");
}
catch { }
});
io["file_exists"] = (Func<string, bool>)(path =>
{
try { return File.Exists(path); }
catch { return false; }
});
io["dir_exists"] = (Func<string, bool>)(path =>
{
try { return Directory.Exists(path); }
catch { return false; }
});
io["list_files"] = (Func<string, string, bool, Table>)((path, pattern, recursive) =>
{
var t = new Table(vm);
try
{
if (!Directory.Exists(path)) return t;
var opt = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var files = Directory.GetFiles(path, pattern ?? "*.*", opt);
for (int i = 0; i < Math.Min(files.Length, 256); i++)
t.Append(DynValue.NewString(files[i]));
}
catch { }
return t;
});
io["path_combine"] = (Func<string, string, string>)((a, b) =>
{
try { return Path.Combine(a ?? "", b ?? ""); }
catch { return ""; }
});
io["path_filename"] = (Func<string, string>)(path =>
{
try { return Path.GetFileName(path); }
catch { return ""; }
});
io["path_ext"] = (Func<string, string>)(path =>
{
try { return Path.GetExtension(path)?.ToLowerInvariant() ?? ""; }
catch { return ""; }
});
}
}
@@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// Thread-safe registry mapping integer handles to live .NET/Il2Cpp objects.
/// Lua scripts use these handles to reference game objects without holding direct managed references.
/// Handles are periodically pruned when objects become null (destroyed Unity objects).
/// </summary>
internal static class gregLuaObjectHandleRegistry
{
private static readonly Dictionary<int, WeakReference<object>> Handles = new();
private static int _nextHandle;
public static int Register(object obj)
{
if (obj == null) return 0;
int handle = Interlocked.Increment(ref _nextHandle);
lock (Handles) { Handles[handle] = new WeakReference<object>(obj); }
return handle;
}
public static object Resolve(int handle)
{
if (handle <= 0) return null;
lock (Handles)
{
if (!Handles.TryGetValue(handle, out var wr)) return null;
if (wr.TryGetTarget(out var obj)) return obj;
Handles.Remove(handle);
return null;
}
}
public static T Resolve<T>(int handle) where T : class
{
return Resolve(handle) as T;
}
public static void Release(int handle)
{
if (handle <= 0) return;
lock (Handles) { Handles.Remove(handle); }
}
public static void Prune()
{
lock (Handles)
{
var dead = new List<int>();
foreach (var kv in Handles)
{
if (!kv.Value.TryGetTarget(out _))
dead.Add(kv.Key);
}
for (int i = 0; i < dead.Count; i++)
Handles.Remove(dead[i]);
}
}
public static void Clear()
{
lock (Handles) { Handles.Clear(); }
}
}
@@ -1,101 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MoonSharp.Interpreter;
using UnityEngine;
using greg.Sdk.Services;
namespace greg.Core.Scripting.Lua.LuaModules;
public sealed class gregSdkLuaModule : iGregLuaModule
{
public void Register(Script vm, Table greg)
{
var sdk = new Table(vm);
greg["sdk"] = sdk;
// --- Targeting Service ---
var targeting = new Table(vm);
sdk["targeting"] = targeting;
targeting["get_target_info"] = (Func<float, Table>)(maxDist => {
var info = GregTargetingService.GetTargetInfo(maxDist);
var t = new Table(vm);
t["type"] = info.TargetType.ToString();
t["name"] = info.Name;
t["distance"] = info.Distance;
t["hit_point"] = new Table(vm) { ["x"] = info.HitPoint.x, ["y"] = info.HitPoint.y, ["z"] = info.HitPoint.z };
return t;
});
// --- Metadata Service ---
var metadata = new Table(vm);
sdk["metadata"] = metadata;
metadata["get_metadata"] = (Func<float, Table>)(maxDist => {
var info = GregTargetingService.GetTargetInfo(maxDist);
var entries = GregComponentMetadataService.GetMetadata(info);
var result = new Table(vm);
result["title"] = info.TargetType == GregTargetType.None ? "SCANNING..." : info.TargetType.ToString().ToUpper();
result["sub_header"] = info.TargetType == GregTargetType.None ? "" : "TELEMETRY";
var list = new Table(vm);
for (int i = 0; i < entries.Count; i++)
{
var entry = entries[i];
var et = new Table(vm);
et["label"] = entry.Label;
et["value"] = entry.Value;
et["color"] = $"#{ColorUtility.ToHtmlStringRGB(entry.ValueColor)}";
list[i + 1] = et;
}
result["entries"] = list;
return result;
});
// --- HUD Service ---
var hud = new Table(vm);
sdk["hud"] = hud;
hud["update_jade_box"] = (Action<string, string, Table>)((title, subHeader, entriesTable) => {
var entries = new List<GregMetadataEntry>();
foreach (var pair in entriesTable.Pairs)
{
var et = pair.Value.Table;
if (et == null) continue;
string label = et.Get("label").String ?? "";
string value = et.Get("value").String ?? "";
string hexColor = et.Get("color").String ?? "#FFFFFF";
ColorUtility.TryParseHtmlString(hexColor, out var color);
entries.Add(new GregMetadataEntry(label, value, color));
}
GregHudService.UpdateJadeBox(title, subHeader, entries);
});
hud["hide_jade_box"] = (Action)(() => GregHudService.HideJadeBox());
// --- UI Hijack Service (v12) ---
var ui = new Table(vm);
sdk["ui"] = ui;
ui["hijack_canvas"] = (Action<string, bool>)((name, active) => {
var canvases = GameObject.FindObjectsOfType<Canvas>(true);
foreach (var c in canvases)
{
if (c.name == name)
{
c.gameObject.SetActive(active);
break;
}
}
});
ui["create_modern_canvas"] = (Func<string, int, DynValue>)((name, sorting) => {
var canvas = GregUiService.CreateCanvas(name, sorting);
return UserData.Create(canvas);
});
}
}
@@ -1,858 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using Il2Cpp;
using MoonSharp.Interpreter;
using UnityEngine;
using greg.Core;
namespace greg.Core.Scripting.Lua;
/// <summary>
/// Generic Unity/Il2Cpp API surface for Lua via integer handles.
/// All heavy Unity work stays in C#; Lua drives policy via handle IDs.
/// </summary>
public sealed class gregUnityLuaModule : iGregLuaModule
{
public void Register(Script vm, Table greg)
{
var unity = new Table(vm);
greg["unity"] = unity;
RegisterFinders(vm, unity);
RegisterComponents(vm, unity);
RegisterProperties(vm, unity);
RegisterTransform(vm, unity);
RegisterInstantiate(vm, unity);
RegisterMaterial(vm, unity);
RegisterTmpro(vm, unity);
RegisterTextMesh(vm, unity);
RegisterPhysics(vm, unity);
RegisterGameObject(vm, unity);
var color = new Table(vm);
greg["color"] = color;
RegisterColor(vm, color);
var config = new Table(vm);
greg["config"] = config;
RegisterConfig(vm, config);
var gui = new Table(vm);
greg["gui"] = gui;
RegisterGui(vm, gui);
var hud = new Table(vm);
greg["hud"] = hud;
RegisterHud(vm, hud);
var target = new Table(vm);
greg["target"] = target;
RegisterTarget(vm, target);
}
private static void RegisterHud(Script vm, Table hud)
{
hud["begin_panel"] = (Action<string, Table>)((id, rect) =>
{
float x = rect["x"] is double dx ? (float)dx : 0f;
float y = rect["y"] is double dy ? (float)dy : 0f;
float w = rect["w"] is double dw ? (float)dw : 200f;
float h = rect["h"] is double dh ? (float)dh : 100f;
gregGameHooks.GuiBeginPanel(id, x, y, w, h);
});
hud["label"] = (Action<string>)(text => gregGameHooks.GuiLabel(text));
hud["end_panel"] = (Action)(() => gregGameHooks.GuiEndPanel());
}
private static void RegisterTarget(Script vm, Table target)
{
target["raycast_forward"] = (Func<double, Table>)(distance =>
{
var hit = gregGameHooks.RaycastForward((float)distance);
if (hit == null) return null;
var t = new Table(vm);
t["name"] = hit.Value.Name;
t["distance"] = (double)hit.Value.Distance;
var pt = new Table(vm);
pt["x"] = (double)hit.Value.Point.x;
pt["y"] = (double)hit.Value.Point.y;
pt["z"] = (double)hit.Value.Point.z;
t["point"] = pt;
if (hit.Value.Entity != null)
{
t["entity_handle"] = gregLuaObjectHandleRegistry.Register(hit.Value.Entity);
}
return t;
});
}
private static void RegisterFinders(Script vm, Table unity)
{
// greg.unity.find(typeName) -> table of handles
unity["find"] = (Func<string, Table>)(typeName =>
{
var result = new Table(vm);
try
{
var type = ResolveIl2CppType(typeName);
if (type == null) return result;
var objects = UnityEngine.Object.FindObjectsOfType(Il2CppInterop.Runtime.Il2CppType.From(type));
if (objects == null) return result;
for (int i = 0; i < objects.Count; i++)
{
var obj = objects[i];
if (obj != null)
result.Append(DynValue.NewNumber(gregLuaObjectHandleRegistry.Register(obj)));
}
}
catch (Exception ex) { CrashLog.LogException("lua:greg.unity.find", ex); }
return result;
});
// greg.unity.find_child(handle, childName) -> handle or 0
unity["find_child"] = (Func<int, string, int>)((handle, childName) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Transform t = null;
if (obj is GameObject go) t = go.transform;
else if (obj is Component c) t = c.transform;
if (t == null) return 0;
var child = t.Find(childName);
return child != null ? gregLuaObjectHandleRegistry.Register(child.gameObject) : 0;
}
catch { return 0; }
});
// greg.unity.handle_alive(handle) -> bool
unity["handle_alive"] = (Func<int, bool>)(handle =>
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj == null) return false;
if (obj is UnityEngine.Object uo) return uo != null;
return true;
});
// greg.unity.release(handle)
unity["release"] = (Action<int>)(handle => gregLuaObjectHandleRegistry.Release(handle));
}
private static void RegisterComponents(Script vm, Table unity)
{
// greg.unity.get_component(handle, typeName) -> handle or 0
unity["get_component"] = (Func<int, string, int>)((handle, typeName) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
GameObject go = null;
if (obj is GameObject g) go = g;
else if (obj is Component c) go = c.gameObject;
if (go == null) return 0;
var type = ResolveIl2CppType(typeName);
if (type == null) return 0;
var comp = go.GetComponent(Il2CppInterop.Runtime.Il2CppType.From(type));
return comp != null ? gregLuaObjectHandleRegistry.Register(comp) : 0;
}
catch { return 0; }
});
// greg.unity.add_component(handle, typeName) -> handle or 0
unity["add_component"] = (Func<int, string, int>)((handle, typeName) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
GameObject go = null;
if (obj is GameObject g) go = g;
else if (obj is Component c) go = c.gameObject;
if (go == null) return 0;
var type = ResolveIl2CppType(typeName);
if (type == null) return 0;
var comp = go.AddComponent(Il2CppInterop.Runtime.Il2CppType.From(type));
return comp != null ? gregLuaObjectHandleRegistry.Register(comp) : 0;
}
catch { return 0; }
});
}
private static void RegisterProperties(Script vm, Table unity)
{
// greg.unity.get_string(handle, memberName) -> string or nil
unity["get_string"] = (Func<int, string, string>)((handle, name) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj == null) return null;
var val = GetMemberValue(obj, name);
return val?.ToString();
}
catch { return null; }
});
// greg.unity.get_number(handle, memberName) -> number or 0
unity["get_number"] = (Func<int, string, double>)((handle, name) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj == null) return 0;
var val = GetMemberValue(obj, name);
if (val is float f) return f;
if (val is double d) return d;
if (val is int i) return i;
if (val is uint u) return u;
return 0;
}
catch { return 0; }
});
// greg.unity.get_bool(handle, memberName) -> bool
unity["get_bool"] = (Func<int, string, bool>)((handle, name) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj == null) return false;
var val = GetMemberValue(obj, name);
return val is true;
}
catch { return false; }
});
// greg.unity.set_string(handle, memberName, value)
unity["set_string"] = (Action<int, string, string>)((handle, name, value) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj != null) SetMemberValue(obj, name, value);
}
catch { }
});
// greg.unity.set_number(handle, memberName, value)
unity["set_number"] = (Action<int, string, double>)((handle, name, value) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj == null) return;
var member = FindMember(obj.GetType(), name);
if (member is FieldInfo fi)
{
if (fi.FieldType == typeof(float)) fi.SetValue(obj, (float)value);
else if (fi.FieldType == typeof(int)) fi.SetValue(obj, (int)value);
else fi.SetValue(obj, value);
}
else if (member is PropertyInfo pi && pi.CanWrite)
{
if (pi.PropertyType == typeof(float)) pi.SetValue(obj, (float)value);
else if (pi.PropertyType == typeof(int)) pi.SetValue(obj, (int)value);
else pi.SetValue(obj, value);
}
}
catch { }
});
// greg.unity.set_bool(handle, memberName, value)
unity["set_bool"] = (Action<int, string, bool>)((handle, name, value) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj != null) SetMemberValue(obj, name, value);
}
catch { }
});
// greg.unity.get_handle(handle, memberName) -> handle to the referenced object
unity["get_handle"] = (Func<int, string, int>)((handle, name) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj == null) return 0;
var val = GetMemberValue(obj, name);
return val != null ? gregLuaObjectHandleRegistry.Register(val) : 0;
}
catch { return 0; }
});
}
private static void RegisterTransform(Script vm, Table unity)
{
// greg.unity.position(handle) -> x, y, z (as table {x, y, z})
unity["position"] = (Func<int, Table>)(handle =>
{
var t = new Table(vm);
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Transform tr = null;
if (obj is GameObject go) tr = go.transform;
else if (obj is Component c) tr = c.transform;
else if (obj is Transform tt) tr = tt;
if (tr != null)
{
t["x"] = (double)tr.position.x;
t["y"] = (double)tr.position.y;
t["z"] = (double)tr.position.z;
}
}
catch { }
return t;
});
// greg.unity.set_position(handle, x, y, z)
unity["set_position"] = (Action<int, double, double, double>)((handle, x, y, z) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Transform tr = null;
if (obj is GameObject go) tr = go.transform;
else if (obj is Component c) tr = c.transform;
else if (obj is Transform tt) tr = tt;
if (tr != null) tr.position = new Vector3((float)x, (float)y, (float)z);
}
catch { }
});
// greg.unity.set_local_scale(handle, x, y, z)
unity["set_local_scale"] = (Action<int, double, double, double>)((handle, x, y, z) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Transform tr = null;
if (obj is GameObject go) tr = go.transform;
else if (obj is Component c) tr = c.transform;
else if (obj is Transform tt) tr = tt;
if (tr != null) tr.localScale = new Vector3((float)x, (float)y, (float)z);
}
catch { }
});
// greg.unity.set_rotation(handle, x, y, z, w)
unity["set_rotation"] = (Action<int, double, double, double, double>)((handle, x, y, z, w) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Transform tr = null;
if (obj is GameObject go) tr = go.transform;
else if (obj is Component c) tr = c.transform;
else if (obj is Transform tt) tr = tt;
if (tr != null) tr.rotation = new Quaternion((float)x, (float)y, (float)z, (float)w);
}
catch { }
});
// greg.unity.set_parent(handle, parentHandle)
unity["set_parent"] = (Action<int, int>)((handle, parentHandle) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
var parentObj = gregLuaObjectHandleRegistry.Resolve(parentHandle);
Transform child = null, parent = null;
if (obj is GameObject go) child = go.transform;
else if (obj is Component c) child = c.transform;
if (parentObj is GameObject pgo) parent = pgo.transform;
else if (parentObj is Component pc) parent = pc.transform;
else if (parentObj is Transform pt) parent = pt;
if (child != null) child.SetParent(parent, true);
}
catch { }
});
}
private static void RegisterInstantiate(Script vm, Table unity)
{
// greg.unity.instantiate(handle, parentHandle?) -> new handle
unity["instantiate"] = (Func<int, int, int>)((handle, parentHandle) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj is not UnityEngine.Object uo) return 0;
var parent = gregLuaObjectHandleRegistry.Resolve(parentHandle);
Transform parentT = null;
if (parent is GameObject pgo) parentT = pgo.transform;
else if (parent is Component pc) parentT = pc.transform;
else if (parent is Transform pt) parentT = pt;
var clone = parentT != null
? UnityEngine.Object.Instantiate(uo, parentT)
: UnityEngine.Object.Instantiate(uo);
return clone != null ? gregLuaObjectHandleRegistry.Register(clone) : 0;
}
catch { return 0; }
});
// greg.unity.destroy(handle)
unity["destroy"] = (Action<int>)(handle =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj is UnityEngine.Object uo) UnityEngine.Object.Destroy(uo);
gregLuaObjectHandleRegistry.Release(handle);
}
catch { }
});
}
private static void RegisterMaterial(Script vm, Table unity)
{
// greg.unity.material_hex(handle, propertyName) -> "#RRGGBB" or nil
unity["material_hex"] = (Func<int, string, string>)((handle, prop) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Material mat = null;
if (obj is Material m) mat = m;
else if (obj is Renderer r) mat = r.sharedMaterial;
else if (obj is Component c)
{
var rend = c.GetComponent<Renderer>();
if (rend != null) mat = rend.sharedMaterial;
}
if (mat == null || !mat.HasProperty(prop)) return null;
var col = mat.GetColor(prop);
return ColorToHex(col);
}
catch { return null; }
});
}
private static void RegisterTmpro(Script vm, Table unity)
{
// greg.unity.tmpro_set(handle, text, fontSize, fontMin, fontMax, autoSize, wordWrap, alignment, colorHex)
unity["tmpro_set"] = (Action<int, string, double, double, double, bool, bool, int, string>)(
(handle, text, fontSize, fontMin, fontMax, autoSize, wordWrap, alignment, colorHex) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
TextMeshProUGUI tmp = null;
if (obj is TextMeshProUGUI t) tmp = t;
else if (obj is GameObject go) tmp = go.GetComponent<TextMeshProUGUI>();
else if (obj is Component c) tmp = c.GetComponent<TextMeshProUGUI>();
if (tmp == null) return;
if (text != null) tmp.text = text;
if (fontSize > 0) tmp.fontSize = (float)fontSize;
tmp.fontSizeMin = (float)fontMin;
tmp.fontSizeMax = (float)fontMax;
tmp.enableAutoSizing = autoSize;
tmp.enableWordWrapping = wordWrap;
tmp.alignment = (TextAlignmentOptions)alignment;
if (!string.IsNullOrEmpty(colorHex) && ColorUtility.TryParseHtmlString(colorHex, out var col))
tmp.color = col;
}
catch { }
});
// greg.unity.tmpro_get_text(handle) -> string
unity["tmpro_get_text"] = (Func<int, string>)(handle =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
TextMeshProUGUI tmp = null;
if (obj is TextMeshProUGUI t) tmp = t;
else if (obj is GameObject go) tmp = go.GetComponent<TextMeshProUGUI>();
else if (obj is Component c) tmp = c.GetComponent<TextMeshProUGUI>();
return tmp?.text;
}
catch { return null; }
});
// greg.unity.tmpro_anchored_pos(handle) -> {x, y}
unity["tmpro_anchored_pos"] = (Func<int, Table>)(handle =>
{
var t = new Table(vm);
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
TextMeshProUGUI tmp = null;
if (obj is TextMeshProUGUI tt) tmp = tt;
else if (obj is GameObject go) tmp = go.GetComponent<TextMeshProUGUI>();
else if (obj is Component c) tmp = c.GetComponent<TextMeshProUGUI>();
if (tmp?.rectTransform != null)
{
t["x"] = (double)tmp.rectTransform.anchoredPosition.x;
t["y"] = (double)tmp.rectTransform.anchoredPosition.y;
}
}
catch { }
return t;
});
// greg.unity.tmpro_set_anchored_pos(handle, x, y)
unity["tmpro_set_anchored_pos"] = (Action<int, double, double>)((handle, x, y) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
TextMeshProUGUI tmp = null;
if (obj is TextMeshProUGUI t) tmp = t;
else if (obj is GameObject go) tmp = go.GetComponent<TextMeshProUGUI>();
else if (obj is Component c) tmp = c.GetComponent<TextMeshProUGUI>();
if (tmp?.rectTransform != null)
tmp.rectTransform.anchoredPosition = new Vector2((float)x, (float)y);
}
catch { }
});
}
private static void RegisterTextMesh(Script vm, Table unity)
{
// greg.unity.textmesh_set(handle, text, fontSize, charSize, colorHex, anchor, alignment)
unity["textmesh_set"] = (Action<int, string, int, double, string, int, int>)(
(handle, text, fontSize, charSize, colorHex, anchor, alignment) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
TextMesh tm = null;
if (obj is TextMesh t) tm = t;
else if (obj is GameObject go) tm = go.GetComponent<TextMesh>();
else if (obj is Component c) tm = c.GetComponent<TextMesh>();
if (tm == null) return;
if (text != null) tm.text = text;
if (fontSize > 0) tm.fontSize = fontSize;
if (charSize > 0) tm.characterSize = (float)charSize;
tm.anchor = (TextAnchor)anchor;
tm.alignment = (TextAlignment)alignment;
if (!string.IsNullOrEmpty(colorHex) && ColorUtility.TryParseHtmlString(colorHex, out var col))
tm.color = col;
}
catch { }
});
}
private static void RegisterPhysics(Script vm, Table unity)
{
// greg.unity.raycast(ox, oy, oz, dx, dy, dz, maxDist) -> { hit, handle, point = {x,y,z} }
unity["raycast"] = (Func<double, double, double, double, double, double, double, Table>)(
(ox, oy, oz, dx, dy, dz, maxDist) =>
{
var t = new Table(vm);
t["hit"] = false;
try
{
var origin = new Vector3((float)ox, (float)oy, (float)oz);
var direction = new Vector3((float)dx, (float)dy, (float)dz);
if (Physics.Raycast(new Ray(origin, direction), out var hitInfo, (float)maxDist))
{
t["hit"] = true;
t["handle"] = gregLuaObjectHandleRegistry.Register(hitInfo.collider.gameObject);
var pt = new Table(vm);
pt["x"] = (double)hitInfo.point.x;
pt["y"] = (double)hitInfo.point.y;
pt["z"] = (double)hitInfo.point.z;
t["point"] = pt;
}
}
catch { }
return t;
});
// greg.unity.camera_ray() -> { ox, oy, oz, dx, dy, dz } from gregMain camera
unity["camera_ray"] = (Func<Table>)(() =>
{
var t = new Table(vm);
try
{
var cam = Camera.main;
if (cam != null)
{
var pos = cam.transform.position;
var fwd = cam.transform.forward;
t["ox"] = (double)pos.x; t["oy"] = (double)pos.y; t["oz"] = (double)pos.z;
t["dx"] = (double)fwd.x; t["dy"] = (double)fwd.y; t["dz"] = (double)fwd.z;
}
}
catch { }
return t;
});
}
private static void RegisterGameObject(Script vm, Table unity)
{
// greg.unity.create_gameobject(name, parentHandle) -> handle
unity["create_gameobject"] = (Func<string, int, int>)((name, parentHandle) =>
{
try
{
var go = new GameObject(name);
var parent = gregLuaObjectHandleRegistry.Resolve(parentHandle);
Transform pt = null;
if (parent is GameObject pgo) pt = pgo.transform;
else if (parent is Component pc) pt = pc.transform;
if (pt != null) go.transform.SetParent(pt, true);
return gregLuaObjectHandleRegistry.Register(go);
}
catch { return 0; }
});
// greg.unity.set_name(handle, name)
unity["set_name"] = (Action<int, string>)((handle, name) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj is GameObject go) go.name = name;
else if (obj is Component c) c.gameObject.name = name;
}
catch { }
});
// greg.unity.get_name(handle) -> string
unity["get_name"] = (Func<int, string>)(handle =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
if (obj is GameObject go) return go.name;
if (obj is Component c) return c.gameObject?.name;
return null;
}
catch { return null; }
});
// greg.unity.get_parent_component(handle, typeName) -> handle
unity["get_parent_component"] = (Func<int, string, int>)((handle, typeName) =>
{
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Component src = null;
if (obj is GameObject go) src = go.transform;
else if (obj is Component c) src = c;
if (src == null) return 0;
var type = ResolveIl2CppType(typeName);
if (type == null) return 0;
var comp = src.GetComponentInParent(Il2CppInterop.Runtime.Il2CppType.From(type));
return comp != null ? gregLuaObjectHandleRegistry.Register(comp) : 0;
}
catch { return 0; }
});
// greg.unity.get_children_components(handle, typeName) -> table of handles
unity["get_children_components"] = (Func<int, string, Table>)((handle, typeName) =>
{
var result = new Table(vm);
try
{
var obj = gregLuaObjectHandleRegistry.Resolve(handle);
Component src = null;
if (obj is GameObject go) src = go.transform;
else if (obj is Component c) src = c;
if (src == null) return result;
var type = ResolveIl2CppType(typeName);
if (type == null) return result;
var comps = src.GetComponentsInChildren(Il2CppInterop.Runtime.Il2CppType.From(type), true);
if (comps == null) return result;
for (int i = 0; i < comps.Count; i++)
{
if (comps[i] != null)
result.Append(DynValue.NewNumber(gregLuaObjectHandleRegistry.Register(comps[i])));
}
}
catch { }
return result;
});
}
private static void RegisterColor(Script vm, Table color)
{
// greg.color.to_hex(r, g, b) -> "#RRGGBB"
color["to_hex"] = (Func<double, double, double, string>)((r, g, b) =>
ColorToHex(new Color((float)r, (float)g, (float)b)));
// greg.color.normalize_hex(raw) -> "#RRGGBB" or nil
color["normalize_hex"] = (Func<string, string>)(raw =>
{
if (string.IsNullOrWhiteSpace(raw)) return null;
var s = raw.Trim();
if (!s.StartsWith("#")) s = "#" + s;
if (ColorUtility.TryParseHtmlString(s, out var c))
return ColorToHex(c);
return null;
});
// greg.color.parse(hex) -> {r, g, b}
color["parse"] = (Func<string, Table>)(hex =>
{
var t = new Table(vm);
if (!string.IsNullOrEmpty(hex))
{
var s = hex.Trim();
if (!s.StartsWith("#")) s = "#" + s;
if (ColorUtility.TryParseHtmlString(s, out var c))
{
t["r"] = (double)c.r; t["g"] = (double)c.g; t["b"] = (double)c.b;
}
}
return t;
});
}
private static void RegisterConfig(Script vm, Table config)
{
// greg.config.load(path) -> table of {key = value}
config["load"] = (Func<string, Table>)(path =>
{
var t = new Table(vm);
try
{
if (!File.Exists(path)) return t;
foreach (var line in File.ReadAllLines(path))
{
if (string.IsNullOrWhiteSpace(line)) continue;
var trimmed = line.Trim();
if (trimmed.StartsWith("#")) continue;
var eq = trimmed.IndexOf('=');
if (eq <= 0 || eq >= trimmed.Length - 1) continue;
var key = trimmed.Substring(0, eq).Trim();
var val = trimmed.Substring(eq + 1).Trim();
if (float.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out var num))
t[key] = (double)num;
else
t[key] = val;
}
}
catch { }
return t;
});
// greg.config.save(path, table)
config["save"] = (Action<string, Table>)((path, data) =>
{
try
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
var lines = new List<string>();
foreach (var pair in data.Pairs)
{
var key = pair.Key.CastToString();
var val = pair.Value.Type == DataType.Number
? pair.Value.Number.ToString(CultureInfo.InvariantCulture)
: pair.Value.CastToString();
lines.Add($"{key}={val}");
}
File.WriteAllLines(path, lines);
}
catch { }
});
// greg.config.userdata_path() -> string
config["userdata_path"] = (Func<string>)(() =>
MelonLoader.Utils.MelonEnvironment.UserDataDirectory);
}
private static void RegisterGui(Script vm, Table gui)
{
// greg.gui functions are only valid inside on_gui callbacks.
// The LuaLanguageBridge calls on_gui Lua functions inside Unity's OnGUI.
gui["box"] = (Action<double, double, double, double, string>)((x, y, w, h, title) =>
GUI.Box(new Rect((float)x, (float)y, (float)w, (float)h), title ?? ""));
gui["label"] = (Action<double, double, double, double, string>)((x, y, w, h, text) =>
GUI.Label(new Rect((float)x, (float)y, (float)w, (float)h), text ?? ""));
gui["button"] = (Func<double, double, double, double, string, bool>)((x, y, w, h, text) =>
GUI.Button(new Rect((float)x, (float)y, (float)w, (float)h), text ?? ""));
gui["toggle"] = (Func<double, double, double, double, bool, string, bool>)((x, y, w, h, value, text) =>
GUI.Toggle(new Rect((float)x, (float)y, (float)w, (float)h), value, text ?? ""));
gui["screen_width"] = (Func<double>)(() => Screen.width);
gui["screen_height"] = (Func<double>)(() => Screen.height);
}
// --- helpers ---
private static string ColorToHex(Color c)
{
int r = Mathf.Clamp(Mathf.RoundToInt(c.r * 255f), 0, 255);
int g = Mathf.Clamp(Mathf.RoundToInt(c.g * 255f), 0, 255);
int b = Mathf.Clamp(Mathf.RoundToInt(c.b * 255f), 0, 255);
return $"#{r:X2}{g:X2}{b:X2}";
}
private static Type ResolveIl2CppType(string typeName)
{
if (string.IsNullOrWhiteSpace(typeName)) return null;
// Try Il2Cpp namespace first
var t = Type.GetType($"Il2Cpp.{typeName}, Assembly-CSharp");
if (t != null) return t;
// Try Il2CppTMPro
t = Type.GetType($"Il2CppTMPro.{typeName}, Il2CppTMPro");
if (t != null) return t;
// Try UnityEngine
t = Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule");
if (t != null) return t;
// Try fully qualified
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
t = asm.GetType(typeName);
if (t != null) return t;
}
return null;
}
private static object GetMemberValue(object obj, string name)
{
if (obj == null || string.IsNullOrEmpty(name)) return null;
var member = FindMember(obj.GetType(), name);
if (member is FieldInfo fi) return fi.GetValue(obj);
if (member is PropertyInfo pi && pi.GetIndexParameters().Length == 0) return pi.GetValue(obj);
return null;
}
private static void SetMemberValue(object obj, string name, object value)
{
if (obj == null || string.IsNullOrEmpty(name)) return;
var member = FindMember(obj.GetType(), name);
if (member is FieldInfo fi) fi.SetValue(obj, value);
else if (member is PropertyInfo pi && pi.CanWrite) pi.SetValue(obj, value);
}
private static MemberInfo FindMember(Type type, string name)
{
var fi = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (fi != null) return fi;
var pi = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
return pi;
}
}
@@ -1,126 +0,0 @@
using System;
using System.Collections.Generic;
using MelonLoader;
using greg.Core;
namespace greg.Core.Scripting.Rust;
/// <summary>
/// Adapter that represents native Rust/C mod support inside the shared language host.
/// Delegates all lifecycle calls to <see cref="gregFfiBridge"/>.
/// </summary>
public sealed class RustLanguageBridgeAdapter : iGregLanguageBridge
{
private static readonly string[] Extensions = { ".dll", ".greg", ".gregr", ".gregl", ".gregp" };
private readonly MelonLogger.Instance _logger;
private readonly string _modsPath;
private readonly gregFfiBridge _ffiBridge;
public RustLanguageBridgeAdapter(MelonLogger.Instance logger, string modsPath, gregFfiBridge gregFfiBridge)
{
_logger = logger;
_modsPath = modsPath;
_ffiBridge = gregFfiBridge;
}
public string LanguageName => "rust/native";
public IReadOnlyList<string> ScriptExtensions => Extensions;
public void Initialize()
{
_logger.Msg($"[gregCore] Rust/native bridge initialized, mods path: {_modsPath}");
CrashLog.Log("RustLanguageBridgeAdapter: Initialize");
}
public int LoadScripts()
{
try
{
_ffiBridge.LoadAllMods();
var units = _ffiBridge.GetLoadedRuntimeUnits();
_logger.Msg($"[gregCore] Rust/native bridge loaded {units.Count} mod(s).");
return units.Count;
}
catch (Exception ex)
{
_logger.Error($"[gregCore] Rust/native bridge LoadAllMods failed: {ex.Message}");
CrashLog.LogException("RustLanguageBridgeAdapter.LoadScripts", ex);
return 0;
}
}
public IReadOnlyList<gregRuntimeUnit> GetRuntimeUnits()
{
return _ffiBridge.GetLoadedRuntimeUnits();
}
public bool SetUnitEnabled(string unitId, bool enabled)
{
if (string.IsNullOrWhiteSpace(unitId) || !unitId.StartsWith("native:", StringComparison.OrdinalIgnoreCase))
return false;
_ffiBridge.SetModEnabled(unitId, enabled);
return true;
}
public int ReloadEnabledUnits()
{
return _ffiBridge.ReloadAllMods();
}
public void OnSceneLoaded(string sceneName)
{
try
{
_ffiBridge.OnSceneLoaded(sceneName);
}
catch (Exception ex)
{
_logger.Error($"[gregCore] Rust/native bridge OnSceneLoaded failed: {ex.Message}");
CrashLog.LogException("RustLanguageBridgeAdapter.OnSceneLoaded", ex);
}
}
public void OnUpdate(float deltaTime)
{
try
{
_ffiBridge.OnUpdate(deltaTime);
}
catch (Exception ex)
{
CrashLog.LogException("RustLanguageBridgeAdapter.OnUpdate", ex);
}
}
public void OnGui()
{
try
{
_ffiBridge.OnGui();
}
catch (Exception ex)
{
CrashLog.LogException("RustLanguageBridgeAdapter.OnGui", ex);
}
}
public void Shutdown()
{
try
{
_logger.Msg("[gregCore] Shutting down Rust/native bridge...");
_ffiBridge.Shutdown();
}
catch (Exception ex)
{
_logger.Error($"[gregCore] Rust/native bridge Shutdown failed: {ex.Message}");
CrashLog.LogException("RustLanguageBridgeAdapter.Shutdown", ex);
}
}
}
-250
View File
@@ -1,250 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using MelonLoader;
using MelonLoader.Utils;
using greg.Core;
namespace greg.Core.Scripting;
/// <summary>
/// gregCoreLoader SDK runtime host that orchestrates language bridges and keeps failures isolated.
/// </summary>
public sealed class gregLanguageBridgeHost
{
private readonly MelonLogger.Instance _logger;
private readonly List<iGregLanguageBridge> _bridges = new();
public Lua.LuaLanguageBridge LuaBridge { get; }
public gregLanguageBridgeHost(MelonLogger.Instance logger, string rustModsPath, greg.Core.gregFfiBridge gregFfiBridge)
{
_logger = logger;
string scriptRoot = Path.Combine(MelonEnvironment.ModsDirectory, "ScriptMods");
string luaRoot = Path.Combine(scriptRoot, "lua");
string jsTsRoot = Path.Combine(scriptRoot, "js");
string goRoot = Path.Combine(scriptRoot, "go");
LuaBridge = TryCreateBridge(
bridgeDisplayName: "Lua",
factory: () => new Lua.LuaLanguageBridge(_logger, luaRoot)) as Lua.LuaLanguageBridge;
if (LuaBridge != null)
{
_bridges.Add(LuaBridge);
}
TryAddBridge(
bridgeDisplayName: "TS/JS",
factory: () => new JS.TypeScriptJavaScriptLanguageBridge(_logger, jsTsRoot));
TryAddBridge(
bridgeDisplayName: "Rust/Native",
factory: () => new Rust.RustLanguageBridgeAdapter(_logger, rustModsPath, gregFfiBridge));
TryAddBridge(
bridgeDisplayName: "Go (WASM)",
factory: () => new Go.GoLanguageBridge(_logger, goRoot));
}
private void TryAddBridge(string bridgeDisplayName, Func<iGregLanguageBridge> factory)
{
iGregLanguageBridge bridge = TryCreateBridge(bridgeDisplayName, factory);
if (bridge != null)
{
_bridges.Add(bridge);
}
}
private iGregLanguageBridge TryCreateBridge(string bridgeDisplayName, Func<iGregLanguageBridge> factory)
{
try
{
return factory();
}
catch (Exception exception) when (
exception is FileNotFoundException ||
exception is FileLoadException ||
exception is DllNotFoundException ||
exception is BadImageFormatException)
{
_logger.Warning($"gregCore bridge '{bridgeDisplayName}' disabled due to missing/invalid dependency: {exception.Message}");
CrashLog.LogException($"gregLanguageBridgeHost.Create.{bridgeDisplayName}", exception);
return null;
}
catch (Exception exception)
{
_logger.Warning($"gregCore bridge '{bridgeDisplayName}' disabled due to initialization error: {exception.Message}");
CrashLog.LogException($"gregLanguageBridgeHost.Create.{bridgeDisplayName}", exception);
return null;
}
}
public void Initialize()
{
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
bridge.Initialize();
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.Initialize.{bridge.LanguageName}", exception);
}
}
}
public int LoadAll()
{
int total = 0;
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
total += bridge.LoadScripts();
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.LoadAll.{bridge.LanguageName}", exception);
}
}
_logger.Msg($"gregCore language host initialized bridges={_bridges.Count}, loadedUnits={total}.");
return total;
}
public void OnSceneLoaded(string sceneName)
{
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
bridge.OnSceneLoaded(sceneName);
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.OnSceneLoaded.{bridge.LanguageName}", exception);
}
}
}
public void OnUpdate(float deltaTime)
{
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
bridge.OnUpdate(deltaTime);
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.OnUpdate.{bridge.LanguageName}", exception);
}
}
}
public void OnGui()
{
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
bridge.OnGui();
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.OnGui.{bridge.LanguageName}", exception);
}
}
}
public IReadOnlyList<gregRuntimeUnit> GetRuntimeUnits()
{
var units = new List<gregRuntimeUnit>();
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
var bridgeUnits = bridge.GetRuntimeUnits();
for (int unitIndex = 0; unitIndex < bridgeUnits.Count; unitIndex++)
{
units.Add(bridgeUnits[unitIndex]);
}
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.GetRuntimeUnits.{bridge.LanguageName}", exception);
}
}
return units;
}
public bool SetUnitEnabled(string unitId, bool enabled)
{
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
if (bridge.SetUnitEnabled(unitId, enabled))
{
return true;
}
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.SetUnitEnabled.{bridge.LanguageName}", exception);
}
}
return false;
}
public int ReloadHotloadableUnits()
{
int total = 0;
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
total += bridge.ReloadEnabledUnits();
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.ReloadEnabledUnits.{bridge.LanguageName}", exception);
}
}
return total;
}
public void Shutdown()
{
for (int index = 0; index < _bridges.Count; index++)
{
iGregLanguageBridge bridge = _bridges[index];
try
{
bridge.Shutdown();
}
catch (Exception exception)
{
CrashLog.LogException($"gregLanguageBridgeHost.Shutdown.{bridge.LanguageName}", exception);
}
}
}
}
-12
View File
@@ -1,12 +0,0 @@
namespace greg.Core.Scripting;
public sealed class gregRuntimeUnit
{
public string Id;
public string DisplayName;
public string Language;
public bool Enabled;
public bool SupportsHotReload;
public bool IsNativeModule;
}
-32
View File
@@ -1,32 +0,0 @@
/// <file-summary>
/// Schicht: Tests
/// Zweck: Tests für den GregDependencyResolver.
/// Maintainer: Testet lineare, zyklische und fehlende Abhängigkeiten.
/// </file-summary>
using Xunit;
using FluentAssertions;
using gregCore.Infrastructure.Plugins;
using gregCore.Core.Models;
using gregCore.Core.Exceptions;
namespace gregCore.Tests.Core;
public class DependencyResolverTests
{
[Fact]
public void Resolve_WithLinearDependencies_ShouldReturnCorrectOrder()
{
var resolver = new GregDependencyResolver();
var plugins = new List<PluginInfo>
{
new() { Manifest = new ModManifest { Id = "C", Dependencies = new[] { "B" } } },
new() { Manifest = new ModManifest { Id = "A", Dependencies = Array.Empty<string>() } },
new() { Manifest = new ModManifest { Id = "B", Dependencies = new[] { "A" } } }
};
var result = resolver.Resolve(plugins);
result.Should().NotBeEmpty();
}
}
-53
View File
@@ -1,53 +0,0 @@
/// <file-summary>
/// Schicht: Tests
/// Zweck: Tests für den GregEventBus.
/// Maintainer: Stellt Thread-Safety und Funktionalität sicher.
/// </file-summary>
using Xunit;
using FluentAssertions;
using gregCore.Core.Events;
using gregCore.Core.Models;
using gregCore.Tests.Mocks;
namespace gregCore.Tests.Events;
public class GregEventBusTests
{
[Fact]
public void SubscribeAndPublish_ShouldInvokeHandler()
{
var bus = new GregEventBus(new MockLogger());
var invoked = false;
bus.Subscribe("test.hook", p => invoked = true);
bus.Publish("test.hook", new EventPayload());
invoked.Should().BeTrue();
}
[Fact]
public void Unsubscribe_ShouldNotInvokeHandler()
{
var bus = new GregEventBus(new MockLogger());
var invoked = false;
Action<EventPayload> handler = p => invoked = true;
bus.Subscribe("test.hook", handler);
bus.Unsubscribe("test.hook", handler);
bus.Publish("test.hook", new EventPayload());
invoked.Should().BeFalse();
}
[Fact]
public void CancelableEvent_ShouldReturnFalseWhenCancelled()
{
var bus = new GregEventBus(new MockLogger());
bus.Subscribe("test.hook", p => p.IsCancelled = true);
var result = bus.Publish("test.hook", new EventPayload { IsCancelable = true });
result.Should().BeFalse();
}
}
-23
View File
@@ -1,23 +0,0 @@
/// <file-summary>
/// Schicht: Tests
/// Zweck: Mock-Logger für Unit-Tests.
/// Maintainer: Nur in Tests verwenden.
/// </file-summary>
namespace gregCore.Tests.Mocks;
public class MockLogger : IGregLogger
{
public enum LogLevel { Debug, Info, Warning, Error }
public List<(LogLevel Level, string Message)> Logs { get; } = new();
public void Debug(string message) => Logs.Add((LogLevel.Debug, message));
public void Info(string message) => Logs.Add((LogLevel.Info, message));
public void Warning(string message) => Logs.Add((LogLevel.Warning, message));
public void Error(string message, Exception? ex = null) => Logs.Add((LogLevel.Error, $"{message} {ex?.Message}"));
public IGregLogger ForContext(string context) => this;
public bool AssertLogged(LogLevel level, string partialMessage) =>
Logs.Any(l => l.Level == level && l.Message.Contains(partialMessage));
}
-18
View File
@@ -1,18 +0,0 @@
<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>
-38
View File
@@ -1,38 +0,0 @@
using UnityEngine;
using UnityEngine.UI;
namespace greg.Core.UI.Components;
public class GregBadge : IGregUIComponent
{
public string Text { get; set; } = "Badge";
public Color? Color { get; set; }
public GameObject Build(Transform parent)
{
var go = new GameObject($"Badge_{Text}");
go.transform.SetParent(parent, false);
var rect = go.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(60, 20);
var img = go.AddComponent<Image>();
img.color = Color ?? GregUITheme.PrimaryContainer;
var textGO = new GameObject("Text");
textGO.transform.SetParent(go.transform, false);
var text = textGO.AddComponent<TextMeshProUGUI>();
text.text = Text;
text.fontSize = 10;
text.color = GregUITheme.OnPrimary;
text.alignment = TextAlignmentOptions.Center;
var textRect = textGO.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.sizeDelta = Vector2.zero;
return go;
}
}
-55
View File
@@ -1,55 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace greg.Core.UI.Components;
public enum GregButtonStyle { Primary, Secondary, Tertiary, Danger }
public class GregButton : IGregUIComponent
{
public string Label { get; set; } = "Button";
public Action OnClick { get; set; }
public GregButtonStyle Style { get; set; } = GregButtonStyle.Primary;
public GameObject Build(Transform parent)
{
var go = new GameObject($"Button_{Label}");
go.transform.SetParent(parent, false);
var rect = go.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 32);
var img = go.AddComponent<Image>();
img.color = Style switch
{
GregButtonStyle.Primary => GregUITheme.Primary,
GregButtonStyle.Secondary => GregUITheme.Secondary,
GregButtonStyle.Tertiary => GregUITheme.Tertiary,
GregButtonStyle.Danger => GregUITheme.Error,
_ => GregUITheme.Primary
};
var btn = go.AddComponent<Button>();
if (OnClick != null)
{
btn.onClick.AddListener(OnClick);
}
var textGO = new GameObject("Label");
textGO.transform.SetParent(go.transform, false);
var text = textGO.AddComponent<TextMeshProUGUI>();
text.text = Label;
text.fontSize = 12;
text.color = GregUITheme.OnPrimary;
text.alignment = TextAlignmentOptions.Center;
var textRect = textGO.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.sizeDelta = Vector2.zero;
return go;
}
}
-28
View File
@@ -1,28 +0,0 @@
using UnityEngine;
namespace greg.Core.UI.Components;
public class GregLabel : IGregUIComponent
{
public string Text { get; set; } = "Label";
public Color? Color { get; set; }
public float FontSize { get; set; } = 12;
public GameObject Build(Transform parent)
{
var go = new GameObject($"Label_{Text}");
go.transform.SetParent(parent, false);
var rect = go.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, FontSize + 4);
var text = go.AddComponent<TextMeshProUGUI>();
text.text = Text;
text.fontSize = FontSize;
text.color = Color ?? GregUITheme.OnSurface;
text.alignment = TextAlignmentOptions.Left;
return go;
}
}
@@ -1,498 +0,0 @@
using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using Il2CppTMPro;
using MelonLoader;
using greg.Core;
using greg.Core.UI;
namespace greg.Core.UI.Components;
public class GregMainMenuReplacement : MonoBehaviour
{
public static GregMainMenuReplacement Instance { get; private set; }
private const int SORTING_ORDER = 999;
private const float REFERENCE_WIDTH = 1920f;
private const float REFERENCE_HEIGHT = 1080f;
private GameObject _menuRoot;
private Canvas _canvas;
private EventSystem _eventSystem;
private bool _isVisible = false;
private float _scaleFactor = 1f;
private Action _onContinueClicked;
private Action _onNewGameClicked;
private Action _onLoadGameClicked;
private Action _onSettingsClicked;
private Action _onModsClicked;
private Action _onQuitClicked;
private Action _onReportBugClicked;
private Action _onDiscordClicked;
private Action _onWishlistClicked;
private Action _onTwitterClicked;
private Action _onStatsClicked;
private float _animTime = 0f;
private const float ANIM_DURATION = 0.6f;
private bool _isAnimating = false;
public bool IsVisible => _isVisible;
private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
gameObject.SetActive(false);
}
public void Configure(Action onContinue, Action onNewGame, Action onLoadGame, Action onSettings, Action onMods, Action onQuit, Action onReportBug, Action onDiscord, Action onWishlist, Action onTwitter, Action onStats)
{
_onContinueClicked = onContinue;
_onNewGameClicked = onNewGame;
_onLoadGameClicked = onLoadGame;
_onSettingsClicked = onSettings;
_onModsClicked = onMods;
_onQuitClicked = onQuit;
_onReportBugClicked = onReportBug;
_onDiscordClicked = onDiscord;
_onWishlistClicked = onWishlist;
_onTwitterClicked = onTwitter;
_onStatsClicked = onStats;
}
public void Show()
{
if (_menuRoot == null) BuildMenu();
_menuRoot.SetActive(true);
_isVisible = true;
_animTime = 0f;
_isAnimating = true;
gameObject.SetActive(true);
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
EnsureEventSystem();
}
public void Hide()
{
_isVisible = false;
_isAnimating = true;
_animTime = 0f;
}
private void Update()
{
if (_isAnimating)
{
_animTime += Time.deltaTime;
float t = Mathf.Clamp01(_animTime / ANIM_DURATION);
float eased = EaseOutExpo(t);
var canvasGroup = _menuRoot?.GetComponent<CanvasGroup>();
if (canvasGroup != null)
{
canvasGroup.alpha = _isVisible ? eased : 1f - eased;
}
if (t >= 1f)
{
_isAnimating = false;
if (!_isVisible)
{
_menuRoot?.SetActive(false);
gameObject.SetActive(false);
}
}
}
}
private float EaseOutExpo(float t) => t >= 1f ? 1f : 1f - Mathf.Pow(2f, -10f * t);
private void EnsureEventSystem()
{
if (_eventSystem != null) return;
_eventSystem = FindObjectOfType<EventSystem>();
if (_eventSystem == null)
{
var eventSystemGo = new GameObject("EventSystem");
eventSystemGo.transform.SetParent(transform);
_eventSystem = AddComponentSafe<EventSystem>(eventSystemGo);
AddComponentSafe<StandaloneInputModule>(eventSystemGo);
MelonLogger.Msg("[GregMainMenu] EventSystem created.");
}
}
private static T AddComponentSafe<T>(GameObject go) where T : Component
{
return AddComponentSafe(go, typeof(T)) as T;
}
private static Component AddComponentSafe(GameObject go, Type componentType)
{
try
{
var method = typeof(GameObject).GetMethod("AddComponent", new Type[] { typeof(Type) });
return method?.Invoke(go, new object[] { componentType }) as Component;
}
catch (Exception ex)
{
MelonLogger.Error($"[GregMainMenu] AddComponentSafe failed: {ex.Message}");
return null;
}
}
private void BuildMenu()
{
try
{
_scaleFactor = CalculateScaleFactor();
_canvas = AddComponentSafe<Canvas>(gameObject);
if (_canvas != null)
{
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_canvas.sortingOrder = SORTING_ORDER;
}
var scaler = AddComponentSafe<CanvasScaler>(gameObject);
if (scaler != null)
{
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(REFERENCE_WIDTH, REFERENCE_HEIGHT);
scaler.matchWidthOrHeight = 0.5f;
}
AddComponentSafe<GraphicRaycaster>(gameObject);
_menuRoot = new GameObject("GregMainMenu");
_menuRoot.transform.SetParent(transform, false);
var rootRect = AddComponentSafe<RectTransform>(_menuRoot);
if (rootRect != null)
{
rootRect.anchorMin = Vector2.zero;
rootRect.anchorMax = Vector2.one;
rootRect.offsetMin = Vector2.zero;
rootRect.offsetMax = Vector2.zero;
}
var canvasGroup = AddComponentSafe<CanvasGroup>(_menuRoot);
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
}
BuildBackground();
BuildLogo();
BuildMenuButtons();
BuildVersionInfo();
BuildAmbientEffects();
EnsureEventSystem();
MelonLogger.Msg("[GregMainMenu] Menu built successfully.");
}
catch (Exception ex)
{
MelonLogger.Error($"[GregMainMenu] BuildMenu failed: {ex.Message}\n{ex.StackTrace}");
}
}
private float CalculateScaleFactor() => Screen.height / REFERENCE_HEIGHT;
private void BuildBackground()
{
var bg = new GameObject("Background");
bg.transform.SetParent(_menuRoot.transform, false);
var bgRect = AddComponentSafe<RectTransform>(bg);
if (bgRect != null) {
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.offsetMin = Vector2.zero;
bgRect.offsetMax = Vector2.zero;
}
var img = AddComponentSafe<Image>(bg);
if (img != null) img.color = GregUITheme.Surface;
var vignette = new GameObject("Vignette");
vignette.transform.SetParent(_menuRoot.transform, false);
var vigRect = AddComponentSafe<RectTransform>(vignette);
if (vigRect != null) {
vigRect.anchorMin = Vector2.zero;
vigRect.anchorMax = Vector2.one;
vigRect.offsetMin = Vector2.zero;
vigRect.offsetMax = Vector2.zero;
}
var vigImg = AddComponentSafe<Image>(vignette);
if (vigImg != null) vigImg.color = new Color(0f, 0.07f, 0.06f, 0.95f);
}
private void BuildLogo()
{
var logoContainer = new GameObject("LogoContainer");
logoContainer.transform.SetParent(_menuRoot.transform, false);
var logoRect = AddComponentSafe<RectTransform>(logoContainer);
if (logoRect != null)
{
logoRect.anchorMin = new Vector2(0.5f, 0.7f);
logoRect.anchorMax = new Vector2(0.5f, 0.85f);
logoRect.pivot = new Vector2(0.5f, 0.5f);
logoRect.sizeDelta = Vector2.zero;
logoRect.anchoredPosition = Vector2.zero;
}
var titleText = CreateText(logoContainer, "GREG");
if (titleText != null)
{
titleText.fontSize = 72 * _scaleFactor;
titleText.fontStyle = FontStyles.Bold;
titleText.characterSpacing = 8;
titleText.color = GregUITheme.Primary;
titleText.alignment = TextAlignmentOptions.Center;
AddTextGlow(titleText, GregUITheme.Primary, 32f);
}
}
private void BuildMenuButtons()
{
var buttonContainer = new GameObject("ButtonContainer");
buttonContainer.transform.SetParent(_menuRoot.transform, false);
var buttonRect = AddComponentSafe<RectTransform>(buttonContainer);
if (buttonRect != null)
{
buttonRect.anchorMin = new Vector2(0.5f, 0.3f);
buttonRect.anchorMax = new Vector2(0.5f, 0.55f);
buttonRect.pivot = new Vector2(0.5f, 0.5f);
buttonRect.sizeDelta = new Vector2(320 * _scaleFactor, 0);
buttonRect.anchoredPosition = Vector2.zero;
}
var layout = AddComponentSafe<VerticalLayoutGroup>(buttonContainer);
if (layout != null)
{
layout.spacing = 16f * _scaleFactor;
layout.padding = new RectOffset();
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
}
var fitter = AddComponentSafe<ContentSizeFitter>(buttonContainer);
if (fitter != null)
{
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
}
float btnHeight = 56f * _scaleFactor;
CreateMenuButton("CONTINUE", GregButtonStyle.Primary, btnHeight, () => _onContinueClicked?.Invoke());
CreateMenuButton("NEW GAME", GregButtonStyle.Secondary, btnHeight, () => _onNewGameClicked?.Invoke());
CreateMenuButton("LOAD GAME", GregButtonStyle.Secondary, btnHeight, () => _onLoadGameClicked?.Invoke());
CreateMenuButton("SETTINGS", GregButtonStyle.Secondary, btnHeight, () => _onSettingsClicked?.Invoke());
CreateMenuButton("MODS", GregButtonStyle.Secondary, btnHeight, () => _onModsClicked?.Invoke());
CreateMenuButton("REPORT BUG", GregButtonStyle.Secondary, btnHeight, () => _onReportBugClicked?.Invoke());
CreateMenuButton("DISCORD", GregButtonStyle.Secondary, btnHeight, () => _onDiscordClicked?.Invoke());
CreateMenuButton("WISHLIST", GregButtonStyle.Secondary, btnHeight, () => _onWishlistClicked?.Invoke());
CreateMenuButton("TWITTER", GregButtonStyle.Secondary, btnHeight, () => _onTwitterClicked?.Invoke());
CreateMenuButton("STATS", GregButtonStyle.Secondary, btnHeight, () => _onStatsClicked?.Invoke());
CreateMenuButton("QUIT", GregButtonStyle.Danger, btnHeight, () => _onQuitClicked?.Invoke());
}
private void CreateMenuButton(string label, GregButtonStyle style, float height, Action onClick)
{
var btnGo = new GameObject($"Button_{label}");
btnGo.transform.SetParent(_menuRoot.transform, false);
var btnRect = AddComponentSafe<RectTransform>(btnGo);
if (btnRect != null)
{
btnRect.sizeDelta = new Vector2(0, height);
}
var img = AddComponentSafe<Image>(btnGo);
ApplyButtonStyle(img, style);
var btn = AddComponentSafe<Button>(btnGo);
if (btn != null)
{
btn.transition = Selectable.Transition.None;
var colors = btn.colors;
colors.pressedColor = ApplyBrightness(GetStyleColor(style), 0.8f);
colors.highlightedColor = ApplyBrightness(GetStyleColor(style), 1.2f);
btn.colors = colors;
btn.onClick.AddListener((UnityAction)(() => {
PlayButtonSound();
onClick?.Invoke();
}));
}
var content = new GameObject("Content");
content.transform.SetParent(btnGo.transform, false);
var contentRect = AddComponentSafe<RectTransform>(content);
if (contentRect != null)
{
contentRect.anchorMin = Vector2.zero;
contentRect.anchorMax = Vector2.one;
contentRect.offsetMin = Vector2.zero;
contentRect.offsetMax = Vector2.zero;
contentRect.sizeDelta = Vector2.zero;
}
var text = CreateText(content, label);
if (text != null)
{
text.fontSize = 18 * _scaleFactor;
text.fontStyle = FontStyles.Bold;
text.characterSpacing = 4;
text.color = GetStyleTextColor(style);
text.alignment = TextAlignmentOptions.Center;
}
}
private void ApplyButtonStyle(Image img, GregButtonStyle style)
{
if (img == null) return;
img.color = style switch
{
GregButtonStyle.Primary => GregUITheme.Primary,
GregButtonStyle.Secondary => new Color(0f, 0.15f, 0.14f, 0.9f),
GregButtonStyle.Danger => new Color(0.93f, 0.26f, 0.27f, 0.9f),
_ => GregUITheme.Primary
};
}
private Color GetStyleColor(GregButtonStyle style) => style switch
{
GregButtonStyle.Primary => GregUITheme.Primary,
GregButtonStyle.Secondary => new Color(0f, 0.15f, 0.14f, 0.9f),
GregButtonStyle.Danger => new Color(0.93f, 0.26f, 0.27f, 0.9f),
_ => GregUITheme.Primary
};
private Color GetStyleTextColor(GregButtonStyle style) => style switch
{
GregButtonStyle.Primary => GregUITheme.OnPrimary,
GregButtonStyle.Secondary => GregUITheme.OnSurface,
GregButtonStyle.Danger => Color.white,
_ => GregUITheme.OnPrimary
};
private Color ApplyBrightness(Color color, float factor) => new Color(
Mathf.Clamp01(color.r * factor),
Mathf.Clamp01(color.g * factor),
Mathf.Clamp01(color.b * factor),
color.a
);
private void BuildVersionInfo()
{
var versionGo = new GameObject("VersionInfo");
versionGo.transform.SetParent(_menuRoot.transform, false);
var versionRect = AddComponentSafe<RectTransform>(versionGo);
if (versionRect != null)
{
versionRect.anchorMin = new Vector2(0, 0);
versionRect.anchorMax = new Vector2(0, 0);
versionRect.pivot = new Vector2(0, 0);
versionRect.sizeDelta = new Vector2(400, 30);
versionRect.anchoredPosition = new Vector2(20, 15);
}
var versionText = CreateText(versionGo, $"gregCore v{gregReleaseVersion.Current} <color=#61F4D8>[teamGreg]</color>");
if (versionText != null)
{
versionText.fontSize = 12 * _scaleFactor;
versionText.color = new Color(0.38f, 0.96f, 0.85f, 0.6f);
versionText.alignment = TextAlignmentOptions.Left;
}
}
private void BuildAmbientEffects()
{
var glow = new GameObject("AmbientGlow");
glow.transform.SetParent(_menuRoot.transform, false);
var glowRect = AddComponentSafe<RectTransform>(glow);
if (glowRect != null)
{
glowRect.anchorMin = new Vector2(0.5f, 0.5f);
glowRect.anchorMax = new Vector2(0.5f, 0.5f);
glowRect.pivot = new Vector2(0.5f, 0.5f);
glowRect.sizeDelta = new Vector2(800 * _scaleFactor, 600 * _scaleFactor);
glowRect.anchoredPosition = new Vector2(0, -50 * _scaleFactor);
}
var glowImg = AddComponentSafe<Image>(glow);
if (glowImg != null) glowImg.color = new Color(0.38f, 0.96f, 0.85f, 0.03f);
}
private TextMeshProUGUI CreateText(GameObject parent, string text)
{
var textGo = new GameObject("Text");
textGo.transform.SetParent(parent.transform, false);
var rect = AddComponentSafe<RectTransform>(textGo);
if (rect != null)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
rect.sizeDelta = Vector2.zero;
}
var tmp = AddComponentSafe<TextMeshProUGUI>(textGo);
if (tmp != null)
{
tmp.text = text;
tmp.font = GetTMPFont();
tmp.enableWordWrapping = false;
tmp.richText = true;
}
return tmp;
}
private TMP_FontAsset GetTMPFont()
{
var fonts = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
foreach (var font in fonts)
{
if (font.name.Contains("Inter") || font.name.Contains("Space"))
return font;
}
return fonts.Length > 0 ? fonts[0] : null;
}
private void AddTextGlow(TextMeshProUGUI text, Color glowColor, float blur)
{
if (text == null) return;
text.fontMaterial = new Material(text.fontMaterial);
text.fontMaterial.EnableKeyword("GLOW_ON");
text.fontMaterial.SetColor("_GlowColor", glowColor);
text.fontMaterial.SetFloat("_GlowOuter", blur / 100f);
text.fontMaterial.SetFloat("_GlowPower", 0.8f);
}
private void PlayButtonSound() { }
private void OnDestroy() => Instance = null;
}
-162
View File
@@ -1,162 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace greg.Core.UI.Components;
/// <summary>
/// A base panel component using gregUI theme.
/// </summary>
public class GregPanel : IGregUIElement
{
public string PanelId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public Vector2 Size { get; set; } = new Vector2(400, 300);
public GregUIAnchor Anchor { get; set; } = GregUIAnchor.Center;
public bool Draggable { get; set; } = true;
public bool Closable { get; set; } = true;
public List<IGregUIComponent> Components { get; set; } = new();
private GameObject _panelGO;
private bool _isVisible = false;
public bool IsVisible => _isVisible;
public GameObject PanelRoot => _panelGO;
public void Attach(Canvas rootCanvas)
{
if (_panelGO != null) return;
_panelGO = new GameObject($"gregUI_{PanelId}");
_panelGO.transform.SetParent(rootCanvas.transform, false);
var rect = _panelGO.AddComponent<RectTransform>();
rect.sizeDelta = Size;
ApplyAnchor(rect, Anchor);
var img = _panelGO.AddComponent<Image>();
img.color = GregUITheme.Surface;
// Add background outline (Ghost Border)
var outline = _panelGO.AddComponent<Outline>();
outline.effectColor = GregUITheme.GhostBorder;
outline.effectDistance = new Vector2(1, 1);
// Header
var header = new GameObject("Header");
header.transform.SetParent(_panelGO.transform, false);
var headerRect = header.AddComponent<RectTransform>();
headerRect.anchorMin = new Vector2(0, 1);
headerRect.anchorMax = new Vector2(1, 1);
headerRect.pivot = new Vector2(0.5f, 1);
headerRect.sizeDelta = new Vector2(0, 32);
headerRect.anchoredPosition = Vector2.zero;
var headerImg = header.AddComponent<Image>();
headerImg.color = GregUITheme.SurfaceContainerHigh;
var titleGO = new GameObject("Title");
titleGO.transform.SetParent(header.transform, false);
var titleText = titleGO.AddComponent<TextMeshProUGUI>();
titleText.text = Title;
titleText.fontSize = 14;
titleText.color = GregUITheme.OnSurface;
titleText.alignment = TextAlignmentOptions.Left;
var titleRect = titleGO.GetComponent<RectTransform>();
titleRect.anchorMin = Vector2.zero;
titleRect.anchorMax = Vector2.one;
titleRect.sizeDelta = new Vector2(-16, 0);
titleRect.anchoredPosition = new Vector2(8, 0);
// Content Area
var content = new GameObject("Content");
content.transform.SetParent(_panelGO.transform, false);
var contentRect = content.AddComponent<RectTransform>();
contentRect.anchorMin = Vector2.zero;
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 0.5f);
contentRect.sizeDelta = new Vector2(0, -32);
contentRect.anchoredPosition = new Vector2(0, -16);
var layout = content.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset { left = 8, right = 8, top = 8, bottom = 8 };
layout.spacing = GregUITheme.SpaceSM;
layout.childControlHeight = true;
layout.childControlWidth = true;
layout.childForceExpandHeight = false;
layout.childForceExpandWidth = true;
foreach (var comp in Components)
{
comp.Build(content.transform);
}
_panelGO.SetActive(_isVisible);
}
private void ApplyAnchor(RectTransform rect, GregUIAnchor anchor)
{
switch (anchor)
{
case GregUIAnchor.TopLeft:
rect.anchorMin = new Vector2(0, 1);
rect.anchorMax = new Vector2(0, 1);
rect.pivot = new Vector2(0, 1);
break;
case GregUIAnchor.TopRight:
rect.anchorMin = new Vector2(1, 1);
rect.anchorMax = new Vector2(1, 1);
rect.pivot = new Vector2(1, 1);
break;
case GregUIAnchor.BottomLeft:
rect.anchorMin = new Vector2(0, 0);
rect.anchorMax = new Vector2(0, 0);
rect.pivot = new Vector2(0, 0);
break;
case GregUIAnchor.BottomRight:
rect.anchorMin = new Vector2(1, 0);
rect.anchorMax = new Vector2(1, 0);
rect.pivot = new Vector2(1, 0);
break;
case GregUIAnchor.Center:
default:
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
break;
}
rect.anchoredPosition = Vector2.zero;
}
public void Show()
{
_isVisible = true;
_panelGO?.SetActive(true);
}
public void Hide()
{
_isVisible = false;
_panelGO?.SetActive(false);
}
public void Toggle()
{
_isVisible = !_isVisible;
_panelGO?.SetActive(_isVisible);
}
public void Destroy()
{
if (_panelGO != null)
{
UnityEngine.Object.Destroy(_panelGO);
_panelGO = null;
}
}
}
public enum GregUIAnchor { TopLeft, TopRight, TopCenter, Center, BottomLeft, BottomRight, BottomCenter }
@@ -1,91 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Il2CppTMPro;
using greg.Core.UI;
using greg.Core.UI.Components;
namespace greg.Core.UI.Components;
public class GregPauseMenuReplacement : MonoBehaviour
{
public static GregPauseMenuReplacement Instance { get; private set; }
// private GameObject _root;
private GregPanel _mainPanel;
private bool _isVisible = false;
private Action _onResumeClicked;
private Action _onSettingsClicked;
private Action _onSaveClicked;
private Action _onLoadClicked;
private Action _onModsClicked;
private Action _onQuitToMenuClicked;
private Action _onQuitToDesktopClicked;
private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeUI();
gameObject.SetActive(false);
}
public void Configure(Action onResume, Action onSettings, Action onSave, Action onLoad, Action onMods, Action onQuitToMenu, Action onQuitToDesktop)
{
_onResumeClicked = onResume;
_onSettingsClicked = onSettings;
_onSaveClicked = onSave;
_onLoadClicked = onLoad;
_onModsClicked = onMods;
_onQuitToMenuClicked = onQuitToMenu;
_onQuitToDesktopClicked = onQuitToDesktop;
}
private void InitializeUI()
{
_mainPanel = GregUIBuilder.Panel("pause_menu_replacement")
.Title("PAUSE")
.Position(GregUIAnchor.Center)
.Size(400, 550)
.Closable(false)
.Draggable(false)
.AddButton("RESUME", () => _onResumeClicked?.Invoke(), GregButtonStyle.Primary)
.AddSeparator()
.AddButton("SETTINGS", () => _onSettingsClicked?.Invoke(), GregButtonStyle.Secondary)
.AddButton("SAVE GAME", () => _onSaveClicked?.Invoke(), GregButtonStyle.Secondary)
.AddButton("LOAD GAME", () => _onLoadClicked?.Invoke(), GregButtonStyle.Secondary)
.AddButton("MODS", () => _onModsClicked?.Invoke(), GregButtonStyle.Secondary)
.AddSeparator()
.AddButton("MAIN MENU", () => _onQuitToMenuClicked?.Invoke(), GregButtonStyle.Danger)
.AddButton("EXIT TO DESKTOP", () => _onQuitToDesktopClicked?.Invoke(), GregButtonStyle.Danger)
.Build();
if (_mainPanel.PanelRoot != null)
{
_mainPanel.PanelRoot.transform.SetParent(this.transform, false);
}
}
public void Show()
{
_isVisible = true;
gameObject.SetActive(true);
_mainPanel.Show();
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
public void Hide()
{
_isVisible = false;
_mainPanel.Hide();
gameObject.SetActive(false);
}
public void Toggle()
{
if (_isVisible) Hide();
else Show();
}
}
-22
View File
@@ -1,22 +0,0 @@
using UnityEngine;
using UnityEngine.UI;
namespace greg.Core.UI.Components;
public class GregSeparator : IGregUIComponent
{
public GameObject Build(Transform parent)
{
var go = new GameObject("Separator");
go.transform.SetParent(parent, false);
var rect = go.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 1);
var img = go.AddComponent<Image>();
img.color = GregUITheme.GhostBorder;
return go;
}
}
-76
View File
@@ -1,76 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Il2CppTMPro;
using greg.Core.UI;
using greg.Core.UI.Components;
namespace greg.Core.UI.Components;
public class GregShopReplacement : MonoBehaviour
{
public static GregShopReplacement Instance { get; private set; }
// private GameObject _root;
private GregPanel _mainPanel;
private bool _isVisible = false;
private Action _onCloseClicked;
private Action _onCheckoutClicked;
private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeUI();
gameObject.SetActive(false);
}
public void Configure(Action onClose, Action onCheckout)
{
_onCloseClicked = onClose;
_onCheckoutClicked = onCheckout;
}
private void InitializeUI()
{
_mainPanel = GregUIBuilder.Panel("shop_replacement")
.Title("🛒 COMPUTER SHOP")
.Position(GregUIAnchor.Center)
.Size(800, 600)
.AddLabel("Select hardware components for your Data Center / Wählen Sie Hardware für Ihr Rechenzentrum", null, 14)
.AddSeparator()
.AddLabel("(Hardware inventory loading...)")
.AddSeparator()
.AddButton("CHECKOUT", () => _onCheckoutClicked?.Invoke(), GregButtonStyle.Primary)
.AddButton("CLOSE", () => _onCloseClicked?.Invoke(), GregButtonStyle.Secondary)
.Build();
if (_mainPanel.PanelRoot != null)
{
_mainPanel.PanelRoot.transform.SetParent(this.transform, false);
}
}
public void Show()
{
_isVisible = true;
gameObject.SetActive(true);
_mainPanel.Show();
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
public void Hide()
{
_isVisible = false;
_mainPanel.Hide();
gameObject.SetActive(false);
}
public void Toggle()
{
if (_isVisible) Hide();
else Show();
}
}
-87
View File
@@ -1,87 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace greg.Core.UI.Components;
public class GregSlider : IGregUIComponent
{
public string Label { get; set; } = "Slider";
public float Min { get; set; } = 0;
public float Max { get; set; } = 100;
public float Value { get; set; }
public Action<float> OnChange { get; set; }
public GameObject Build(Transform parent)
{
var go = new GameObject($"Slider_{Label}");
go.transform.SetParent(parent, false);
var rect = go.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 40);
var layout = go.AddComponent<VerticalLayoutGroup>();
layout.childControlHeight = false;
layout.childForceExpandHeight = false;
layout.spacing = 4;
var labelGO = new GameObject("Label");
labelGO.transform.SetParent(go.transform, false);
var text = labelGO.AddComponent<TextMeshProUGUI>();
text.text = Label;
text.fontSize = 10;
text.color = GregUITheme.OnSurface;
var sliderRoot = new GameObject("SliderRoot");
sliderRoot.transform.SetParent(go.transform, false);
var sliderRect = sliderRoot.AddComponent<RectTransform>();
sliderRect.sizeDelta = new Vector2(0, 20);
var slider = sliderRoot.AddComponent<Slider>();
var background = new GameObject("Background");
background.transform.SetParent(sliderRoot.transform, false);
var bgImg = background.AddComponent<Image>();
bgImg.color = GregUITheme.SurfaceContainer;
var bgRect = background.GetComponent<RectTransform>();
bgRect.anchorMin = new Vector2(0, 0.25f);
bgRect.anchorMax = new Vector2(1, 0.75f);
bgRect.sizeDelta = Vector2.zero;
var fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(sliderRoot.transform, false);
var faRect = fillArea.AddComponent<RectTransform>();
faRect.anchorMin = new Vector2(0, 0.25f);
faRect.anchorMax = new Vector2(1, 0.75f);
faRect.sizeDelta = new Vector2(-20, 0);
var fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
var fillImg = fill.AddComponent<Image>();
fillImg.color = GregUITheme.Primary;
slider.fillRect = fill.GetComponent<RectTransform>();
slider.fillRect.sizeDelta = Vector2.zero;
var handleArea = new GameObject("Handle Area");
handleArea.transform.SetParent(sliderRoot.transform, false);
var haRect = handleArea.AddComponent<RectTransform>();
haRect.anchorMin = new Vector2(0, 0);
haRect.anchorMax = new Vector2(1, 1);
haRect.sizeDelta = new Vector2(-20, 0);
var handle = new GameObject("Handle");
handle.transform.SetParent(handleArea.transform, false);
var handleImg = handle.AddComponent<Image>();
handleImg.color = GregUITheme.Secondary;
slider.handleRect = handle.GetComponent<RectTransform>();
slider.handleRect.sizeDelta = new Vector2(20, 0);
slider.minValue = Min;
slider.maxValue = Max;
slider.value = Value;
slider.onValueChanged.AddListener(OnChange);
return go;
}
}
-55
View File
@@ -1,55 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace greg.Core.UI.Components;
public class GregToggle : IGregUIComponent
{
public string Label { get; set; } = "Toggle";
public bool Value { get; set; }
public Action<bool> OnChange { get; set; }
public GameObject Build(Transform parent)
{
var go = new GameObject($"Toggle_{Label}");
go.transform.SetParent(parent, false);
var rect = go.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 24);
var layout = go.AddComponent<HorizontalLayoutGroup>();
layout.childControlWidth = false;
layout.childForceExpandWidth = false;
layout.spacing = 8;
var toggleGO = new GameObject("ToggleRoot");
toggleGO.transform.SetParent(go.transform, false);
var toggleRect = toggleGO.AddComponent<RectTransform>();
toggleRect.sizeDelta = new Vector2(24, 24);
var bg = toggleGO.AddComponent<Image>();
bg.color = GregUITheme.SurfaceContainer;
var toggle = toggleGO.AddComponent<Toggle>();
var checkmarkGO = new GameObject("Checkmark");
checkmarkGO.transform.SetParent(toggleGO.transform, false);
var checkmark = checkmarkGO.AddComponent<Image>();
checkmark.color = GregUITheme.Primary;
toggle.graphic = checkmark;
toggle.isOn = Value;
toggle.onValueChanged.AddListener(OnChange);
var labelGO = new GameObject("Label");
labelGO.transform.SetParent(go.transform, false);
var text = labelGO.AddComponent<TextMeshProUGUI>();
text.text = Label;
text.fontSize = 12;
text.color = GregUITheme.OnSurface;
return go;
}
}
-68
View File
@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace greg.Core.UI.Components;
public class GregUIBuilder
{
private string _panelId;
private string _title = string.Empty;
private Vector2 _size = new(400, 300);
private GregUIAnchor _anchor = GregUIAnchor.Center;
private bool _draggable = true;
private bool _closable = true;
private readonly List<IGregUIComponent> _components = new();
private GregUIBuilder(string panelId) => _panelId = panelId;
public static GregUIBuilder Panel(string panelId) => new(panelId);
public GregUIBuilder Title(string title) { _title = title; return this; }
public GregUIBuilder Size(float w, float h) { _size = new(w, h); return this; }
public GregUIBuilder Position(GregUIAnchor anchor){ _anchor = anchor; return this; }
public GregUIBuilder Draggable(bool v = true) { _draggable = v; return this; }
public GregUIBuilder Closable(bool v = true) { _closable = v; return this; }
public GregUIBuilder AddButton(string label, Action onClick, GregButtonStyle style = GregButtonStyle.Primary)
{
_components.Add(new GregButton { Label = label, OnClick = onClick, Style = style });
return this;
}
public GregUIBuilder AddLabel(string text, Color? color = null, float fontSize = 12)
{
_components.Add(new GregLabel { Text = text, Color = color ?? GregUITheme.OnSurface, FontSize = fontSize });
return this;
}
public GregUIBuilder AddToggle(string label, bool initial, Action<bool> onChange)
{
_components.Add(new GregToggle { Label = label, Value = initial, OnChange = onChange });
return this;
}
public GregUIBuilder AddSeparator()
{
_components.Add(new GregSeparator());
return this;
}
public GregPanel Build()
{
var panel = new GregPanel
{
PanelId = _panelId,
Title = _title,
Size = _size,
Anchor = _anchor,
Draggable = _draggable,
Closable = _closable,
Components = _components,
};
GregUIManager.Register(_panelId, panel);
return panel;
}
}
-12
View File
@@ -1,12 +0,0 @@
using UnityEngine;
namespace greg.Core.UI.Components;
/// <summary>
/// Interface for a component within a gregUI panel.
/// </summary>
public interface IGregUIComponent
{
GameObject Build(Transform parent);
}
-36
View File
@@ -1,36 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Il2CppTMPro;
namespace greg.Sdk.UI;
/// <summary>
/// Skeleton for Task 1.3: Data table UI system.
/// </summary>
public class FixedTableUI : MonoBehaviour
{
public FixedTableUI(IntPtr ptr) : base(ptr) { }
private GameObject _container;
private VerticalLayoutGroup _layout;
void Awake()
{
// Setup base container
_container = gameObject;
_layout = _container.AddComponent<VerticalLayoutGroup>();
_layout.childForceExpandHeight = false;
_layout.spacing = 5;
// Add dummy row
AddRow("gregCore Framework", "v1.0.0", "Active");
}
public void AddRow(string col1, string col2, string col3)
{
// To be implemented: Instantiate row prefab and set text
Debug.Log($"[gregUI] Table Row: {col1} | {col2} | {col3}");
}
}
-271
View File
@@ -1,271 +0,0 @@
using System;
using System.Reflection;
using UnityEngine;
using MelonLoader;
namespace greg.Core.UI;
public static class GameMethodInvoker
{
private static readonly BindingFlags DefaultFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
public static bool Invoke(string typeName, string methodName, params object[] args)
{
try
{
var assembly = Assembly.Load("Assembly-CSharp");
var type = assembly.GetType(typeName);
if (type == null)
{
MelonLogger.Warning($"[GameMethodInvoker] Type not found: {typeName}");
return false;
}
return Invoke(type, methodName, null, args);
}
catch (Exception ex)
{
MelonLogger.Error($"[GameMethodInvoker] Failed: {ex.Message}");
return false;
}
}
public static bool Invoke<T>(string methodName, T instance, params object[] args)
{
return Invoke(typeof(T), methodName, instance, args);
}
public static bool Invoke(Type type, string methodName, object instance, params object[] args)
{
try
{
var method = FindMethod(type, methodName, args?.Length ?? 0);
if (method == null)
{
MelonLogger.Warning($"[GameMethodInvoker] Method not found: {type.Name}.{methodName}");
return false;
}
var result = method.Invoke(instance, args);
if (result is bool boolResult && !boolResult)
{
MelonLogger.Warning($"[GameMethodInvoker] Method returned false: {type.Name}.{methodName}");
return false;
}
MelonLogger.Msg($"[GameMethodInvoker] Invoked: {type.Name}.{methodName}");
return true;
}
catch (Exception ex)
{
MelonLogger.Error($"[GameMethodInvoker] {type.Name}.{methodName}: {ex.Message}");
return false;
}
}
public static T GetStaticField<T>(string typeName, string fieldName)
{
try
{
var assembly = Assembly.Load("Assembly-CSharp");
var type = assembly.GetType(typeName);
if (type == null) return default;
var field = type.GetField(fieldName, DefaultFlags);
if (field == null) return default;
var value = field.GetValue(null);
return value != null ? (T)value : default;
}
catch
{
return default;
}
}
public static T GetInstanceField<T>(object instance, string fieldName)
{
try
{
if (instance == null) return default;
var type = instance.GetType();
var field = type.GetField(fieldName, DefaultFlags);
if (field == null) return default;
var value = field.GetValue(instance);
return value != null ? (T)value : default;
}
catch
{
return default;
}
}
public static void SetInstanceField(object instance, string fieldName, object value)
{
try
{
if (instance == null) return;
var type = instance.GetType();
var field = type.GetField(fieldName, DefaultFlags);
field?.SetValue(instance, value);
}
catch (Exception ex)
{
MelonLogger.Warning($"[GameMethodInvoker] SetField failed: {ex.Message}");
}
}
private static MethodInfo FindMethod(Type type, string methodName, int argCount)
{
var methods = type.GetMethods(DefaultFlags);
foreach (var method in methods)
{
if (method.Name != methodName) continue;
var parameters = method.GetParameters();
if (parameters.Length != argCount) continue;
return method;
}
foreach (var method in methods)
{
if (method.Name != methodName) continue;
if (method.GetParameters().Length == 0 || argCount == 0) return method;
}
return null;
}
}
public static class GameUIElements
{
public static class Paths
{
public const string MainGameManager = "MainGameManager";
public const string PauseMenuCanvas = "PauseMenuCanvas";
public const string PauseMenu = "PauseMenu";
public const string MainMenu = "MainMenu";
public const string ComputerShop = "ComputerShop";
public const string GameManager = "GameManager";
}
public static class Methods
{
public const string LoadGame = "LoadGame";
public const string SaveGame = "SaveGame";
public const string Resume = "Resume";
public const string Pause = "Pause";
public const string OpenShop = "OpenShop";
public const string CloseShop = "CloseShop";
public const string ReturnToMenu = "ReturnToMenu";
}
public static class Types
{
public const string MainGameManager = "MainGameManager";
public const string GameManager = "GameManager";
public const string PauseMenu = "PauseMenu";
public const string MainMenu = "MainMenu";
public const string ComputerShop = "ComputerShop";
}
}
public static class GameUIButtons
{
public static bool ClickButton(string buttonPath)
{
var button = FindButton(buttonPath);
if (button == null) return false;
button.onClick?.Invoke();
MelonLogger.Msg($"[GameUIButtons] Clicked: {buttonPath}");
return true;
}
private static UnityEngine.UI.Button FindButton(string path)
{
var go = GameObject.Find(path);
if (go != null) return go.GetComponent<UnityEngine.UI.Button>();
// If not found (likely inactive), check all buttons
var allButtons = Resources.FindObjectsOfTypeAll<UnityEngine.UI.Button>();
foreach (var btn in allButtons)
{
if (GetGameObjectPath(btn.gameObject).Contains(path)) return btn;
}
return null;
}
private static string GetGameObjectPath(GameObject obj)
{
string path = "/" + obj.name;
while (obj.transform.parent != null)
{
obj = obj.transform.parent.gameObject;
path = "/" + obj.name + path;
}
return path;
}
public static bool ClickButtonByName(string buttonName)
{
var allButtons = Resources.FindObjectsOfTypeAll<UnityEngine.UI.Button>();
foreach (var btn in allButtons)
{
if (btn.name == buttonName)
{
btn.onClick?.Invoke();
MelonLogger.Msg($"[GameUIButtons] Clicked by name: {buttonName}");
return true;
}
}
return false;
}
public static bool ClickButtonInCanvas(string canvasName, string buttonName)
{
return ClickButton($"{canvasName}/{buttonName}");
}
public static bool ClickLoadButton()
{
return ClickButtonByName("LoadGame") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/LoadGame") || ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/LoadButton");
}
public static bool ClickNewGameButton()
{
return ClickButtonByName("NewGame") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/NewGame");
}
public static bool ClickContinueButton()
{
return ClickButtonByName("Continue") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/Continue");
}
public static bool ClickSaveButton()
{
return ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/SaveButton");
}
public static bool ClickResumeButton()
{
return ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/ResumeButton");
}
public static bool ClickMainMenuButton()
{
return ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/MainMenuButton");
}
public static bool ClickReportBugButton() => ClickButtonByName("Report Bug") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/Report Bug");
public static bool ClickDiscordButton() => ClickButtonByName("Discord") || ClickButton("Canvas - MainMenuScript/TopRight_VerticalLayout/Discord");
public static bool ClickWishlistButton() => ClickButtonByName("Wishlist") || ClickButton("Canvas - MainMenuScript/TopRight_VerticalLayout/Wishlist");
public static bool ClickTwitterButton() => ClickButton("Canvas - MainMenuScript/Twitter/Button");
public static bool ClickStatsButton() => ClickButtonByName("SteamStats") || ClickButton("Canvas - MainMenuScript/Leaderboards and stats/SteamStats");
}
-51
View File
@@ -1,51 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Il2CppTMPro;
namespace greg.Sdk.UI;
/// <summary>
/// Skeleton for Task 1.3: Standardized Button component.
/// </summary>
public class GregButton : MonoBehaviour
{
public GregButton(IntPtr ptr) : base(ptr) { }
public delegate void OnClickDelegate();
public OnClickDelegate OnClick;
public void Setup(string label)
{
var text = GetComponentInChildren<TextMeshProUGUI>();
if (text != null) text.text = label;
var btn = GetComponent<Button>();
if (btn != null)
{
btn.onClick.AddListener(new Action(() => OnClick?.Invoke()));
}
}
}
/// <summary>
/// Skeleton for Task 1.3: Standardized Toggle component.
/// </summary>
public class GregToggle : MonoBehaviour
{
public GregToggle(IntPtr ptr) : base(ptr) { }
public delegate void OnToggleDelegate(bool value);
public OnToggleDelegate OnValueChanged;
public void Setup(bool initialState)
{
var toggle = GetComponent<Toggle>();
if (toggle != null)
{
toggle.isOn = initialState;
toggle.onValueChanged.AddListener(new Action<bool>((val) => OnValueChanged?.Invoke(val)));
}
}
}
-78
View File
@@ -1,78 +0,0 @@
using System;
using System.Collections.Generic;
using MelonLoader;
using UnityEngine;
namespace greg.Core.UI;
/// <summary>
/// Central registry for all gregUI elements.
/// Lifecycle: Init → Register → OnSceneLoad → Teardown
/// </summary>
public static class GregUIManager
{
private static readonly Dictionary<string, IGregUIElement> _registry = new();
private static Canvas _rootCanvas;
public static bool IsReady => _rootCanvas != null;
public static void Init()
{
// Hook into scene load to find root canvas
// The game uses StaticUIElements which has a canvasStatic.
// We'll also look for any canvas if that fails.
}
public static void OnSceneLoaded(string sceneName)
{
// Search for the root canvas.
// Based on analysis, "canvasStatic" is a strong candidate, or just find any root canvas.
_rootCanvas = GameObject.FindObjectOfType<Canvas>();
if (_rootCanvas == null)
{
MelonLogger.Warning($"[GregUIManager] Root canvas not found in scene: {sceneName}");
return;
}
MelonLogger.Msg($"[GregUIManager] Root canvas found: {_rootCanvas.name}");
// Re-attach all registered panels
foreach (var element in _registry.Values)
{
try
{
element.Attach(_rootCanvas);
}
catch (Exception ex)
{
MelonLogger.Error($"[GregUIManager] Failed to attach element {element.PanelId}: {ex.Message}");
}
}
}
public static void Register(string panelId, IGregUIElement element)
{
_registry[panelId] = element;
if (_rootCanvas != null)
{
element.Attach(_rootCanvas);
}
}
public static T Get<T>(string panelId) where T : class, IGregUIElement
{
return _registry.TryGetValue(panelId, out var el) ? el as T : null;
}
public static void Teardown()
{
foreach (var el in _registry.Values)
{
try { el.Destroy(); } catch { }
}
_registry.Clear();
_rootCanvas = null;
}
}

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