feat: Implement core performance and automation modules
- 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:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" };
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace gregCore.Core.Models;
|
||||
|
||||
public enum CableType
|
||||
{
|
||||
Cat5,
|
||||
Cat6,
|
||||
Cat6A,
|
||||
Fiber,
|
||||
PowerAC,
|
||||
PowerDC,
|
||||
Console
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace gregCore.Core.Models;
|
||||
|
||||
public enum OperationPriority
|
||||
{
|
||||
Critical = 0,
|
||||
High = 1,
|
||||
Normal = 2,
|
||||
Low = 3,
|
||||
Background = 4
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"]));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
namespace gregCore.PublicApi.Types;
|
||||
public record DeviceInfo(string Id, string Name, string IpAddress, bool IsOnline);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
namespace gregCore.PublicApi.Types;
|
||||
public record struct MoneyResult(float NewBalance, float Delta, bool Success);
|
||||
@@ -0,0 +1,2 @@
|
||||
namespace gregCore.PublicApi.Types;
|
||||
public record ServerInfo(string Id, string Type, int RackId, int Slot, float PowerUsage);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user