chore: add DataCenter-RustBridge submodule

Initialize the DataCenter-RustBridge submodule to integrate the Rust bridge plugin for the DataCenter project. This enables using the external Rust-based components as a git submodule.
This commit is contained in:
Marvin
2026-04-20 14:48:52 +02:00
parent 49b3b472f6
commit 84b6f56a50
93 changed files with 38893 additions and 264 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "plugins/DataCenter-RustBridge"]
path = plugins/DataCenter-RustBridge
url = https://github.com/mleem97/DataCenter-RustBridge.git
+1
View File
@@ -0,0 +1 @@
1.1.0-prod
Binary file not shown.
@@ -0,0 +1,29 @@
# gregCore Hook API Tutorial
The gregCore Hook API is a powerful, event-driven system that allows mods to interact with the game in real-time. It is designed to be cross-language, stable, and easy to use.
## Core Concepts
- **Hook Name**: A string that identifies the hook, following the schema `greg.DOMAIN.Class.Method` (e.g., `greg.PLAYER.CoinChanged`).
- **Trigger**: When the hook is fired (e.g., "NativePatch", "LuaMod").
- **Payload**: A standardized data object containing `hook_name`, `trigger`, and a `data` dictionary.
## Supported Languages
Click on a language to view its specific tutorial:
- [C# Tutorial](./csharp.md)
- [Lua Tutorial](./lua.md)
- [Python Tutorial](./python.md)
- [Rust Tutorial](./rust.md)
- [Go Tutorial](./go.md)
- [JavaScript/TypeScript Tutorial](./javascript.md)
## Common Hook List
You can find the full list of 1771 available hooks in the [Hooks Catalog](../../api-reference/hooks-catalog.md).
### Example Hooks:
- `greg.PLAYER.CoinChanged`: Fired when the player's money changes.
- `greg.SYSTEM.GameSaved`: Fired when the game is saved.
- `greg.UI.PauseMenu.Opened`: Fired when the pause menu is opened.
+57
View File
@@ -0,0 +1,57 @@
package main
/*
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint32_t api_version;
void (*log_info)(const char*);
void (*log_warning)(const char*);
void (*log_error)(const char*);
double (*get_player_money)();
// ... rest of fields
} GregCoreAPI;
typedef struct {
const char* id;
const char* name;
const char* version;
const char* author;
const char* description;
uint32_t api_version;
} GregModInfo;
*/
import "C"
import "unsafe"
var api *C.GregCoreAPI
//export greg_mod_info
func greg_mod_info() C.GregModInfo {
return C.GregModInfo{
id: C.CString("go_example"),
name: C.CString("Go Example Mod"),
version: C.CString("1.0.0"),
author: C.CString("teamGreg"),
description: C.CString("A sample mod in Go."),
api_version: 1,
}
}
//export greg_mod_init
func greg_mod_init(api_ptr *C.GregCoreAPI) bool {
api = api_ptr
msg := C.CString("Go Mod Initialized!")
defer C.free(unsafe.Pointer(msg))
C.bridge_log_info(api.log_info, msg)
return true
}
// Helper to call C function pointers
//go:uintptrescapes
func callLog(fn unsafe.Pointer, msg *C.char) {
// This requires cgo bridge helpers usually
}
func main() {}
+27
View File
@@ -0,0 +1,27 @@
-- gregCore Lua Example Mod
function on_init()
greg.log_info("Lua Example Mod geladen!")
greg.show_notification("Lua Mod Initialisiert")
-- Event abonnieren
greg.subscribe_event(100, function(data)
greg.log_info("Geld hat sich geändert! Neuer Stand: " .. greg.get_player_money())
end)
end
function on_update(dt)
-- Wird jeden Frame aufgerufen
end
function on_event(event_id, data)
-- Generischer Event-Handler
end
function on_scene_loaded(name)
greg.log_info("Szene geladen: " .. name)
end
function on_shutdown()
greg.log_info("Lua Mod wird beendet.")
end
+7
View File
@@ -0,0 +1,7 @@
{
"id": "example_mod",
"name": "Example Lua Mod",
"version": "1.0.0",
"author": "teamGreg",
"description": "Ein Beispiel-Mod für das gregCore Lua FFI."
}
+17
View File
@@ -0,0 +1,17 @@
def on_init():
greg.log_info("Python Example Mod initialized!")
greg.show_notification("Python Mod Active")
def on_update(dt):
# dt is deltaTime
pass
def on_event(event_id, data):
if event_id == 100: # MoneyChanged
greg.log_info("Money changed! Current: " + str(greg.get_player_money()))
def on_scene_loaded(name):
greg.log_info("Entered scene: " + name)
def on_shutdown():
greg.log_info("Python Mod shutting down.")
+53
View File
@@ -0,0 +1,53 @@
// gregCore Rust Example Mod
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
#[repr(C)]
pub struct GregModInfo {
pub id: *const c_char,
pub name: *const c_char,
pub version: *const c_char,
pub author: *const c_char,
pub description: *const c_char,
pub api_version: u32,
}
#[repr(C)]
pub struct GregCoreAPI {
pub api_version: u32,
pub log_info: extern "C" fn(*const c_char),
pub log_warning: extern "C" fn(*const c_char),
pub log_error: extern "C" fn(*const c_char),
pub get_player_money: extern "C" fn() -> f64,
// ... restliche Felder
}
static mut API: Option<&GregCoreAPI> = None;
#[no_mangle]
pub extern "C" fn greg_mod_info() -> GregModInfo {
GregModInfo {
id: b"rust_example\0".as_ptr() as *const c_char,
name: b"Rust Example Mod\0".as_ptr() as *const c_char,
version: b"1.0.0\0".as_ptr() as *const c_char,
author: b"teamGreg\0".as_ptr() as *const c_char,
description: b"Ein Beispiel-Mod in Rust.\0".as_ptr() as *const c_char,
api_version: 1,
}
}
#[no_mangle]
pub extern "C" fn greg_mod_init(api: *const GregCoreAPI) -> bool {
unsafe {
API = Some(&*api);
let msg = CString::new("Rust Mod Initialisiert!").unwrap();
((*api).log_info)(msg.as_ptr());
}
true
}
#[no_mangle]
pub extern "C" fn greg_mod_update(dt: f32) {
// Logik pro Frame
}
+21848
View File
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -12,17 +12,15 @@
<!-- ── NuGet Identity ────────────────────────────────────────── -->
<PackageId>gregCore</PackageId>
<Version>0.1.0</Version>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<Authors>mleem97</Authors>
<Company>mleem97</Company>
<Version>1.0.0.33-pre</Version>
<AssemblyVersion>1.0.0.33</AssemblyVersion>
<FileVersion>1.0.0.33</FileVersion>
<Authors>TeamGreg</Authors>
<Company>TeamGreg</Company>
<Product>gregCore</Product>
<Description>
Framework SDK for Data Center (Unity 6 IL2CPP) MelonLoader mods.
Provides GregHookBus, Services, Registries and Harmony-safe patch
infrastructure. Reference-only package — runtime DLL ships separately
via MelonLoader Mods folder.
gregCore Modding Framework r Data Center
Inkludiert C# Compatibility Layer für DataCenter-RustBridge.
</Description>
<PackageTags>melonloader;unity;il2cpp;modding;datacenter;gregcore</PackageTags>
<PackageProjectUrl>https://github.com/mleem97/gregCore</PackageProjectUrl>
@@ -91,10 +89,13 @@
<ItemGroup>
<PackageReference Include="Jint" Version="4.1.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
<PackageReference Include="pythonnet" Version="3.0.3" />
</ItemGroup>
<ItemGroup>
<Compile Remove="tests\**" />
<Compile Remove="plugins\DataCenter-RustBridge\**" />
<EmbeddedResource Remove="tests\**" />
<None Remove="tests\**" />
</ItemGroup>
+263
View File
@@ -0,0 +1,263 @@
using MelonLoader;
using MelonLoader.Utils;
using System;
using System.IO;
using HarmonyLib;
using UnityEngine;
[assembly: MelonInfo(typeof(DataCenterModLoader.Core), "gregCore", "1.0.0", "TeamGreg")]
[assembly: MelonGame("", "Data Center")]
namespace DataCenterModLoader;
// file-based crash logger, never throws
public static class CrashLog
{
private static string _logPath;
private static readonly object _lock = new();
public static void Init(string gameRoot)
{
try
{
_logPath = Path.Combine(gameRoot, "dc_modloader_debug.log");
var header =
$"===== RustBridge Debug Log ====={Environment.NewLine}" +
$"Started: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}{Environment.NewLine}" +
$"========================================={Environment.NewLine}";
File.WriteAllText(_logPath, header);
}
catch { }
}
public static void Log(string msg)
{
try
{
if (_logPath == null) return;
lock (_lock)
{
File.AppendAllText(_logPath,
$"[{DateTime.Now:HH:mm:ss.fff}] {msg}{Environment.NewLine}");
}
}
catch { }
}
public static void LogException(string context, Exception ex)
{
try
{
if (_logPath == null) return;
lock (_lock)
{
File.AppendAllText(_logPath,
$"[{DateTime.Now:HH:mm:ss.fff}] EXCEPTION in {context}:{Environment.NewLine}" +
$" Type: {ex.GetType().FullName}{Environment.NewLine}" +
$" Message: {ex.Message}{Environment.NewLine}" +
$" StackTrace:{Environment.NewLine}{ex.StackTrace}{Environment.NewLine}" +
(ex.InnerException != null
? $" InnerException: {ex.InnerException.GetType().FullName}: {ex.InnerException.Message}{Environment.NewLine}" +
$" InnerStackTrace:{Environment.NewLine}{ex.InnerException.StackTrace}{Environment.NewLine}"
: "") +
Environment.NewLine);
}
}
catch { }
}
}
public class Core : MelonMod
{
public static Core Instance { get; private set; }
private FFIBridge _ffiBridge;
private MultiplayerBridge _mpBridge;
private string _modsPath;
public override void OnInitializeMelon()
{
try
{
Instance = this;
CrashLog.Init(MelonEnvironment.GameRootDirectory);
CrashLog.Log("step: CrashLog initialized");
_modsPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Mods", "native");
LoggerInstance.Msg("╔══════════════════════════════════════════╗");
LoggerInstance.Msg("║ Rust Bridge v0.1.0 ║");
LoggerInstance.Msg("║ Rust FFI Bridge Active ║");
LoggerInstance.Msg("╚══════════════════════════════════════════╝");
if (!Directory.Exists(_modsPath))
{
Directory.CreateDirectory(_modsPath);
LoggerInstance.Msg($"Created Mods/native directory: {_modsPath}");
}
CrashLog.Log("step: creating FFIBridge");
_ffiBridge = new FFIBridge(LoggerInstance, _modsPath);
CrashLog.Log("step: initializing EventDispatcher");
EventDispatcher.Initialize(_ffiBridge, LoggerInstance);
CrashLog.Log("step: applying Harmony patches");
try
{
HarmonyInstance.PatchAll(typeof(Core).Assembly);
LoggerInstance.Msg("Harmony patches applied.");
CrashLog.Log("step: Harmony patches applied successfully");
}
catch (Exception ex)
{
LoggerInstance.Error($"Failed to apply Harmony patches: {ex.Message}");
LoggerInstance.Msg("Continuing without full event support.");
CrashLog.LogException("Harmony patching", ex);
}
CrashLog.Log("step: initializing ModConfigSystem");
ModConfigSystem.Initialize(LoggerInstance);
CrashLog.Log("step: loading all mods");
_ffiBridge.LoadAllMods();
if (!_ffiBridge.IsRustAvailable)
{
LoggerInstance.Warning("═══════════════════════════════════════════════════════════");
LoggerInstance.Warning("⚠ Rust Bridge: Running in C# Compatibility Mode Only");
LoggerInstance.Warning($" → {_ffiBridge.RustStatusMessage}");
LoggerInstance.Warning("═══════════════════════════════════════════════════════════");
}
else
{
LoggerInstance.Msg($"✓ {_ffiBridge.RustStatusMessage}");
}
var mpDllPath = Path.Combine(_modsPath, "dc_multiplayer.dll");
if (File.Exists(mpDllPath))
{
_mpBridge = new MultiplayerBridge(LoggerInstance);
}
LoggerInstance.Msg("Modloader initialization complete.");
CrashLog.Log("step: OnInitializeMelon complete");
}
catch (Exception ex)
{
CrashLog.LogException("OnInitializeMelon", ex);
throw;
}
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
try
{
_ffiBridge?.OnSceneLoaded(sceneName);
_mpBridge?.OnSceneLoaded(sceneName);
ModConfigSystem.OnSceneLoaded(sceneName);
CustomEmployeeManager.ResetInjectionState();
}
catch (Exception ex)
{
CrashLog.LogException("OnSceneWasLoaded", ex);
}
}
// Drain the TechnicianManager.pendingDispatches queue periodically so that jobs
// queued by the game's own "Add all broken devices" button (or restored from a
// save) are assigned to free technicians even when no CommandCenterOperator is hired.
private float _queueDrainTimer = 0f;
private const float QUEUE_DRAIN_INTERVAL = 2f;
public override void OnUpdate()
{
try
{
_ffiBridge?.OnUpdate(Time.deltaTime);
_mpBridge?.OnUpdate(Time.deltaTime);
ModConfigSystem.OnUpdate(Time.deltaTime);
CustomEmployeeManager.ReregisterSalariesIfNeeded();
EntityManager.Update();
CarryStateMonitor.Update();
// Periodically force-process any pending dispatch queue entries that
// the game's ProcessDispatchQueue coroutine would normally handle only
// when a CommandCenterOperator is hired.
_queueDrainTimer += Time.deltaTime;
if (_queueDrainTimer >= QUEUE_DRAIN_INTERVAL)
{
_queueDrainTimer = 0f;
try
{
var tm = Il2Cpp.TechnicianManager.instance;
if (tm != null)
GameHooks.ForceProcessPendingQueue(tm);
}
catch { }
}
}
catch (Exception ex)
{
CrashLog.LogException("OnUpdate", ex);
}
}
public override void OnFixedUpdate()
{
try
{
_ffiBridge?.OnFixedUpdate(Time.fixedDeltaTime);
}
catch (Exception ex)
{
CrashLog.LogException("OnFixedUpdate", ex);
}
}
public override void OnGUI()
{
try
{
_mpBridge?.DrawGUI();
ModConfigSystem.DrawGUI();
// Show Rust Bridge status in top-left corner
if (_ffiBridge != null && !_ffiBridge.IsRustAvailable)
{
var oldColor = GUI.color;
GUI.color = new Color(1f, 0.6f, 0f, 0.8f);
GUI.Label(new Rect(10, 10, 400, 20), $"[RustBridge] C# Compatibility Mode");
GUI.color = oldColor;
}
}
catch (Exception ex)
{
CrashLog.LogException("OnGUI", ex);
}
}
public override void OnApplicationQuit()
{
try
{
LoggerInstance.Msg("Shutting down modloader...");
CrashLog.Log("step: OnApplicationQuit starting");
EntityManager.DestroyAll();
_mpBridge?.Shutdown();
ModConfigSystem.Shutdown();
_ffiBridge?.Shutdown();
_ffiBridge?.Dispose();
CrashLog.Log("step: OnApplicationQuit complete");
}
catch (Exception ex)
{
CrashLog.LogException("OnApplicationQuit", ex);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,812 @@
using System;
using System.Collections.Generic;
using MelonLoader;
using UnityEngine;
using UnityEngine.AI;
using Il2Cpp;
using Il2CppUMA;
using Il2CppUMA.CharacterSystem;
using Il2CppTMPro;
namespace DataCenterModLoader;
public static class EntityManager
{
private class ManagedEntity
{
public uint Id;
public GameObject GO;
public Animator Animator;
public NavMeshAgent NavAgent;
public bool WaitingForUMA;
public float UMAWaitStart;
public int SpeedParamHash;
public int WalkingParamHash;
public bool HasSpeedParam;
public bool HasWalkingParam;
public bool AnimParamsDiscovered;
public int CrouchParamHash;
public int SittingParamHash;
public int CarryingParamHash;
public bool HasCrouchParam;
public bool HasSittingParam;
public bool HasCarryingParam;
public GameObject NameTagGO;
public Vector3 LastPos;
public GameObject CarryProxyGO;
public Transform HandBone;
public bool HandBoneSearched;
public bool ColliderAdded;
}
private static readonly Dictionary<uint, ManagedEntity> _entities = new();
private static uint _nextId = 1;
private static bool _gameOffsetsLogged = false;
public static uint SpawnCharacter(uint prefabIdx, float x, float y, float z, float rotY, string name)
{
try
{
GameObject go = null;
var mgr = MainGameManager.instance;
if (mgr != null && mgr.techniciansPrefabs != null && mgr.techniciansPrefabs.Length > 0)
{
int idx = (int)(prefabIdx % (uint)mgr.techniciansPrefabs.Length);
var prefab = mgr.techniciansPrefabs[idx];
bool prefabWasActive = prefab.activeSelf;
prefab.SetActive(false);
go = UnityEngine.Object.Instantiate(prefab);
if (prefabWasActive) prefab.SetActive(true);
}
else
{
go = GameObject.CreatePrimitive(PrimitiveType.Capsule);
go.transform.position = new Vector3(x, y, z);
var col = go.GetComponent<Collider>();
if (col != null) UnityEngine.Object.Destroy(col);
uint capsuleId = _nextId++;
var capsuleEntity = new ManagedEntity
{
Id = capsuleId,
GO = go,
WaitingForUMA = false,
};
go.name = $"Entity_{capsuleId}";
AddNameTag(go, name, capsuleEntity);
_entities[capsuleId] = capsuleEntity;
CrashLog.Log($"[EntityManager] Spawned capsule fallback entity {capsuleId} '{name}'");
return capsuleId;
}
go.SetActive(false);
var spawnPos = new Vector3(x, y, z);
go.transform.position = spawnPos;
go.transform.eulerAngles = new Vector3(0, rotY, 0);
var navCheck = go.GetComponent<NavMeshAgent>();
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null) continue;
string typeName = mb.GetIl2CppType().Name;
if (typeName.Contains("UMA") || typeName.Contains("DynamicCharacter") ||
typeName.Contains("Avatar") || typeName.Contains("Generator") ||
typeName == "Animator" || typeName.Contains("Renderer"))
continue;
try { mb.enabled = false; } catch { }
}
if (navCheck != null)
try { UnityEngine.Object.DestroyImmediate(navCheck); } catch { }
foreach (var cc in go.GetComponentsInChildren<CharacterController>(true))
try { UnityEngine.Object.DestroyImmediate(cc); } catch { }
foreach (var c in go.GetComponentsInChildren<Collider>(true))
try { UnityEngine.Object.DestroyImmediate(c); } catch { }
foreach (var rb in go.GetComponentsInChildren<Rigidbody>(true))
try { UnityEngine.Object.DestroyImmediate(rb); } catch { }
foreach (var nav in go.GetComponentsInChildren<NavMeshAgent>(true))
try { UnityEngine.Object.DestroyImmediate(nav); } catch { }
go.SetActive(true);
Animator animator = go.GetComponentInChildren<Animator>();
if (animator != null)
animator.applyRootMotion = false;
uint id = _nextId++;
go.name = $"Entity_{id}";
var entity = new ManagedEntity
{
Id = id,
GO = go,
Animator = animator,
NavAgent = null, // NavMeshAgent destroyed — remote entities don't need pathfinding
WaitingForUMA = true,
UMAWaitStart = Time.time,
LastPos = spawnPos,
};
AddNameTag(go, name, entity);
_entities[id] = entity;
CrashLog.Log($"[EntityManager] Spawned entity {id} '{name}' at ({spawnPos.x:F1},{spawnPos.y:F1},{spawnPos.z:F1}) anim={animator != null}");
return id;
}
catch (Exception ex)
{
CrashLog.LogException("EntityManager.SpawnCharacter", ex);
return 0;
}
}
public static void DestroyEntity(uint entityId)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.CarryProxyGO != null) UnityEngine.Object.Destroy(entity.CarryProxyGO);
if (entity.NameTagGO != null) UnityEngine.Object.Destroy(entity.NameTagGO);
if (entity.GO != null) UnityEngine.Object.Destroy(entity.GO);
_entities.Remove(entityId);
}
public static void SetPosition(uint entityId, float x, float y, float z, float rotY)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.GO == null) { _entities.Remove(entityId); return; }
// Direct transform — no NavMeshAgent involvement for remote entities
entity.GO.transform.position = new Vector3(x, y, z);
entity.GO.transform.eulerAngles = new Vector3(0f, rotY, 0f);
}
public static bool IsEntityReady(uint entityId)
{
if (!_entities.TryGetValue(entityId, out var entity)) return false;
return !entity.WaitingForUMA;
}
public static void SetAnimation(uint entityId, float speed, bool isWalking)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.Animator == null) return;
try
{
if (entity.HasSpeedParam)
{
// Smooth the speed to avoid jittery animation blending
float current = entity.Animator.GetFloat(entity.SpeedParamHash);
float smoothed = Mathf.Lerp(current, speed, Time.deltaTime * 8f);
entity.Animator.SetFloat(entity.SpeedParamHash, smoothed);
}
if (entity.HasWalkingParam)
entity.Animator.SetBool(entity.WalkingParamHash, isWalking);
}
catch { }
}
/// <summary>Set just the carry animator bool (cheap, can be called every frame)</summary>
public static void SetCarryAnim(uint entityId, bool isCarrying)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.Animator == null || !entity.HasCarryingParam) return;
try { entity.Animator.SetBool(entity.CarryingParamHash, isCarrying); }
catch { }
}
/// <summary>Create a visual proxy from real game prefab, parented to entity root</summary>
public static void CreateCarryVisual(uint entityId, uint objectInHandType)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
try
{
LogGameItemOffsets();
// Destroy existing proxy if any
if (entity.CarryProxyGO != null)
{
UnityEngine.Object.Destroy(entity.CarryProxyGO);
entity.CarryProxyGO = null;
}
// Find hand bone if not searched yet
if (!entity.HandBoneSearched && entity.GO != null)
{
entity.HandBoneSearched = true;
entity.HandBone = FindHandBone(entity.GO.transform);
if (entity.HandBone != null)
CrashLog.Log($"[EntityManager] Found hand bone '{entity.HandBone.name}' for entity {entity.Id}");
else
CrashLog.Log($"[EntityManager] No hand bone found for entity {entity.Id}");
}
// Try real game prefab first, fall back to primitive
GameObject proxy = TryCreateFromGamePrefab(objectInHandType);
if (proxy == null)
proxy = CreateFallbackProxy(objectInHandType);
if (proxy != null)
{
Transform parent = entity.GO?.transform;
if (parent != null)
{
proxy.transform.SetParent(parent, false);
proxy.transform.localPosition = Vector3.zero;
proxy.transform.localRotation = Quaternion.identity;
}
entity.CarryProxyGO = proxy;
}
CrashLog.Log($"[EntityManager] Created carry visual type={objectInHandType} prefab={proxy != null} for entity {entity.Id} parent={proxy?.transform.parent?.name ?? "none"} (using entity root)");
}
catch (Exception ex) { CrashLog.LogException("EntityManager.CreateCarryVisual", ex); }
}
/// <summary>Destroy the carry visual proxy</summary>
public static void DestroyCarryVisual(uint entityId)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.CarryProxyGO != null)
{
UnityEngine.Object.Destroy(entity.CarryProxyGO);
entity.CarryProxyGO = null;
}
}
private static void LogGameItemOffsets()
{
if (_gameOffsetsLogged) return;
_gameOffsetsLogged = true;
try
{
// Log moveItemPosition from PlayerManager
var pm = PlayerManager.instance;
if (pm != null && pm.moveItemPosition != null)
{
var mip = pm.moveItemPosition;
CrashLog.Log($"[CarryDebug] moveItemPosition localPos=({mip.localPosition.x:F3},{mip.localPosition.y:F3},{mip.localPosition.z:F3}) localRot=({mip.localEulerAngles.x:F1},{mip.localEulerAngles.y:F1},{mip.localEulerAngles.z:F1})");
}
// Log objectInHandGO array
if (pm != null && pm.objectInHandGO != null)
{
CrashLog.Log($"[CarryDebug] objectInHandGO.Length={pm.objectInHandGO.Length}");
for (int i = 0; i < pm.objectInHandGO.Length; i++)
{
var go = pm.objectInHandGO[i];
if (go != null)
CrashLog.Log($"[CarryDebug] [{i}] name='{go.name}' active={go.activeSelf} localPos=({go.transform.localPosition.x:F3},{go.transform.localPosition.y:F3},{go.transform.localPosition.z:F3}) localRot=({go.transform.localEulerAngles.x:F1},{go.transform.localEulerAngles.y:F1},{go.transform.localEulerAngles.z:F1}) localScale=({go.transform.localScale.x:F3},{go.transform.localScale.y:F3},{go.transform.localScale.z:F3})");
else
CrashLog.Log($"[CarryDebug] [{i}] null");
}
}
// Find all UsableObject instances and log their offsets
var usableObjects = UnityEngine.Object.FindObjectsOfType<UsableObject>();
if (usableObjects != null)
{
CrashLog.Log($"[CarryDebug] Found {usableObjects.Length} UsableObject instances in scene");
// Log just a few unique types to avoid spam
var loggedTypes = new HashSet<int>();
foreach (var uo in usableObjects)
{
if (uo == null) continue;
int typeVal = (int)uo.objectInHandType;
if (loggedTypes.Contains(typeVal)) continue;
loggedTypes.Add(typeVal);
try
{
CrashLog.Log($"[CarryDebug] UsableObject type={typeVal} ({uo.objectInHandType}) name='{uo.gameObject.name}' offsetPivotPos=({uo.offsetPivotPosition.x:F3},{uo.offsetPivotPosition.y:F3},{uo.offsetPivotPosition.z:F3}) offsetPivotRot=({uo.offsetPivotRotation.x:F1},{uo.offsetPivotRotation.y:F1},{uo.offsetPivotRotation.z:F1})");
}
catch (Exception ex)
{
CrashLog.Log($"[CarryDebug] Failed to read UsableObject type={typeVal}: {ex.Message}");
}
}
}
}
catch (Exception ex)
{
CrashLog.LogException("[CarryDebug] LogGameItemOffsets", ex);
}
}
public static Vector3? GetEntityPosition(uint entityId)
{
if (!_entities.TryGetValue(entityId, out var entity)) return null;
if (entity.GO == null) return null;
return entity.GO.transform.position;
}
public static void AddEntityCollider(uint entityId)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.ColliderAdded || entity.GO == null) return;
try
{
var capsule = entity.GO.AddComponent<CapsuleCollider>();
capsule.center = new Vector3(0f, 0.9f, 0f);
capsule.radius = 0.3f;
capsule.height = 1.8f;
entity.ColliderAdded = true;
CrashLog.Log($"[EntityManager] Added collision capsule to entity {entity.Id}");
}
catch (Exception ex)
{
CrashLog.LogException($"[EntityManager] Failed to add collider to entity {entity.Id}", ex);
}
}
public static void SetEntityCarryTransform(uint entityId, float posX, float posY, float posZ, float rotX, float rotY, float rotZ)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.CarryProxyGO == null) return;
entity.CarryProxyGO.transform.localPosition = new Vector3(posX, posY, posZ);
entity.CarryProxyGO.transform.localRotation = Quaternion.Euler(rotX, rotY, rotZ);
}
/// <summary>Find the right hand bone in a humanoid UMA rig</summary>
private static Transform FindHandBone(Transform root)
{
// UMA humanoid rigs use standard naming; search for right hand
string[] handNames = { "Right Hand", "RightHand", "Hand_R", "hand_r", "R_Hand", "Bip01 R Hand" };
foreach (var name in handNames)
{
var bone = FindChildRecursive(root, name);
if (bone != null) return bone;
}
// Fallback: search for any transform containing "hand" and "r" (case insensitive)
return FindChildByPattern(root, t =>
{
string n = t.name.ToLower();
return n.Contains("hand") && (n.Contains("right") || (n.Contains("_r") || n.Contains(".r") || n.StartsWith("r_") || n.EndsWith(" r")));
});
}
private static Transform FindChildRecursive(Transform parent, string name)
{
if (parent.name == name) return parent;
for (int i = 0; i < parent.childCount; i++)
{
var found = FindChildRecursive(parent.GetChild(i), name);
if (found != null) return found;
}
return null;
}
private static Transform FindChildByPattern(Transform parent, Func<Transform, bool> predicate)
{
if (predicate(parent)) return parent;
for (int i = 0; i < parent.childCount; i++)
{
var found = FindChildByPattern(parent.GetChild(i), predicate);
if (found != null) return found;
}
return null;
}
/// <summary>Cached prefab templates per ObjectInHand type (stripped visual clones)</summary>
private static readonly Dictionary<uint, GameObject> _carryPrefabCache = new();
private static bool _prefabCacheAttempted = false;
/// <summary>Try to clone a real game prefab for the carried item type</summary>
private static GameObject TryCreateFromGamePrefab(uint objectInHandType)
{
try
{
// Check cache first
if (_carryPrefabCache.TryGetValue(objectInHandType, out var cachedTemplate))
{
if (cachedTemplate != null)
{
var clone = UnityEngine.Object.Instantiate(cachedTemplate);
clone.SetActive(true);
clone.name = $"CarryVisual_{objectInHandType}";
return clone;
}
return null;
}
var shop = UnityEngine.Object.FindObjectOfType<ComputerShop>();
if (shop == null || shop.shopItems == null)
{
CrashLog.Log("[EntityManager] ComputerShop not found, using fallback proxy");
return null;
}
PlayerManager.ObjectInHand targetType = (PlayerManager.ObjectInHand)(int)objectInHandType;
foreach (var shopItem in shop.shopItems)
{
if (shopItem == null || shopItem.shopItemSO == null) continue;
if (shopItem.shopItemSO.itemType != targetType) continue;
int itemID = shopItem.shopItemSO.itemID;
var prefab = shop.GetPrefabForItem(itemID, targetType);
if (prefab == null) continue;
var template = UnityEngine.Object.Instantiate(prefab);
StripToVisualOnly(template);
template.SetActive(false);
template.name = $"CarryTemplate_{objectInHandType}";
UnityEngine.Object.DontDestroyOnLoad(template);
_carryPrefabCache[objectInHandType] = template;
CrashLog.Log($"[EntityManager] Cached carry prefab for type {objectInHandType} (itemID={itemID})");
var instance = UnityEngine.Object.Instantiate(template);
instance.SetActive(true);
instance.name = $"CarryVisual_{objectInHandType}";
return instance;
}
// No matching shop item found, cache null to avoid retrying
CrashLog.Log($"[EntityManager] No shop prefab found for type {objectInHandType}");
_carryPrefabCache[objectInHandType] = null;
return null;
}
catch (Exception ex)
{
CrashLog.LogException("EntityManager.TryCreateFromGamePrefab", ex);
_carryPrefabCache[objectInHandType] = null;
return null;
}
}
/// <summary>Strip all non-visual components from a GameObject (physics, scripts, nav)</summary>
private static void StripToVisualOnly(GameObject go)
{
// Remove all colliders
foreach (var col in go.GetComponentsInChildren<Collider>(true))
try { UnityEngine.Object.DestroyImmediate(col); } catch { }
// Remove all rigidbodies
foreach (var rb in go.GetComponentsInChildren<Rigidbody>(true))
try { UnityEngine.Object.DestroyImmediate(rb); } catch { }
// Remove NavMeshAgents
foreach (var nav in go.GetComponentsInChildren<NavMeshAgent>(true))
try { UnityEngine.Object.DestroyImmediate(nav); } catch { }
// Remove CharacterControllers
foreach (var cc in go.GetComponentsInChildren<CharacterController>(true))
try { UnityEngine.Object.DestroyImmediate(cc); } catch { }
// Remove all game scripts (MonoBehaviours) — keeps Transform, MeshFilter, MeshRenderer, etc.
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
try { UnityEngine.Object.DestroyImmediate(mb); } catch { }
// Disable animators (don't want independent animation)
foreach (var anim in go.GetComponentsInChildren<Animator>(true))
try { anim.enabled = false; } catch { }
}
/// <summary>Create a primitive fallback when real prefab isn't available</summary>
private static GameObject CreateFallbackProxy(uint objectInHandType)
{
try
{
var proxy = new GameObject($"CarryFallback_{objectInHandType}");
var visual = GameObject.CreatePrimitive(PrimitiveType.Cube);
// Remove collider
var col = visual.GetComponent<Collider>();
if (col != null) UnityEngine.Object.DestroyImmediate(col);
visual.transform.SetParent(proxy.transform, false);
Vector3 scale;
Color color;
switch (objectInHandType)
{
case 1: // Server1U
scale = new Vector3(0.43f, 0.045f, 0.5f);
color = new Color(0.2f, 0.2f, 0.25f);
break;
case 2: // Server2U
scale = new Vector3(0.43f, 0.09f, 0.5f);
color = new Color(0.2f, 0.2f, 0.25f);
break;
case 3: // Server3U
scale = new Vector3(0.43f, 0.135f, 0.5f);
color = new Color(0.2f, 0.2f, 0.25f);
break;
case 4: // Switch
scale = new Vector3(0.43f, 0.045f, 0.3f);
color = new Color(0.15f, 0.3f, 0.15f);
break;
case 5: // Rack
scale = new Vector3(0.6f, 1.2f, 0.8f);
color = new Color(0.3f, 0.3f, 0.3f);
break;
case 6: // CableSpinner
scale = new Vector3(0.15f, 0.15f, 0.15f);
color = new Color(0.4f, 0.3f, 0.1f);
break;
case 7: // PatchPanel
scale = new Vector3(0.43f, 0.045f, 0.3f);
color = new Color(0.25f, 0.25f, 0.3f);
break;
case 8: // SFPModule
scale = new Vector3(0.02f, 0.01f, 0.06f);
color = new Color(0.6f, 0.6f, 0.6f);
break;
case 9: // SFPBox
scale = new Vector3(0.1f, 0.06f, 0.08f);
color = new Color(0.35f, 0.35f, 0.4f);
break;
default:
scale = new Vector3(0.3f, 0.15f, 0.4f);
color = new Color(0.25f, 0.25f, 0.3f);
break;
}
visual.transform.localScale = scale;
var renderer = visual.GetComponent<Renderer>();
if (renderer != null)
{
try
{
var mat = new Material(Shader.Find("Standard"));
mat.color = color;
renderer.material = mat;
}
catch { }
}
return proxy;
}
catch (Exception ex)
{
CrashLog.LogException("EntityManager.CreateFallbackProxy", ex);
return null;
}
}
public static void SetCrouching(uint entityId, bool isCrouching)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.Animator == null) return;
try
{
if (entity.HasCrouchParam)
entity.Animator.SetBool(entity.CrouchParamHash, isCrouching);
}
catch { }
}
public static void SetSitting(uint entityId, bool isSitting)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.Animator == null) return;
try
{
if (entity.HasSittingParam)
entity.Animator.SetBool(entity.SittingParamHash, isSitting);
}
catch { }
}
public static uint GetPrefabCount()
{
try
{
var mgr = MainGameManager.instance;
if (mgr != null && mgr.techniciansPrefabs != null)
return (uint)mgr.techniciansPrefabs.Length;
}
catch { }
return 0;
}
public static void SetEntityName(uint entityId, string name)
{
if (!_entities.TryGetValue(entityId, out var entity)) return;
if (entity.NameTagGO == null) return;
try
{
var tmp = entity.NameTagGO.GetComponentInChildren<TextMeshProUGUI>();
if (tmp != null) tmp.text = name;
}
catch { }
}
public static void Update()
{
foreach (var kvp in _entities)
{
var entity = kvp.Value;
if (!entity.WaitingForUMA) continue;
if (entity.GO == null) continue;
var umaData = entity.GO.GetComponentInChildren<UMAData>(true);
bool meshReady = false;
int rendererCount = 0;
if (umaData != null && umaData.isOfficiallyCreated)
{
try
{
var rends = umaData.GetRenderers();
if (rends != null)
{
for (int r = 0; r < rends.Length; r++)
{
var smr = rends[r];
if (smr != null && smr.sharedMesh != null)
rendererCount++;
}
}
}
catch (Exception ex)
{
CrashLog.LogException($"[EntityManager] UMAData.GetRenderers() error: {ex.Message}", ex);
}
meshReady = rendererCount > 0;
}
if (meshReady)
{
CrashLog.Log($"[EntityManager] UMA mesh ready for entity {entity.Id} {rendererCount} renderer");
foreach (var mb in entity.GO.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null) continue;
string typeName = mb.GetIl2CppType().Name;
if (typeName == "Animator" || typeName.Contains("Renderer")) continue;
try { mb.enabled = false; } catch { }
}
entity.WaitingForUMA = false;
// Disable NavMeshAgent — remote entities don't need pathfinding
if (entity.NavAgent != null && entity.NavAgent.enabled)
entity.NavAgent.enabled = false;
if (!entity.AnimParamsDiscovered)
{
if (entity.Animator == null)
entity.Animator = entity.GO.GetComponentInChildren<Animator>();
if (entity.Animator != null)
{
entity.Animator.applyRootMotion = false;
try
{
foreach (var param in entity.Animator.parameters)
{
string lower = param.name.ToLower();
if (!entity.HasSpeedParam && param.type == AnimatorControllerParameterType.Float &&
(lower.Contains("speed") || lower.Contains("velocity") || lower.Contains("move") || lower.Contains("forward")))
{
entity.SpeedParamHash = param.nameHash;
entity.HasSpeedParam = true;
}
if (!entity.HasWalkingParam && param.type == AnimatorControllerParameterType.Bool &&
(lower.Contains("walk") || lower.Contains("moving") || lower.Contains("run")))
{
entity.WalkingParamHash = param.nameHash;
entity.HasWalkingParam = true;
}
if (!entity.HasCrouchParam && param.type == AnimatorControllerParameterType.Bool &&
lower.Contains("crouch"))
{
entity.CrouchParamHash = param.nameHash;
entity.HasCrouchParam = true;
}
if (!entity.HasSittingParam && param.type == AnimatorControllerParameterType.Bool &&
(lower == "issitting" || lower.Contains("sitting")))
{
entity.SittingParamHash = param.nameHash;
entity.HasSittingParam = true;
}
if (!entity.HasCarryingParam && param.type == AnimatorControllerParameterType.Bool &&
(lower.Contains("carry") || lower.Contains("carrying")))
{
entity.CarryingParamHash = param.nameHash;
entity.HasCarryingParam = true;
}
}
}
catch (Exception ex)
{
CrashLog.LogException($"[EntityManager] Animator param discovery error: {ex.Message}", ex);
}
}
entity.AnimParamsDiscovered = true;
}
}
}
}
public static void DestroyAll()
{
foreach (var kvp in _entities)
{
if (kvp.Value.CarryProxyGO != null) UnityEngine.Object.Destroy(kvp.Value.CarryProxyGO);
if (kvp.Value.NameTagGO != null) UnityEngine.Object.Destroy(kvp.Value.NameTagGO);
if (kvp.Value.GO != null) UnityEngine.Object.Destroy(kvp.Value.GO);
}
_entities.Clear();
foreach (var kvp in _carryPrefabCache)
if (kvp.Value != null) UnityEngine.Object.Destroy(kvp.Value);
_carryPrefabCache.Clear();
_nextId = 1;
}
private static void AddNameTag(GameObject parent, string name, ManagedEntity entity)
{
try
{
var parentScale = parent.transform.lossyScale;
float scale = 0.01f;
float fontSize = 5f;
float rectW = 70f;
float rectH = 10f;
if (parentScale.x > 0.001f)
{
float compensate = 1f / parentScale.x;
scale *= compensate;
}
var canvasGO = new GameObject($"NameTag_Entity_{entity.Id}");
canvasGO.transform.position = parent.transform.position + new Vector3(0, 1.75f, 0);
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
var canvasRect = canvasGO.GetComponent<RectTransform>();
if (canvasRect != null)
canvasRect.sizeDelta = new Vector2(rectW, rectH);
canvasGO.transform.localScale = new Vector3(scale, scale, scale);
var bgGO = new GameObject("Background");
bgGO.transform.SetParent(canvasGO.transform, false);
var bgImage = bgGO.AddComponent<UnityEngine.UI.Image>();
bgImage.color = new Color(0f, 0f, 0f, 0.45f);
var bgRect = bgGO.GetComponent<RectTransform>();
bgRect.anchorMin = new Vector2(0f, 0f);
bgRect.anchorMax = new Vector2(1f, 1f);
bgRect.offsetMin = Vector2.zero;
bgRect.offsetMax = Vector2.zero;
var textGO = new GameObject("Text");
textGO.transform.SetParent(canvasGO.transform, false);
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = name;
tmp.fontSize = fontSize;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
tmp.enableWordWrapping = false;
tmp.overflowMode = TextOverflowModes.Overflow;
tmp.outlineWidth = 0.2f;
tmp.outlineColor = new Color32(0, 0, 0, 200);
var rect = textGO.GetComponent<RectTransform>();
rect.anchorMin = new Vector2(0f, 0f);
rect.anchorMax = new Vector2(1f, 1f);
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
var bb = canvasGO.AddComponent<BillboardNameTag>();
bb.followTarget = parent.transform;
bb.offsetY = 1.85f;
entity.NameTagGO = canvasGO;
}
catch (Exception ex)
{
CrashLog.LogException("EntityManager.AddNameTag", ex);
}
}
}
@@ -0,0 +1,499 @@
using System;
using System.Runtime.InteropServices;
using MelonLoader;
namespace DataCenterModLoader;
// must match dc_api/src/events.rs
public static class EventIds
{
public const uint MoneyChanged = 100;
public const uint XPChanged = 101;
public const uint ReputationChanged = 102;
public const uint ServerPowered = 200;
public const uint ServerBroken = 201;
public const uint ServerRepaired = 202;
public const uint ServerInstalled = 203;
public const uint CableConnected = 204;
public const uint CableDisconnected = 205;
public const uint ServerCustomerChanged = 206;
public const uint ServerAppChanged = 207;
public const uint RackUnmounted = 208;
public const uint SwitchBroken = 209;
public const uint SwitchRepaired = 210;
public const uint ObjectSpawned = 211;
public const uint ObjectPickedUp = 212;
public const uint ObjectDropped = 213;
public const uint DayEnded = 300;
public const uint MonthEnded = 301;
public const uint CustomerAccepted = 400;
public const uint CustomerSatisfied = 401;
public const uint CustomerUnsatisfied = 402;
public const uint ShopCheckout = 500;
public const uint ShopItemAdded = 501;
public const uint ShopCartCleared = 502;
public const uint ShopItemRemoved = 503;
public const uint EmployeeHired = 600;
public const uint EmployeeFired = 601;
public const uint GameSaved = 700;
public const uint GameLoaded = 701;
public const uint GameAutoSaved = 702;
public const uint WallPurchased = 800;
public const uint NetWatchDispatched = 900; // 9xx = mod systems
// mod systems (10xx)
public const uint CustomEmployeeHired = 1000;
public const uint CustomEmployeeFired = 1001;
}
// must match rust repr(C) layouts
[StructLayout(LayoutKind.Sequential)]
public struct ValueChangedData
{
public double OldValue;
public double NewValue;
public double Delta;
}
[StructLayout(LayoutKind.Sequential)]
public struct ServerPoweredData
{
public uint PoweredOn; // 1 = on, 0 = off
}
[StructLayout(LayoutKind.Sequential)]
public struct DayEndedData
{
public uint Day;
}
[StructLayout(LayoutKind.Sequential)]
public struct CustomerAcceptedData
{
public int CustomerId;
}
[StructLayout(LayoutKind.Sequential)]
public struct CustomerSatisfiedData
{
public int CustomerBaseId;
}
[StructLayout(LayoutKind.Sequential)]
public struct ServerCustomerChangedData
{
public int NewCustomerId;
}
[StructLayout(LayoutKind.Sequential)]
public struct ServerAppChangedData
{
public int NewAppId;
}
[StructLayout(LayoutKind.Sequential)]
public struct MonthEndedData
{
public int Month;
}
[StructLayout(LayoutKind.Sequential)]
public struct ShopItemAddedData
{
public int ItemId;
public int Price;
public int ItemType;
}
[StructLayout(LayoutKind.Sequential)]
public struct ShopItemRemovedData
{
public int Uid;
}
[StructLayout(LayoutKind.Sequential)]
public struct NetWatchDispatchedData
{
public int DeviceType; // 0 = server, 1 = switch
public int Reason; // 0 = broken, 1 = eol_warning
}
[StructLayout(LayoutKind.Sequential)]
public struct CustomEmployeeEventData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] EmployeeId;
public static CustomEmployeeEventData Create(string employeeId)
{
var data = new CustomEmployeeEventData { EmployeeId = new byte[64] };
if (!string.IsNullOrEmpty(employeeId))
{
var bytes = System.Text.Encoding.ASCII.GetBytes(employeeId);
Array.Copy(bytes, data.EmployeeId, Math.Min(bytes.Length, 63));
}
return data;
}
}
/// Payload for ServerInstalled events. Must match Rust's ServerInstalledData layout.
[StructLayout(LayoutKind.Sequential)]
public struct ServerInstalledData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] ServerId;
public byte ObjectType;
public int RackPositionUid;
public static ServerInstalledData Create(string serverId, byte objectType, int rackPositionUid)
{
var data = new ServerInstalledData
{
ServerId = new byte[64],
ObjectType = objectType,
RackPositionUid = rackPositionUid
};
if (!string.IsNullOrEmpty(serverId))
{
var bytes = System.Text.Encoding.ASCII.GetBytes(serverId);
Array.Copy(bytes, data.ServerId, Math.Min(bytes.Length, 63));
}
return data;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct ObjectSpawnedData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] ObjectId;
public byte ObjectType;
public int PrefabId;
public float PosX;
public float PosY;
public float PosZ;
public float RotX;
public float RotY;
public float RotZ;
public float RotW;
public static ObjectSpawnedData Create(string objectId, byte objectType, int prefabId,
float px, float py, float pz, float rx, float ry, float rz, float rw)
{
var data = new ObjectSpawnedData
{
ObjectId = new byte[64],
ObjectType = objectType,
PrefabId = prefabId,
PosX = px,
PosY = py,
PosZ = pz,
RotX = rx,
RotY = ry,
RotZ = rz,
RotW = rw
};
if (!string.IsNullOrEmpty(objectId))
{
var bytes = System.Text.Encoding.ASCII.GetBytes(objectId);
Array.Copy(bytes, data.ObjectId, Math.Min(bytes.Length, 63));
}
return data;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct ObjectPickedUpData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] ObjectId;
public byte ObjectType;
public static ObjectPickedUpData Create(string objectId, byte objectType)
{
var data = new ObjectPickedUpData
{
ObjectId = new byte[64],
ObjectType = objectType
};
if (!string.IsNullOrEmpty(objectId))
{
var bytes = System.Text.Encoding.ASCII.GetBytes(objectId);
Array.Copy(bytes, data.ObjectId, Math.Min(bytes.Length, 63));
}
return data;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct ObjectDroppedData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] ObjectId;
public byte ObjectType;
public float PosX;
public float PosY;
public float PosZ;
public float RotX;
public float RotY;
public float RotZ;
public float RotW;
public static ObjectDroppedData Create(string objectId, byte objectType,
float px, float py, float pz, float rx, float ry, float rz, float rw)
{
var data = new ObjectDroppedData
{
ObjectId = new byte[64],
ObjectType = objectType,
PosX = px,
PosY = py,
PosZ = pz,
RotX = rx,
RotY = ry,
RotZ = rz,
RotW = rw
};
if (!string.IsNullOrEmpty(objectId))
{
var bytes = System.Text.Encoding.ASCII.GetBytes(objectId);
Array.Copy(bytes, data.ObjectId, Math.Min(bytes.Length, 63));
}
return data;
}
}
public static class EventDispatcher
{
private static FFIBridge _bridge;
private static MelonLogger.Instance _logger;
// dedup: harmony + il2cpp can double-fire patches
private static uint _lastEventId;
private static long _lastEventTick;
private static double _lastEventPayloadHash;
public static void Initialize(FFIBridge bridge, MelonLogger.Instance logger)
{
_bridge = bridge;
_logger = logger;
}
private static bool IsDuplicate(uint eventId, double payloadHash = 0.0)
{
long now = System.Diagnostics.Stopwatch.GetTimestamp();
long elapsed = now - _lastEventTick;
long threshold = System.Diagnostics.Stopwatch.Frequency / 20; // ~50ms window
bool isDup = (eventId == _lastEventId)
&& (elapsed < threshold)
&& (Math.Abs(payloadHash - _lastEventPayloadHash) < 0.0001);
_lastEventId = eventId;
_lastEventTick = now;
_lastEventPayloadHash = payloadHash;
return isDup;
}
private static void DispatchWithData<T>(uint eventId, T data, double payloadHash = 0.0) where T : struct
{
if (_bridge == null) return;
if (IsDuplicate(eventId, payloadHash)) return;
int size = Marshal.SizeOf<T>();
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(data, ptr, false);
_bridge.DispatchEvent(eventId, ptr, (uint)size);
}
catch (Exception ex)
{
_logger?.Error($"Failed to dispatch event {eventId}: {ex.Message}");
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
public static void FireSimple(uint eventId)
{
if (_bridge == null) return;
if (IsDuplicate(eventId)) return;
try
{
_bridge.DispatchEvent(eventId, IntPtr.Zero, 0);
}
catch (Exception ex) { _logger?.Error($"Failed to dispatch event {eventId}: {ex.Message}"); }
}
public static void LogError(string message)
{
_logger?.Error("[Events] " + message);
}
public static void FireValueChanged(uint eventId, double oldValue, double newValue, double delta)
{
DispatchWithData(eventId, new ValueChangedData
{
OldValue = oldValue,
NewValue = newValue,
Delta = delta,
}, oldValue + newValue * 31.0);
}
public static void FireServerPowered(bool poweredOn)
{
DispatchWithData(EventIds.ServerPowered, new ServerPoweredData
{
PoweredOn = poweredOn ? 1u : 0u,
}, poweredOn ? 1.0 : 0.0);
}
public static void FireDayEnded(uint day)
{
DispatchWithData(EventIds.DayEnded, new DayEndedData { Day = day }, day);
}
public static void FireCustomerAccepted(int customerId)
{
DispatchWithData(EventIds.CustomerAccepted, new CustomerAcceptedData { CustomerId = customerId }, customerId);
}
public static void FireCustomerSatisfied(int customerBaseId)
{
DispatchWithData(EventIds.CustomerSatisfied, new CustomerSatisfiedData { CustomerBaseId = customerBaseId }, customerBaseId);
}
public static void FireCustomerUnsatisfied(int customerBaseId)
{
DispatchWithData(EventIds.CustomerUnsatisfied, new CustomerSatisfiedData { CustomerBaseId = customerBaseId }, customerBaseId + 0.5);
}
public static void FireCableConnected()
{
FireSimple(EventIds.CableConnected);
}
public static void FireCableDisconnected()
{
FireSimple(EventIds.CableDisconnected);
}
public static void FireServerCustomerChanged(int newCustomerId)
{
DispatchWithData(EventIds.ServerCustomerChanged, new ServerCustomerChangedData { NewCustomerId = newCustomerId }, newCustomerId);
}
public static void FireServerAppChanged(int newAppId)
{
DispatchWithData(EventIds.ServerAppChanged, new ServerAppChangedData { NewAppId = newAppId }, newAppId);
}
public static void FireServerInstalled(string serverId, byte objectType, int rackPositionUid)
{
DispatchWithData(EventIds.ServerInstalled,
ServerInstalledData.Create(serverId, objectType, rackPositionUid),
serverId?.GetHashCode() ?? 0 + rackPositionUid * 31.0);
}
public static void FireObjectSpawned(string objectId, byte objectType, int prefabId,
UnityEngine.Vector3 pos, UnityEngine.Quaternion rot)
{
DispatchWithData(EventIds.ObjectSpawned,
ObjectSpawnedData.Create(objectId, objectType, prefabId,
pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, rot.w),
(objectId?.GetHashCode() ?? 0) + prefabId * 31.0);
}
public static void FireObjectPickedUp(string objectId, byte objectType)
{
DispatchWithData(EventIds.ObjectPickedUp,
ObjectPickedUpData.Create(objectId, objectType),
(objectId?.GetHashCode() ?? 0) + objectType * 31.0);
}
public static void FireObjectDropped(string objectId, byte objectType,
UnityEngine.Vector3 pos, UnityEngine.Quaternion rot)
{
DispatchWithData(EventIds.ObjectDropped,
ObjectDroppedData.Create(objectId, objectType,
pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, rot.w),
(objectId?.GetHashCode() ?? 0) + objectType * 31.0 + pos.x);
}
public static void FireRackUnmounted()
{
FireSimple(EventIds.RackUnmounted);
}
public static void FireSwitchBroken()
{
FireSimple(EventIds.SwitchBroken);
}
public static void FireSwitchRepaired()
{
FireSimple(EventIds.SwitchRepaired);
}
public static void FireMonthEnded(int month)
{
DispatchWithData(EventIds.MonthEnded, new MonthEndedData { Month = month }, month);
}
public static void FireShopItemAdded(int itemId, int price, int itemType)
{
DispatchWithData(EventIds.ShopItemAdded, new ShopItemAddedData { ItemId = itemId, Price = price, ItemType = itemType }, itemId * 1000.0 + price + itemType * 0.1);
}
public static void FireShopItemRemoved(int uid)
{
DispatchWithData(EventIds.ShopItemRemoved, new ShopItemRemovedData { Uid = uid }, uid);
}
public static void FireShopCartCleared()
{
FireSimple(EventIds.ShopCartCleared);
}
public static void FireGameAutoSaved()
{
FireSimple(EventIds.GameAutoSaved);
}
public static void FireWallPurchased()
{
FireSimple(EventIds.WallPurchased);
}
public static void FireNetWatchDispatched(int deviceType, int reason)
{
DispatchWithData(EventIds.NetWatchDispatched, new NetWatchDispatchedData
{
DeviceType = deviceType,
Reason = reason
}, deviceType * 10.0 + reason);
}
public static void FireCustomEmployeeHired(string employeeId)
{
DispatchWithData(EventIds.CustomEmployeeHired, CustomEmployeeEventData.Create(employeeId), employeeId.GetHashCode());
}
public static void FireCustomEmployeeFired(string employeeId)
{
DispatchWithData(EventIds.CustomEmployeeFired, CustomEmployeeEventData.Create(employeeId), employeeId.GetHashCode() + 0.5);
}
}
@@ -0,0 +1,327 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MelonLoader;
namespace DataCenterModLoader;
public class FFIBridge : IDisposable
{
private readonly MelonLogger.Instance _logger;
private readonly string _modsPath;
private readonly GameAPIManager _apiManager;
private readonly List<RustMod> _loadedMods = new();
private bool _rustAvailable = false;
private string _rustStatusMessage = "";
public bool IsRustAvailable => _rustAvailable;
public string RustStatusMessage => _rustStatusMessage;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ModInfoFFI ModInfoDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.U1)]
private delegate bool ModInitDelegate(IntPtr apiTable);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ModUpdateDelegate(float deltaTime);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ModOnSceneLoadedDelegate(IntPtr sceneName);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ModShutdownDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ModOnEventDelegate(uint eventId, IntPtr eventData, uint dataSize);
[StructLayout(LayoutKind.Sequential)]
private struct ModInfoFFI
{
public IntPtr Id;
public IntPtr Name;
public IntPtr Version;
public IntPtr Author;
public IntPtr Description;
}
private class RustMod
{
public string FilePath = "";
public string Id = "unknown";
public string Name = "Unknown";
public string Version = "0.0.0";
public string Author = "Unknown";
public IntPtr Handle;
public ModUpdateDelegate Update;
public ModUpdateDelegate FixedUpdate;
public ModOnSceneLoadedDelegate OnSceneLoaded;
public ModShutdownDelegate Shutdown;
public ModOnEventDelegate OnEvent;
}
public FFIBridge(MelonLogger.Instance logger, string modsPath)
{
_logger = logger;
_modsPath = modsPath;
_apiManager = new GameAPIManager(logger);
}
public void LoadAllMods()
{
if (!Directory.Exists(_modsPath))
{
_logger.Warning("Mods/native/ directory not found. Creating...");
try { Directory.CreateDirectory(_modsPath); }
catch (Exception ex) {
_rustStatusMessage = $"Failed to create Mods/native/: {ex.Message}";
_logger.Error(_rustStatusMessage);
return;
}
}
var dllFiles = Directory.GetFiles(_modsPath, "*.dll", SearchOption.AllDirectories);
if (dllFiles.Length == 0)
{
_rustStatusMessage = "No Rust mod DLLs found. Running in C#-only compatibility mode.";
_logger.Warning(_rustStatusMessage);
_logger.Warning(" → Native Rust mods require Rust toolchain to build.");
_logger.Warning(" → C# mods (gregCore) are fully supported.");
_rustAvailable = false;
return;
}
int loadedCount = 0;
int failedCount = 0;
_logger.Msg($"Found {dllFiles.Length} potential Rust mod DLL(s). Attempting to load...");
foreach (var dllPath in dllFiles)
{
try {
LoadMod(dllPath);
loadedCount++;
}
catch (Exception ex) {
failedCount++;
_logger.Error($"Failed to load '{Path.GetFileName(dllPath)}': {ex.Message}");
}
}
if (loadedCount > 0)
{
_rustAvailable = true;
_rustStatusMessage = $"Rust Bridge active: {loadedCount} mod(s) loaded.";
_logger.Msg($"✓ {_loadedMods.Count} Rust mod(s) loaded successfully.");
}
else
{
_rustStatusMessage = $"Failed to load all {dllFiles.Length} Rust DLLs. Running in compatibility fallback mode.";
_logger.Warning(_rustStatusMessage);
_logger.Warning(" → This usually means:");
_logger.Warning(" - DLL was built for wrong architecture (x64 vs x86)");
_logger.Warning(" - Missing Visual C++ runtime");
_logger.Warning(" - Rust DLL exports are incompatible");
_logger.Warning(" → gregCore C# mods work independently.");
_rustAvailable = false;
}
}
private void LoadMod(string dllPath)
{
var fileName = Path.GetFileName(dllPath);
_logger.Msg($"Loading Rust mod: {fileName}");
CrashLog.Log($"LoadMod: about to call LoadLibrary for '{fileName}'");
var handle = LoadLibrary(dllPath);
if (handle == IntPtr.Zero)
{
var error = Marshal.GetLastWin32Error();
throw new Exception($"LoadLibrary failed with error code {error}");
}
CrashLog.Log($"LoadMod: LoadLibrary succeeded for '{fileName}', handle=0x{handle.ToInt64():X}");
var mod = new RustMod { FilePath = dllPath, Handle = handle };
// mod_info
var modInfoPtr = GetProcAddress(handle, "mod_info");
if (modInfoPtr != IntPtr.Zero)
{
var modInfoFn = Marshal.GetDelegateForFunctionPointer<ModInfoDelegate>(modInfoPtr);
CrashLog.Log($"LoadMod: about to call modInfoFn() for '{fileName}'");
var info = modInfoFn();
CrashLog.Log($"LoadMod: modInfoFn() returned for '{fileName}'");
mod.Id = Marshal.PtrToStringAnsi(info.Id) ?? "unknown";
mod.Name = Marshal.PtrToStringAnsi(info.Name) ?? "Unknown";
mod.Version = Marshal.PtrToStringAnsi(info.Version) ?? "0.0.0";
mod.Author = Marshal.PtrToStringAnsi(info.Author) ?? "Unknown";
var description = Marshal.PtrToStringAnsi(info.Description) ?? "";
_logger.Msg($" Mod: {mod.Name} v{mod.Version} by {mod.Author}");
_logger.Msg($" Description: {description}");
}
else
{
_logger.Warning($" '{fileName}' has no mod_info() export.");
}
// mod_init
var modInitPtr = GetProcAddress(handle, "mod_init");
if (modInitPtr != IntPtr.Zero)
{
var modInitFn = Marshal.GetDelegateForFunctionPointer<ModInitDelegate>(modInitPtr);
CrashLog.Log($"LoadMod: about to call modInitFn() for '{mod.Name}'");
if (!modInitFn(_apiManager.GetTablePointer()))
{
_logger.Error($" Mod '{mod.Name}' mod_init() returned false.");
FreeLibrary(handle);
return;
}
CrashLog.Log($"LoadMod: modInitFn() succeeded for '{mod.Name}'");
_logger.Msg($" Mod '{mod.Name}' initialized.");
}
else
{
_logger.Warning($" '{fileName}' has no mod_init() export.");
}
if (!string.IsNullOrEmpty(mod.Id) && mod.Id != "unknown")
{
ModConfigSystem.SetModInfo(mod.Id, mod.Author, mod.Version);
}
// Optional exports
CrashLog.Log($"LoadMod: resolving optional export 'mod_update' for '{mod.Name}'");
var updatePtr = GetProcAddress(handle, "mod_update");
if (updatePtr != IntPtr.Zero)
mod.Update = Marshal.GetDelegateForFunctionPointer<ModUpdateDelegate>(updatePtr);
CrashLog.Log($"LoadMod: resolving optional export 'mod_fixed_update' for '{mod.Name}'");
var fixedUpdatePtr = GetProcAddress(handle, "mod_fixed_update");
if (fixedUpdatePtr != IntPtr.Zero)
mod.FixedUpdate = Marshal.GetDelegateForFunctionPointer<ModUpdateDelegate>(fixedUpdatePtr);
CrashLog.Log($"LoadMod: resolving optional export 'mod_on_scene_loaded' for '{mod.Name}'");
var sceneLoadedPtr = GetProcAddress(handle, "mod_on_scene_loaded");
if (sceneLoadedPtr != IntPtr.Zero)
mod.OnSceneLoaded = Marshal.GetDelegateForFunctionPointer<ModOnSceneLoadedDelegate>(sceneLoadedPtr);
CrashLog.Log($"LoadMod: resolving optional export 'mod_shutdown' for '{mod.Name}'");
var shutdownPtr = GetProcAddress(handle, "mod_shutdown");
if (shutdownPtr != IntPtr.Zero)
mod.Shutdown = Marshal.GetDelegateForFunctionPointer<ModShutdownDelegate>(shutdownPtr);
CrashLog.Log($"LoadMod: resolving optional export 'mod_on_event' for '{mod.Name}'");
var onEventPtr = GetProcAddress(handle, "mod_on_event");
if (onEventPtr != IntPtr.Zero)
{
mod.OnEvent = Marshal.GetDelegateForFunctionPointer<ModOnEventDelegate>(onEventPtr);
_logger.Msg($" Mod '{mod.Name}' supports game events.");
}
CrashLog.Log($"LoadMod: finished loading '{mod.Name}' successfully");
_loadedMods.Add(mod);
}
public void OnUpdate(float deltaTime)
{
try
{
foreach (var mod in _loadedMods)
{
try { mod.Update?.Invoke(deltaTime); }
catch (Exception ex)
{
_logger.Error($"[{mod.Name}] mod_update crashed: {ex.Message}");
CrashLog.LogException($"[{mod.Name}] mod_update", ex);
}
}
}
catch (Exception ex)
{
CrashLog.LogException("FFIBridge.OnUpdate outer", ex);
}
}
public void OnFixedUpdate(float deltaTime)
{
try
{
foreach (var mod in _loadedMods)
{
try { mod.FixedUpdate?.Invoke(deltaTime); }
catch (Exception ex)
{
_logger.Error($"[{mod.Name}] mod_fixed_update crashed: {ex.Message}");
CrashLog.LogException($"[{mod.Name}] mod_fixed_update", ex);
}
}
}
catch (Exception ex)
{
CrashLog.LogException("FFIBridge.OnFixedUpdate outer", ex);
}
}
public void OnSceneLoaded(string sceneName)
{
var ptr = Marshal.StringToHGlobalAnsi(sceneName);
try
{
foreach (var mod in _loadedMods)
{
try { mod.OnSceneLoaded?.Invoke(ptr); }
catch (Exception ex) { _logger.Error($"[{mod.Name}] mod_on_scene_loaded crashed: {ex.Message}"); }
}
}
finally { Marshal.FreeHGlobal(ptr); }
}
public void DispatchEvent(uint eventId, IntPtr eventData, uint dataSize)
{
foreach (var mod in _loadedMods)
{
if (mod.OnEvent == null) continue;
try { mod.OnEvent.Invoke(eventId, eventData, dataSize); }
catch (Exception ex) { _logger.Error($"[{mod.Name}] mod_on_event(id={eventId}) crashed: {ex.Message}"); }
}
}
public void Shutdown()
{
foreach (var mod in _loadedMods)
{
try
{
_logger.Msg($"Shutting down: {mod.Name}");
mod.Shutdown?.Invoke();
}
catch (Exception ex) { _logger.Error($"[{mod.Name}] mod_shutdown crashed: {ex.Message}"); }
}
}
public void Dispose()
{
foreach (var mod in _loadedMods)
{
if (mod.Handle != IntPtr.Zero)
FreeLibrary(mod.Handle);
}
_loadedMods.Clear();
_apiManager.Dispose();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,796 @@
using System;
using System.Collections.Generic;
using Il2Cpp;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using UnityEngine;
namespace DataCenterModLoader;
// safe game state accessors, returns defaults when singletons are null
public static class GameHooks
{
public static int EnsureAllRackPositionUIDs()
{
try
{
var mgr = MainGameManager.instance;
if (mgr == null)
{
CrashLog.Log("[WorldSync] EnsureAllRackPositionUIDs: MainGameManager is null");
return 0;
}
var allPositions = UnityEngine.Object.FindObjectsOfType<RackPosition>();
if (allPositions == null || allPositions.Count == 0)
{
CrashLog.Log("[WorldSync] EnsureAllRackPositionUIDs: no RackPositions found");
return 0;
}
var sorted = new List<RackPosition>();
foreach (var rp in allPositions)
{
if (rp != null) sorted.Add(rp);
}
sorted.Sort((a, b) =>
{
var pa = a.transform.position;
var pb = b.transform.position;
int cmp = pa.x.CompareTo(pb.x);
if (cmp != 0) return cmp;
cmp = pa.z.CompareTo(pb.z);
if (cmp != 0) return cmp;
cmp = pa.y.CompareTo(pb.y);
if (cmp != 0) return cmp;
cmp = a.positionIndex.CompareTo(b.positionIndex);
if (cmp != 0) return cmp;
string nameA = "", nameB = "";
try { nameA = a.rack?.gameObject?.name ?? ""; } catch { }
try { nameB = b.rack?.gameObject?.name ?? ""; } catch { }
return string.Compare(nameA, nameB, StringComparison.Ordinal);
});
const int SYNC_UID_BASE = 10000;
mgr.lastUsedRackPositionGlobalUID = SYNC_UID_BASE;
int assigned = 0;
foreach (var rp in sorted)
{
try
{
mgr.lastUsedRackPositionGlobalUID++;
rp.rackPosGlobalUID = mgr.lastUsedRackPositionGlobalUID;
assigned++;
}
catch { /* field access can fail during teardown */ }
}
try
{
var servers = UnityEngine.Object.FindObjectsOfType<Il2Cpp.Server>();
int updated = 0;
foreach (var srv in servers)
{
try
{
if (srv.currentRackPosition != null)
{
int oldUid = srv.rackPositionUID;
int newUid = srv.currentRackPosition.rackPosGlobalUID;
if (oldUid != newUid)
{
srv.rackPositionUID = newUid;
updated++;
}
}
}
catch { }
}
if (updated > 0)
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: updated {updated} server rackPositionUID references");
}
catch (Exception ex)
{
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: server ref update failed: {ex.Message}");
}
try
{
var switches = UnityEngine.Object.FindObjectsOfType<Il2Cpp.NetworkSwitch>();
int swUpdated = 0;
foreach (var sw in switches)
{
try
{
if (sw.currentRackPosition != null)
{
int oldUid = sw.rackPositionUID;
int newUid = sw.currentRackPosition.rackPosGlobalUID;
if (oldUid != newUid)
{
sw.rackPositionUID = newUid;
swUpdated++;
}
}
}
catch { }
}
if (swUpdated > 0)
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: updated {swUpdated} switch rackPositionUID references");
}
catch (Exception ex)
{
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: switch ref update failed: {ex.Message}");
}
try
{
var panels = UnityEngine.Object.FindObjectsOfType<Il2Cpp.PatchPanel>();
int ppUpdated = 0;
foreach (var pp in panels)
{
try
{
if (pp.currentRackPosition != null)
{
int oldUid = pp.rackPositionUID;
int newUid = pp.currentRackPosition.rackPosGlobalUID;
if (oldUid != newUid)
{
pp.rackPositionUID = newUid;
ppUpdated++;
}
}
}
catch { }
}
if (ppUpdated > 0)
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: updated {ppUpdated} patchpanel rackPositionUID references");
}
catch (Exception ex)
{
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: patchpanel ref update failed: {ex.Message}");
}
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs: assigned {assigned}/{sorted.Count} positions (counter now {mgr.lastUsedRackPositionGlobalUID})");
return assigned;
}
catch (Exception ex)
{
CrashLog.Log($"[WorldSync] EnsureAllRackPositionUIDs failed: {ex.Message}");
return 0;
}
}
public static float GetPlayerMoney()
{
try { return PlayerManager.instance?.playerClass?.money ?? 0f; }
catch { return 0f; }
}
public static void SetPlayerMoney(float value)
{
try
{
var player = PlayerManager.instance?.playerClass;
if (player != null) player.money = value;
}
catch { }
}
public static float GetPlayerXP()
{
try { return PlayerManager.instance?.playerClass?.xp ?? 0f; }
catch { return 0f; }
}
public static void SetPlayerXP(float value)
{
try
{
var player = PlayerManager.instance?.playerClass;
if (player != null) player.xp = value;
}
catch { }
}
public static float GetPlayerReputation()
{
try { return PlayerManager.instance?.playerClass?.reputation ?? 0f; }
catch { return 0f; }
}
public static void SetPlayerReputation(float value)
{
try
{
var player = PlayerManager.instance?.playerClass;
if (player != null) player.reputation = value;
}
catch { }
}
public static float GetTimeOfDay()
{
try { return TimeController.instance?.currentTimeOfDay ?? 0f; }
catch { return 0f; }
}
public static int GetDay()
{
try { return TimeController.instance?.day ?? 0; }
catch { return 0; }
}
public static float GetSecondsInFullDay()
{
try { return TimeController.instance?.secondsInFullDay ?? 0f; }
catch { return 0f; }
}
public static void SetSecondsInFullDay(float value)
{
try
{
var tc = TimeController.instance;
if (tc != null) tc.secondsInFullDay = value;
}
catch { }
}
public static int[] GetDeviceCounts()
{
try
{
var nm = NetworkMap.instance;
if (nm == null) return Array.Empty<int>();
Il2CppStructArray<int> arr = nm.GetNumberOfDevices();
if (arr == null) return Array.Empty<int>();
int[] result = new int[arr.Length];
for (int i = 0; i < arr.Length; i++) result[i] = arr[i];
return result;
}
catch { return Array.Empty<int>(); }
}
public static uint GetServerCount()
{
var counts = GetDeviceCounts();
return counts.Length > 0 ? (uint)Math.Max(0, counts[0]) : 0;
}
public static uint GetSwitchCount()
{
var counts = GetDeviceCounts();
return counts.Length > 1 ? (uint)Math.Max(0, counts[1]) : 0;
}
public static uint GetRackCount()
{
try
{
var racks = UnityEngine.Object.FindObjectsOfType<Rack>();
return racks != null ? (uint)racks.Length : 0;
}
catch { return 0; }
}
public static int GetSatisfiedCustomerCount()
{
try { return CustomerBase.satisfiedCustomerCount; }
catch { return 0; }
}
// Technician & Device management
public static uint GetBrokenServerCount()
{
try
{
var nm = NetworkMap.instance;
if (nm == null) return 0;
var dict = nm.brokenServers;
if (dict == null) return 0;
return (uint)Math.Max(0, dict.Count);
}
catch { return 0; }
}
public static uint GetBrokenSwitchCount()
{
try
{
var nm = NetworkMap.instance;
if (nm == null) return 0;
var dict = nm.brokenSwitches;
if (dict == null) return 0;
return (uint)Math.Max(0, dict.Count);
}
catch { return 0; }
}
public static uint GetEolServerCount()
{
try
{
var nm = NetworkMap.instance;
if (nm == null) return 0;
var dict = nm.servers;
if (dict == null) return 0;
uint count = 0;
// copy keys first to avoid Il2Cpp iteration issues
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
foreach (var key in keys)
{
try
{
var server = dict[key];
if (server == null) continue;
if (server.isBroken) continue;
// eolTime counts down; <= 0 means at/past EOL
if (server.eolTime <= 0) count++;
}
catch { }
}
return count;
}
catch { return 0; }
}
private static int _eolSwitchDiagCounter = 0;
public static uint GetEolSwitchCount()
{
try
{
var nm = NetworkMap.instance;
if (nm == null) return 0;
var dict = nm.switches;
if (dict == null) return 0;
uint count = 0;
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
foreach (var key in keys)
{
try
{
var sw = dict[key];
if (sw == null) continue;
if (sw.isBroken) continue;
// Check both warning signs AND eolTime countdown (like servers)
bool isEol = sw.existingWarningSigns > 0;
if (!isEol)
{
try { isEol = sw.eolTime <= 0; } catch { }
}
if (isEol) count++;
}
catch { }
}
// Periodic diagnostic dump when EOL switches exist (every ~30s = 6 scans)
if (count > 0)
{
_eolSwitchDiagCounter++;
if (_eolSwitchDiagCounter >= 6)
{
_eolSwitchDiagCounter = 0;
DumpSwitchDiagnostics();
}
}
else
{
_eolSwitchDiagCounter = 0;
}
return count;
}
catch { return 0; }
}
public static uint GetFreeTechnicianCount()
{
try
{
var tm = TechnicianManager.instance;
if (tm == null) return 0;
var techs = tm.technicians;
if (techs == null) return 0;
int total = techs.Count;
if (total == 0) return 0;
// Primary: use GetActiveJobs() — counts all busy techs across all 6 slots
try
{
var activeJobs = tm.GetActiveJobs();
int activeCount = activeJobs != null ? activeJobs.Count : 0;
return (uint)Math.Max(0, total - activeCount);
}
catch { }
// Fallback: iterate isBusy per-technician (pre-update behaviour)
uint count = 0;
for (int i = 0; i < total; i++)
{
try
{
var tech = techs[i];
if (tech != null && !tech.isBusy) count++;
}
catch { }
}
return count;
}
catch { return 0; }
}
public static uint GetTotalTechnicianCount()
{
try
{
var tm = TechnicianManager.instance;
if (tm == null) return 0;
var techs = tm.technicians;
if (techs == null) return 0;
// Return the exact count of Technician objects — all 6 when all are hired/active.
// CommandCenterOperator entries live in tm.commandCenterOperator (separate list)
// and cannot do physical repairs, so they are intentionally excluded here.
return (uint)Math.Max(0, techs.Count);
}
catch { return 0; }
}
public static uint GetQueuedJobCount()
{
try
{
var tm = TechnicianManager.instance;
if (tm == null) return 0;
return (uint)Math.Max(0, tm.QueuedJobCount);
}
catch { return 0; }
}
/// <summary>
/// The new game update added <c>CommandCenterOperator</c> NPCs that must be hired before
/// <c>ProcessDispatchQueue</c> will move jobs from <c>pendingDispatches</c> to actual
/// technicians. If no operator is hired the queue grows forever while techs stand idle.
///
/// This method bypasses that requirement: it drains <c>pendingDispatches</c> directly and
/// calls <c>Technician.AssignJob</c> on every free technician we can find. It is called
/// immediately after every <c>SendTechnician</c> so the SysAdmin mod keeps working even
/// without a hired Command-Center Operator.
/// </summary>
public static void ForceProcessPendingQueue(TechnicianManager tm)
{
try
{
var pending = tm.pendingDispatches;
if (pending == null || pending.Count == 0) return;
var techs = tm.technicians;
if (techs == null || techs.Count == 0) return;
// Build active-technician set via GetActiveJobs() for accuracy
var activeTechIds = new System.Collections.Generic.HashSet<int>();
try
{
var activeJobs = tm.GetActiveJobs();
if (activeJobs != null)
{
foreach (var aj in activeJobs)
{
try
{
if (aj.assignedTechnician != null)
activeTechIds.Add(aj.assignedTechnician.technicianID);
}
catch { }
}
}
}
catch { }
int assigned = 0;
for (int i = 0; i < techs.Count && pending.Count > 0; i++)
{
try
{
var tech = techs[i];
if (tech == null) continue;
// Skip techs that are already working
bool busy = activeTechIds.Contains(tech.technicianID);
if (!busy)
{
try { busy = tech.isBusy; } catch { }
}
if (busy) continue;
// Dequeue next pending job and assign directly
var job = pending.Dequeue();
tech.AssignJob(job);
assigned++;
try
{
CrashLog.Log($"ForceProcessPendingQueue: assigned '{job.DeviceName}' → tech #{tech.technicianID} ({tech.technicianName})");
}
catch { }
}
catch { }
}
if (assigned > 0)
CrashLog.Log($"ForceProcessPendingQueue: force-assigned {assigned} job(s) (bypassed CommandCenterOperator check)");
}
catch (Exception ex)
{
CrashLog.LogException("ForceProcessPendingQueue", ex);
}
}
// Returns: 1 = dispatched, 0 = no target, -1 = no free technician
public static int DispatchRepairServer()
{
try
{
var nm = NetworkMap.instance;
var tm = TechnicianManager.instance;
if (nm == null || tm == null) return 0;
if (GetFreeTechnicianCount() == 0) return -1;
var dict = nm.brokenServers;
if (dict == null || dict.Count == 0) return 0;
// copy keys to avoid iteration issues
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
int skipped = 0;
foreach (var key in keys)
{
try
{
Server server;
try { server = dict[key]; } catch { continue; }
if (server == null) continue;
if (tm.IsDeviceAlreadyAssigned(null, server)) { skipped++; continue; }
tm.SendTechnician(null, server);
ForceProcessPendingQueue(tm);
return 1;
}
catch { }
}
if (skipped > 0)
CrashLog.Log($"DispatchRepairServer: no target — {skipped}/{keys.Count} device(s) already assigned in queue");
return 0;
}
catch { return 0; }
}
public static int DispatchRepairSwitch()
{
try
{
var nm = NetworkMap.instance;
var tm = TechnicianManager.instance;
if (nm == null || tm == null) return 0;
if (GetFreeTechnicianCount() == 0) return -1;
var dict = nm.brokenSwitches;
if (dict == null || dict.Count == 0) return 0;
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
int skipped = 0;
foreach (var key in keys)
{
try
{
NetworkSwitch sw;
try { sw = dict[key]; } catch { continue; }
if (sw == null) continue;
if (tm.IsDeviceAlreadyAssigned(sw, null)) { skipped++; continue; }
tm.SendTechnician(sw, null);
ForceProcessPendingQueue(tm);
return 1;
}
catch { }
}
if (skipped > 0)
CrashLog.Log($"DispatchRepairSwitch: no target — {skipped}/{keys.Count} device(s) already assigned in queue");
return 0;
}
catch { return 0; }
}
public static int DispatchReplaceServer()
{
try
{
var nm = NetworkMap.instance;
var tm = TechnicianManager.instance;
if (nm == null || tm == null) return 0;
if (GetFreeTechnicianCount() == 0) return -1;
var dict = nm.servers;
if (dict == null || dict.Count == 0) return 0;
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
int skipped = 0;
foreach (var key in keys)
{
try
{
Server server;
try { server = dict[key]; } catch { continue; }
if (server == null) continue;
if (server.isBroken) continue;
if (server.eolTime > 0) continue;
if (tm.IsDeviceAlreadyAssigned(null, server)) { skipped++; continue; }
tm.SendTechnician(null, server);
ForceProcessPendingQueue(tm);
return 1;
}
catch { }
}
if (skipped > 0)
CrashLog.Log($"DispatchReplaceServer: no target — {skipped}/{keys.Count} device(s) already assigned in queue");
return 0;
}
catch { return 0; }
}
public static int DispatchReplaceSwitch()
{
try
{
var nm = NetworkMap.instance;
var tm = TechnicianManager.instance;
if (nm == null || tm == null) return 0;
if (GetFreeTechnicianCount() == 0) return -1;
var dict = nm.switches;
if (dict == null || dict.Count == 0) return 0;
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
int skipped = 0;
foreach (var key in keys)
{
try
{
NetworkSwitch sw;
try { sw = dict[key]; } catch { continue; }
if (sw == null) continue;
if (sw.isBroken) continue;
// Check both warning signs AND eolTime countdown (like servers)
bool isEol = sw.existingWarningSigns > 0;
if (!isEol)
{
try { isEol = sw.eolTime <= 0; } catch { }
}
if (!isEol) continue; // not EOL
if (tm.IsDeviceAlreadyAssigned(sw, null)) { skipped++; continue; }
tm.SendTechnician(sw, null);
ForceProcessPendingQueue(tm);
return 1;
}
catch { }
}
if (skipped > 0)
CrashLog.Log($"DispatchReplaceSwitch: no target — {skipped}/{keys.Count} device(s) already assigned in queue");
return 0;
}
catch { return 0; }
}
/// <summary>
/// Logs detailed per-switch diagnostics to CrashLog so we can identify
/// which switch is missing from EOL detection.
/// </summary>
public static void DumpSwitchDiagnostics()
{
try
{
var nm = NetworkMap.instance;
var tm = TechnicianManager.instance;
if (nm == null) { MelonLoader.MelonLogger.Msg("[SwitchDiag] NetworkMap is null"); return; }
var dict = nm.switches;
if (dict == null) { MelonLoader.MelonLogger.Msg("[SwitchDiag] switches dict is null"); return; }
var keys = new System.Collections.Generic.List<string>();
foreach (var kvp in dict) keys.Add(kvp.Key);
MelonLoader.MelonLogger.Msg($"[SwitchDiag] --- {keys.Count} switch(es) in nm.switches ---");
foreach (var key in keys)
{
try
{
var sw = dict[key];
if (sw == null) { MelonLoader.MelonLogger.Msg($"[SwitchDiag] key={key} => null"); continue; }
bool broken = false;
try { broken = sw.isBroken; } catch { }
int warningSigns = -999;
try { warningSigns = sw.existingWarningSigns; } catch { }
float eolTime = float.NaN;
try { eolTime = sw.eolTime; } catch { }
bool assigned = false;
try { if (tm != null) assigned = tm.IsDeviceAlreadyAssigned(sw, null); } catch { }
MelonLoader.MelonLogger.Msg(
$"[SwitchDiag] key={key} broken={broken} warningSigns={warningSigns} eolTime={eolTime:F1} assigned={assigned}"
);
}
catch (Exception ex)
{
MelonLoader.MelonLogger.Msg($"[SwitchDiag] key={key} => exception: {ex.Message}");
}
}
// Also check brokenSwitches dict
var brokenDict = nm.brokenSwitches;
int brokenCount = 0;
if (brokenDict != null)
{
var brokenKeys = new System.Collections.Generic.List<string>();
foreach (var kvp in brokenDict) brokenKeys.Add(kvp.Key);
brokenCount = brokenKeys.Count;
foreach (var key in brokenKeys)
{
try
{
var sw = brokenDict[key];
float eolTime = float.NaN;
try { eolTime = sw.eolTime; } catch { }
int warningSigns = -999;
try { warningSigns = sw.existingWarningSigns; } catch { }
MelonLoader.MelonLogger.Msg(
$"[SwitchDiag] BROKEN key={key} warningSigns={warningSigns} eolTime={eolTime:F1}"
);
}
catch { }
}
}
MelonLoader.MelonLogger.Msg($"[SwitchDiag] --- total: {keys.Count} normal + {brokenCount} broken ---");
}
catch (Exception ex)
{
MelonLoader.MelonLogger.Msg($"[SwitchDiag] exception: {ex.Message}");
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using Il2Cpp;
using UnityEngine;
namespace DataCenterModLoader;
public static class TechnicianHiring
{
private readonly struct TechDef
{
public readonly string Id;
public readonly string Name;
public readonly string Description;
public readonly float Salary;
public readonly float RequiredRep;
public TechDef(string id, string name, string description, float salary, float requiredRep)
{
Id = id;
Name = name;
Description = description;
Salary = salary;
RequiredRep = requiredRep;
}
}
private static readonly List<TechDef> _definitions = new()
{
new TechDef("tech_extra_1", "Junior Technician I", "A fresh hire eager to learn the ropes. Slow but cheap.", 1500f, 0f),
new TechDef("tech_extra_2", "Junior Technician II", "A second pair of hands. Still learning.", 2000f, 100f),
new TechDef("tech_extra_3", "Technician III", "A reliable technician with solid skills.", 3000f, 250f),
new TechDef("tech_extra_4", "Technician IV", "An experienced technician who gets the job done.", 4000f, 500f),
new TechDef("tech_extra_5", "Senior Technician V", "A senior technician. Fast and efficient.", 5500f, 800f),
new TechDef("tech_extra_6", "Senior Technician VI", "A highly skilled senior technician.", 7000f, 1200f),
new TechDef("tech_extra_7", "Lead Technician", "The best in the business. Worth every penny.", 9000f, 1800f),
};
// Spawned tech IDs in hire order (LIFO for firing).
private static readonly List<int> _spawnedTechIds = new();
// Starts at 1000 to avoid collisions with built-in technicians.
private static int _nextTechId = 1000;
private static bool _initialized = false;
// Safe to call multiple times.
public static void Initialize()
{
if (_initialized) return;
try
{
CrashLog.Log("TechnicianHiring.Initialize: registering 7 extra technician employees");
foreach (var def in _definitions)
{
try
{
int result = CustomEmployeeManager.Register(
def.Id,
def.Name,
def.Description,
def.Salary,
def.RequiredRep,
requiresConfirmation: true
);
CrashLog.Log($"TechnicianHiring: registered '{def.Id}' ({def.Name}) result={result}");
Core.Instance?.LoggerInstance.Msg(
$"[TechnicianHiring] Registered: {def.Name} (salary={def.Salary}/h, rep={def.RequiredRep})");
}
catch (Exception ex)
{
CrashLog.LogException($"TechnicianHiring.Initialize: failed to register '{def.Id}'", ex);
}
}
_initialized = true;
CrashLog.Log("TechnicianHiring.Initialize: complete");
}
catch (Exception ex)
{
CrashLog.LogException("TechnicianHiring.Initialize", ex);
}
}
public static void OnEmployeeHired(string employeeId)
{
try
{
if (string.IsNullOrEmpty(employeeId) || !employeeId.StartsWith("tech_extra_"))
return;
CrashLog.Log($"TechnicianHiring.OnEmployeeHired: handling '{employeeId}'");
var tm = TechnicianManager.instance;
if (tm == null)
{
CrashLog.Log("TechnicianHiring.OnEmployeeHired: TechnicianManager.instance is null aborting");
Core.Instance?.LoggerInstance.Error("[TechnicianHiring] TechnicianManager not available");
return;
}
var existing = tm.technicians;
if (existing == null || existing.Count == 0)
{
CrashLog.Log("TechnicianHiring.OnEmployeeHired: No existing technicians to clone");
Core.Instance?.LoggerInstance.Error("[TechnicianHiring] No existing technicians to clone");
return;
}
int existingCount = existing.Count;
int sourceIndex = _spawnedTechIds.Count % existingCount;
var source = existing[sourceIndex];
if (source == null)
{
CrashLog.Log($"TechnicianHiring.OnEmployeeHired: source technician at index {sourceIndex} is null");
return;
}
CrashLog.Log($"TechnicianHiring.OnEmployeeHired: cloning technician at index {sourceIndex} ('{source.technicianName}')");
var clone = UnityEngine.Object.Instantiate(source.gameObject);
if (clone == null)
{
CrashLog.Log("TechnicianHiring.OnEmployeeHired: Object.Instantiate returned null");
return;
}
var tech = clone.GetComponent<Technician>();
if (tech == null)
{
CrashLog.Log("TechnicianHiring.OnEmployeeHired: cloned object has no Technician component");
UnityEngine.Object.Destroy(clone);
return;
}
int newId = _nextTechId++;
tech.technicianID = newId;
tech.technicianName = "Mod Tech " + newId;
// Salary handled by CustomEmployeeManager, zero it out here
tech.salary = 0;
tech.transformContainer = tm.transformContainer;
tech.transformDumpster = tm.transformDumpster;
tech.transformDeviceSpawnPosition = tm.transformDeviceSpawnPosition;
if (tm.transformIdle != null && tm.transformIdle.Length > 0)
{
int idleIndex = _spawnedTechIds.Count % tm.transformIdle.Length;
tech.transformIdle = tm.transformIdle[idleIndex];
CrashLog.Log($"TechnicianHiring.OnEmployeeHired: assigned idle transform index {idleIndex}");
}
else
{
CrashLog.Log("TechnicianHiring.OnEmployeeHired: no idle transforms available on TechnicianManager");
}
tm.AddTechnician(tech);
_spawnedTechIds.Add(newId);
CrashLog.Log($"TechnicianHiring.OnEmployeeHired: spawned technician id={newId} for employee '{employeeId}' (total spawned: {_spawnedTechIds.Count})");
Core.Instance?.LoggerInstance.Msg($"[TechnicianHiring] Spawned technician #{newId} for '{employeeId}'");
}
catch (Exception ex)
{
CrashLog.LogException($"TechnicianHiring.OnEmployeeHired({employeeId})", ex);
}
}
public static void OnEmployeeFired(string employeeId)
{
try
{
if (string.IsNullOrEmpty(employeeId) || !employeeId.StartsWith("tech_extra_"))
return;
CrashLog.Log($"TechnicianHiring.OnEmployeeFired: handling '{employeeId}'");
if (_spawnedTechIds.Count == 0)
{
CrashLog.Log("TechnicianHiring.OnEmployeeFired: no spawned technicians to remove ignoring");
Core.Instance?.LoggerInstance.Warning("[TechnicianHiring] No spawned technicians to fire");
return;
}
var id = _spawnedTechIds[^1];
_spawnedTechIds.RemoveAt(_spawnedTechIds.Count - 1);
CrashLog.Log($"TechnicianHiring.OnEmployeeFired: firing technician id={id} (remaining: {_spawnedTechIds.Count})");
TechnicianManager.instance?.FireTechnician(id);
CrashLog.Log($"TechnicianHiring.OnEmployeeFired: technician id={id} fired successfully");
Core.Instance?.LoggerInstance.Msg($"[TechnicianHiring] Fired technician #{id} for '{employeeId}'");
}
catch (Exception ex)
{
CrashLog.LogException($"TechnicianHiring.OnEmployeeFired({employeeId})", ex);
}
}
public static void RestoreOnLoad()
{
try
{
CrashLog.Log("TechnicianHiring.RestoreOnLoad: checking for previously hired technicians");
_spawnedTechIds.Clear();
int restored = 0;
foreach (var def in _definitions)
{
try
{
if (CustomEmployeeManager.IsHired(def.Id))
{
CrashLog.Log($"TechnicianHiring.RestoreOnLoad: '{def.Id}' is hired re-spawning");
OnEmployeeHired(def.Id);
restored++;
}
}
catch (Exception ex)
{
CrashLog.LogException($"TechnicianHiring.RestoreOnLoad: failed to restore '{def.Id}'", ex);
}
}
CrashLog.Log($"TechnicianHiring.RestoreOnLoad: restored {restored} technician(s)");
if (restored > 0)
{
Core.Instance?.LoggerInstance.Msg($"[TechnicianHiring] Restored {restored} extra technician(s) from save");
}
}
catch (Exception ex)
{
CrashLog.LogException("TechnicianHiring.RestoreOnLoad", ex);
}
}
}
@@ -0,0 +1,148 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<AssemblyName>greg.JoniMLCompatMod</AssemblyName>
<RootNamespace>DataCenterModLoader</RootNamespace>
<Description>MelonLoader mod that provides a Rust FFI bridge for Data Center</Description>
<!-- Suppress warnings about version conflicts with Il2Cpp assemblies -->
<NoWarn>CS1701;CS1702</NoWarn>
<!-- Add both MelonLoader directories as assembly search paths for transitive dependency resolution -->
<ReferencePath>$(MelonLoaderDir)\net6;$(MelonLoaderDir)\Il2CppAssemblies</ReferencePath>
<AssemblySearchPaths>{CandidateAssemblyFiles};{HintPathFromItem};$(MelonLoaderDir)\net6;$(MelonLoaderDir)\Il2CppAssemblies;{TargetFrameworkDirectory};{RawFileName}</AssemblySearchPaths>
</PropertyGroup>
<!--
MelonLoader + Il2Cpp assemblies live under the game folder.
Override if your install path differs:
dotnet build -c Release -p:DataCenterGameRoot="C:\Program Files (x86)\Steam\steamapps\common\Data Center"
Or set DataCenterGameRoot in Directory.Build.props.
-->
<PropertyGroup>
<DataCenterGameRoot Condition="'$(DataCenterGameRoot)' == ''">C:\Program Files (x86)\Steam\steamapps\common\Data Center</DataCenterGameRoot>
<MelonLoaderDir>$(DataCenterGameRoot)\MelonLoader</MelonLoaderDir>
</PropertyGroup>
<ItemGroup>
<!-- MelonLoader core -->
<Reference Include="MelonLoader">
<HintPath>$(MelonLoaderDir)\net6\MelonLoader.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- HarmonyX (shipped with MelonLoader, needed for [HarmonyPatch] attributes) -->
<Reference Include="0Harmony">
<HintPath>$(MelonLoaderDir)\net6\0Harmony.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Il2CppInterop runtime (needed for Il2Cpp type access) -->
<Reference Include="Il2CppInterop.Runtime">
<HintPath>$(MelonLoaderDir)\net6\Il2CppInterop.Runtime.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Il2Cpp mscorlib (needed for Il2Cpp base types like Il2CppObjectBase) -->
<Reference Include="Il2Cppmscorlib">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\Il2Cppmscorlib.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Il2CppSystem (needed for Il2Cpp collection types) -->
<Reference Include="Il2CppSystem">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\Il2CppSystem.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Game assembly (contains PlayerManager, BalanceSheet, TimeController, etc.) -->
<Reference Include="Assembly-CSharp">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\Assembly-CSharp.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Unity engine core (contains UnityEngine.Object, Time, MonoBehaviour, etc.) -->
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.CoreModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Unity UI module (needed for Button, Text, Image, LayoutGroup, etc.) -->
<Reference Include="UnityEngine.UI">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.UI.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- TextMeshPro (needed for TextMeshProUGUI text component access) -->
<Reference Include="Unity.TextMeshPro">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\Unity.TextMeshPro.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- AI Module (needed for NavMeshAgent removal on remote player models) -->
<Reference Include="UnityEngine.AIModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.AIModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- UI Module (needed for Canvas, RenderMode on nametags) -->
<Reference Include="UnityEngine.UIModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.UIModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Input Legacy Module (needed for Input.GetKeyDown, KeyCode) -->
<Reference Include="UnityEngine.InputLegacyModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.InputLegacyModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Unity Input System (new Input System package, needed for Keyboard.current) -->
<Reference Include="Unity.InputSystem">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\Unity.InputSystem.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Text Rendering Module (needed for FontStyle, TextAnchor in IMGUI styles) -->
<Reference Include="UnityEngine.TextRenderingModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.TextRenderingModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- IMGUI Module (needed for GUI/GUILayout in multiplayer panel) -->
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.IMGUIModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Unity Physics Module (needed for Collider/Rigidbody if used) -->
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.PhysicsModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Image Conversion Module (needed for ImageConversion.LoadImage to load PNG portraits) -->
<Reference Include="UnityEngine.ImageConversionModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.ImageConversionModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- Animation Module (needed for Animator, AnimatorControllerParameterType on remote players) -->
<Reference Include="UnityEngine.AnimationModule">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\UnityEngine.AnimationModule.dll</HintPath>
<Private>false</Private>
</Reference>
<!-- UMA Core (needed for DynamicCharacterAvatar, UMAData, UMAGenerator on remote players) -->
<Reference Include="Il2CppUMA_Core">
<HintPath>$(MelonLoaderDir)\Il2CppAssemblies\Il2CppUMA_Core.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+87
View File
@@ -0,0 +1,87 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v6.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"gregCore/1.0.0.33-pre": {
"dependencies": {
"Jint": "4.1.0",
"Mono.Cecil": "0.11.6"
},
"runtime": {
"gregCore.dll": {}
}
},
"Acornima/1.1.0": {
"runtime": {
"lib/net6.0/Acornima.dll": {
"assemblyVersion": "1.1.0.0",
"fileVersion": "1.1.0.0"
}
}
},
"Jint/4.1.0": {
"dependencies": {
"Acornima": "1.1.0"
},
"runtime": {
"lib/net6.0/Jint.dll": {
"assemblyVersion": "4.1.0.0",
"fileVersion": "4.0.0.0"
}
}
},
"Mono.Cecil/0.11.6": {
"runtime": {
"lib/netstandard2.0/Mono.Cecil.Mdb.dll": {
"assemblyVersion": "0.11.6.0",
"fileVersion": "0.11.6.0"
},
"lib/netstandard2.0/Mono.Cecil.Pdb.dll": {
"assemblyVersion": "0.11.6.0",
"fileVersion": "0.11.6.0"
},
"lib/netstandard2.0/Mono.Cecil.Rocks.dll": {
"assemblyVersion": "0.11.6.0",
"fileVersion": "0.11.6.0"
},
"lib/netstandard2.0/Mono.Cecil.dll": {
"assemblyVersion": "0.11.6.0",
"fileVersion": "0.11.6.0"
}
}
}
}
},
"libraries": {
"gregCore/1.0.0.33-pre": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Acornima/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MFK/GNp09PF8H6uSkAZght553TddOejD9P1InvKWlGw6ILhmZjI+Y2xiIjGoJ73sbkeZzxwnCWZioK5Wed/J2g==",
"path": "acornima/1.1.0",
"hashPath": "acornima.1.1.0.nupkg.sha512"
},
"Jint/4.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-4PzK4dTMOkS7NarKpAGmj3TMdlAUJ+V7w03usuLA4+ETqZnM6lt/bFlZXTtrOhLn32tjqoO803SzTMIv27EgYw==",
"path": "jint/4.1.0",
"hashPath": "jint.4.1.0.nupkg.sha512"
},
"Mono.Cecil/0.11.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==",
"path": "mono.cecil/0.11.6",
"hashPath": "mono.cecil.0.11.6.nupkg.sha512"
}
}
}
Binary file not shown.
Binary file not shown.
+227
View File
@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MelonLoader;
using UnityEngine;
using gregCore.PublicApi;
using gregCore.Core.Models;
namespace gregCore.API;
public enum GregEventId : uint
{
MoneyChanged = 100, XpChanged = 101, ReputationChanged = 102,
ServerPowered = 200, ServerBroken = 201, ServerRepaired = 202,
ServerInstalled = 203, CableConnected = 204, CableDisconnected = 205,
ServerCustomerChanged = 206, ServerAppChanged = 207, RackUnmounted = 208,
SwitchBroken = 209, SwitchRepaired = 210, ObjectSpawned = 211,
ObjectPickedUp = 212, ObjectDropped = 213,
DayEnded = 300, MonthEnded = 301,
CustomerAccepted = 400, CustomerSatisfied = 401, CustomerUnsatisfied = 402,
ShopCheckout = 500, ShopItemAdded = 501, ShopCartCleared = 502, ShopItemRemoved = 503,
EmployeeHired = 600, EmployeeFired = 601,
GameSaved = 700, GameLoaded = 701, GameAutoSaved = 702,
WallPurchased = 800,
CustomEmployeeHired = 1000, CustomEmployeeFired = 1001,
}
public static class GregAPI
{
private static readonly Dictionary<GregEventId, string> _eventIdToHook = new()
{
{ GregEventId.MoneyChanged, "greg.economy.PlayerCoinUpdated" },
{ GregEventId.XpChanged, "greg.economy.PlayerXpUpdated" },
{ GregEventId.ReputationChanged, "greg.economy.PlayerReputationUpdated" },
{ GregEventId.ServerPowered, "greg.hardware.ServerPowered" },
{ GregEventId.ServerBroken, "greg.hardware.ServerBroken" },
{ GregEventId.ServerRepaired, "greg.hardware.ServerRepaired" },
{ GregEventId.ServerInstalled, "greg.hardware.ServerInstalled" },
{ GregEventId.DayEnded, "greg.lifecycle.DayEnded" },
{ GregEventId.MonthEnded, "greg.lifecycle.MonthEnded" },
{ GregEventId.GameLoaded, "greg.lifecycle.GameLoaded" },
{ GregEventId.GameSaved, "greg.lifecycle.GameSaved" },
};
private static readonly Dictionary<GregEventId, List<Action<ulong>>> _subscriptions = new();
public static void Initialize()
{
LogInfo("GregAPI initializing...");
}
// internal DI container hooks for new services
internal static gregCore.Infrastructure.Settings.GregKeybindRegistry _keybindReg;
internal static gregCore.Infrastructure.Settings.GregModSettingsService _modSettingsService;
private static gregCore.Sdk.IGregAPI? _sdkApi;
public static gregCore.Sdk.IGregAPI GetSdkApi()
{
if (_sdkApi == null)
{
_sdkApi = gregCore.GameLayer.Bootstrap.GregServiceContainer.Get<gregCore.Sdk.IGregAPI>();
}
return _sdkApi ?? throw new Exception("SDK API not initialized");
}
public static void RegisterMod(string modId, string name, string version, object apiObject = null)
{
GetSdkApi().RegisterMod(modId, name, version, apiObject);
}
public static class Settings
{
public static void RegisterToggle(string modId, string settingId, string displayName, bool defaultValue, Action<bool> onChanged = null, string category = "General", string description = "")
{
GetSdkApi().RegisterToggle(modId, settingId, displayName, defaultValue, onChanged, category, description);
}
public static void RegisterSlider(string modId, string settingId, string displayName, float defaultValue, Action<float> onChanged = null, string category = "General", string description = "")
{
GetSdkApi().RegisterSlider(modId, settingId, displayName, defaultValue, onChanged, category, description);
}
}
public static class Input
{
public static void RegisterKeybind(string modId, string actionId, string displayName, KeyCode defaultKey, Action onPress, string category = "Controls", string description = "")
{
GetSdkApi().RegisterKeybind(modId, actionId, displayName, defaultKey, onPress, category, description);
}
}
public static class Hooks
{
public static void On(string hookName, Action<gregCore.Sdk.Models.GregPayload> handler)
{
GetSdkApi().On(hookName, handler);
}
public static void Fire(string hookName, gregCore.Sdk.Models.GregPayload payload)
{
GetSdkApi().Fire(hookName, payload);
}
}
// --- Economy ---
public static double GetPlayerMoney() => gregCore.PublicApi.greg.Economy.GetBalance();
public static void SetPlayerMoney(double amount) => gregCore.PublicApi.greg.Economy.SetBalance((float)amount);
public static double GetPlayerXp() => gregCore.PublicApi.greg.Economy.GetXP();
public static void SetPlayerXp(double amount) => gregCore.PublicApi.greg.Economy.AddXP((float)(amount - GetPlayerXp()));
public static double GetPlayerReputation() => gregCore.PublicApi.greg.Economy.GetReputation();
public static void SetPlayerReputation(double amount) => gregCore.PublicApi.greg.Economy.AddReputation((float)(amount - GetPlayerReputation()));
// --- World ---
public static uint GetServerCount() => (uint)gregCore.PublicApi.greg.Server.GetCount();
public static uint GetRackCount() => (uint)gregCore.PublicApi.greg.Facility.GetRackCount();
public static uint GetSwitchCount() => (uint)gregCore.PublicApi.greg.Network.GetSwitchCount();
public static uint GetBrokenServerCount() => (uint)gregCore.PublicApi.greg.Server.GetBrokenCount();
public static uint GetBrokenSwitchCount() => (uint)gregCore.PublicApi.greg.Network.GetBrokenSwitchCount();
// --- Technicians ---
public static uint GetFreeTechnicianCount() => (uint)gregCore.PublicApi.greg.Npc.GetFreeTechnicianCount();
public static uint GetTotalTechnicianCount() => (uint)gregCore.PublicApi.greg.Npc.GetTotalTechnicianCount();
public static int DispatchRepairServer() => gregCore.PublicApi.greg.Npc.DispatchRepairServer(null!) ? 0 : -1;
public static int DispatchRepairSwitch() => gregCore.PublicApi.greg.Npc.DispatchRepairSwitch(null!) ? 0 : -1;
// --- Time ---
public static float GetTimeOfDay() => gregCore.PublicApi.greg.Time.GetTimeOfDay();
public static uint GetDay() => (uint)gregCore.PublicApi.greg.Time.GetDay();
public static float GetSecondsInFullDay() => gregCore.PublicApi.greg.Time.GetSecondsInFullDay();
public static void SetSecondsInFullDay(float s) => gregCore.PublicApi.greg.Time.SetSecondsInFullDay(s);
// --- Game ---
public static string GetCurrentScene() => gregCore.PublicApi.greg.Save.GetCurrentScene();
public static bool IsGamePaused() => gregCore.PublicApi.greg.Time.IsPaused();
public static void SetGamePaused(bool paused) => gregCore.PublicApi.greg.Time.SetPaused(paused);
public static float GetTimeScale() => gregCore.PublicApi.greg.Time.GetTimeScale();
public static void SetTimeScale(float scale) => gregCore.PublicApi.greg.Time.SetTimeScale(scale);
public static int TriggerSave() { gregCore.PublicApi.greg.Save.TriggerSave(); return 0; }
public static int GetDifficulty() => gregCore.PublicApi.greg.Save.GetDifficulty();
// --- Player ---
public static (float x, float y, float z, float ry) GetPlayerPosition()
{
var pos = gregCore.PublicApi.greg.Player.GetPosition();
var rot = gregCore.PublicApi.greg.Player.GetRotation();
return (pos.x, pos.y, pos.z, rot.y);
}
// --- UI / Logging ---
public static void ShowNotification(string message) => gregCore.PublicApi.greg.UI.ShowNotification(message);
public static void LogInfo(string message) {
gregCore.Infrastructure.Logging.GregLogger.Info("API", message);
gregCore.Infrastructure.UI.GregDevConsole.Instance.AddLog(message, LogType.Log);
}
public static void LogWarning(string message) {
gregCore.Infrastructure.Logging.GregLogger.Warning("API", message);
gregCore.Infrastructure.UI.GregDevConsole.Instance.AddLog(message, LogType.Warning);
}
public static void LogError(string message) {
gregCore.Infrastructure.Logging.GregLogger.Error("API", message);
gregCore.Infrastructure.UI.GregDevConsole.Instance.AddLog(message, LogType.Error);
}
public static void LogSuccess(string message) {
gregCore.Infrastructure.Logging.GregLogger.Success("API", message);
gregCore.Infrastructure.UI.GregDevConsole.Instance.AddLog(message, LogType.Log);
}
// --- Events ---
public static void FireEvent(GregEventId eventId, ulong data = 0)
{
if (gregCore.PublicApi.greg.IsInitialized && _eventIdToHook.TryGetValue(eventId, out string hookName))
{
var ctx = gregCore.PublicApi.greg._context;
ctx?.EventBus.Publish(hookName, new EventPayload
{
HookName = hookName,
Data = new Dictionary<string, object> { { "raw_data", data } }
});
}
if (_subscriptions.TryGetValue(eventId, out var handlers))
{
foreach (var handler in handlers)
{
try { handler(data); }
catch (Exception ex) { LogError($"Error in Event-Handler for {eventId}: {ex.Message}"); }
}
}
}
public static void Subscribe(GregEventId eventId, Action<ulong> handler)
{
if (!_subscriptions.ContainsKey(eventId))
_subscriptions[eventId] = new List<Action<ulong>>();
_subscriptions[eventId].Add(handler);
if (gregCore.PublicApi.greg.IsInitialized && _eventIdToHook.TryGetValue(eventId, out string hookName))
{
var ctx = gregCore.PublicApi.greg._context;
ctx?.EventBus.Subscribe(hookName, payload => {
ulong data = 0;
if (payload.Data.TryGetValue("raw_data", out var d)) data = Convert.ToUInt64(d);
else if (payload.Data.TryGetValue("NewValue", out var nv)) data = Convert.ToUInt64(nv);
handler(data);
});
}
}
public static void Unsubscribe(GregEventId eventId, Action<ulong> handler)
{
if (_subscriptions.TryGetValue(eventId, out var handlers))
{
handlers.Remove(handler);
}
}
// --- Config ---
public static void ConfigSetBool(string modId, string key, bool value) => gregCore.PublicApi.greg.Save.Set($"{modId}.{key}", value);
public static bool ConfigGetBool(string modId, string key, bool defaultValue = false) => gregCore.PublicApi.greg.Save.Get($"{modId}.{key}", defaultValue);
public static void ConfigSetInt(string modId, string key, int value) => gregCore.PublicApi.greg.Save.Set($"{modId}.{key}", value);
public static int ConfigGetInt(string modId, string key, int defaultValue = 0) => gregCore.PublicApi.greg.Save.Get($"{modId}.{key}", defaultValue);
public static void ConfigSetFloat(string modId, string key, float value) => gregCore.PublicApi.greg.Save.Set($"{modId}.{key}", value);
public static float ConfigGetFloat(string modId, string key, float defaultValue = 0f) => gregCore.PublicApi.greg.Save.Get($"{modId}.{key}", defaultValue);
public static void ConfigSetString(string modId, string key, string value) => gregCore.PublicApi.greg.Save.Set($"{modId}.{key}", value);
public static string ConfigGetString(string modId, string key, string defaultValue = "") => gregCore.PublicApi.greg.Save.Get($"{modId}.{key}", defaultValue);
}
+247
View File
@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MelonLoader;
using gregCore.API;
using gregCore.Core.Models;
namespace gregCore.Bridge.GoFFI;
public static class GoFFIBridge
{
private static readonly List<IntPtr> _loadedLibraries = new();
private static readonly List<GoPlugin> _plugins = new();
private static GregCoreAPI _apiTable;
private static IntPtr _apiTablePtr;
// Delegates for the function table
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void LogDelegate(IntPtr msg);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate double GetDoubleDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetDoubleDelegate(double val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate uint GetUintDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int DispatchDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate float GetFloatDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetFloatDelegate(float val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate IntPtr GetStringDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetStringDelegate(IntPtr str);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int GetIntDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetIntDelegate(int val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void GetPlayerPosDelegate(out float x, out float y, out float z, out float ry);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void EventActionDelegate(uint eventId, ulong data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SubscribeDelegate(uint eventId, IntPtr callbackPtr);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void OnHookDelegate(IntPtr hookName, IntPtr callbackPtr);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void FireHookDelegate(IntPtr hookName, IntPtr jsonData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void HookActionDelegate(IntPtr hookName, IntPtr trigger, IntPtr jsonData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void ConfigSetBoolDelegate(IntPtr modId, IntPtr key, uint val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate uint ConfigGetBoolDelegate(IntPtr modId, IntPtr key, uint def);
private static readonly List<Delegate> _delegateCache = new();
public static void Initialize()
{
GregAPI.LogInfo("GoFFIBridge initializing...");
SetupApiTable();
LoadPlugins();
}
private static void SetupApiTable()
{
_apiTable = new GregCoreAPI { api_version = 1 };
_apiTable.log_info = AddDelegate<LogDelegate>(ptr => GregAPI.LogInfo(Marshal.PtrToStringAnsi(ptr) ?? ""));
_apiTable.log_warning = AddDelegate<LogDelegate>(ptr => GregAPI.LogWarning(Marshal.PtrToStringAnsi(ptr) ?? ""));
_apiTable.log_error = AddDelegate<LogDelegate>(ptr => GregAPI.LogError(Marshal.PtrToStringAnsi(ptr) ?? ""));
_apiTable.get_player_money = AddDelegate<GetDoubleDelegate>(() => GregAPI.GetPlayerMoney());
_apiTable.set_player_money = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetPlayerMoney(val));
_apiTable.get_player_xp = AddDelegate<GetDoubleDelegate>(() => GregAPI.GetPlayerXp());
_apiTable.set_player_xp = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetPlayerXp(val));
_apiTable.get_player_reputation = AddDelegate<GetDoubleDelegate>(() => GregAPI.GetPlayerReputation());
_apiTable.set_player_reputation = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetPlayerReputation(val));
_apiTable.get_server_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetServerCount());
_apiTable.get_rack_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetRackCount());
_apiTable.get_switch_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetSwitchCount());
_apiTable.get_broken_server_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetBrokenServerCount());
_apiTable.get_broken_switch_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetBrokenSwitchCount());
_apiTable.get_free_technician_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetFreeTechnicianCount());
_apiTable.get_total_technician_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetTotalTechnicianCount());
_apiTable.dispatch_repair_server = AddDelegate<DispatchDelegate>(() => GregAPI.DispatchRepairServer());
_apiTable.dispatch_repair_switch = AddDelegate<DispatchDelegate>(() => GregAPI.DispatchRepairSwitch());
_apiTable.get_time_of_day = AddDelegate<GetFloatDelegate>(() => GregAPI.GetTimeOfDay());
_apiTable.get_day = AddDelegate<GetUintDelegate>(() => GregAPI.GetDay());
_apiTable.get_seconds_in_full_day = AddDelegate<GetFloatDelegate>(() => GregAPI.GetSecondsInFullDay());
_apiTable.set_seconds_in_full_day = AddDelegate<SetFloatDelegate>(val => GregAPI.SetSecondsInFullDay(val));
_apiTable.get_current_scene = AddDelegate<GetStringDelegate>(() => Marshal.StringToHGlobalAnsi(GregAPI.GetCurrentScene()));
_apiTable.is_game_paused = AddDelegate<GetUintDelegate>(() => GregAPI.IsGamePaused() ? 1u : 0u);
_apiTable.set_game_paused = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetGamePaused(val > 0));
_apiTable.get_time_scale = AddDelegate<GetFloatDelegate>(() => GregAPI.GetTimeScale());
_apiTable.set_time_scale = AddDelegate<SetFloatDelegate>(val => GregAPI.SetTimeScale(val));
_apiTable.trigger_save = AddDelegate<DispatchDelegate>(() => GregAPI.TriggerSave());
_apiTable.get_difficulty = AddDelegate<GetIntDelegate>(() => GregAPI.GetDifficulty());
_apiTable.get_player_position = AddDelegate<GetPlayerPosDelegate>((out float x, out float y, out float z, out float ry) => {
var pos = GregAPI.GetPlayerPosition();
x = pos.x; y = pos.y; z = pos.z; ry = pos.ry;
});
_apiTable.show_notification = AddDelegate<LogDelegate>(ptr => GregAPI.ShowNotification(Marshal.PtrToStringAnsi(ptr) ?? ""));
_apiTable.subscribe_event = AddDelegate<SubscribeDelegate>((eventId, cbPtr) => {
var callback = Marshal.GetDelegateForFunctionPointer<EventActionDelegate>(cbPtr);
GregAPI.Subscribe((GregEventId)eventId, data => callback(eventId, data));
});
_apiTable.fire_event = AddDelegate<EventActionDelegate>((id, data) => GregAPI.FireEvent((GregEventId)id, data));
// Hook API (New)
_apiTable.on_hook = AddDelegate<OnHookDelegate>((hookPtr, cbPtr) => {
string hookName = Marshal.PtrToStringAnsi(hookPtr) ?? "";
var callback = Marshal.GetDelegateForFunctionPointer<HookActionDelegate>(cbPtr);
GregAPI.Hooks.On(hookName, payload => {
string json = Newtonsoft.Json.JsonConvert.SerializeObject(payload.Data);
IntPtr hPtr = Marshal.StringToHGlobalAnsi(payload.HookName);
IntPtr tPtr = Marshal.StringToHGlobalAnsi(payload.Trigger);
IntPtr jPtr = Marshal.StringToHGlobalAnsi(json);
callback(hPtr, tPtr, jPtr);
Marshal.FreeHGlobal(hPtr);
Marshal.FreeHGlobal(tPtr);
Marshal.FreeHGlobal(jPtr);
});
});
_apiTable.fire_hook = AddDelegate<FireHookDelegate>((hookPtr, jsonPtr) => {
string hookName = Marshal.PtrToStringAnsi(hookPtr) ?? "";
string json = Marshal.PtrToStringAnsi(jsonPtr) ?? "{}";
var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(json) ?? new();
var payload = new gregCore.Sdk.Models.GregPayload(hookName, "GoMod") { Data = data };
GregAPI.Hooks.Fire(hookName, payload);
});
// Config
_apiTable.config_set_bool = AddDelegate<ConfigSetBoolDelegate>((modId, key, val) =>
GregAPI.ConfigSetBool(Marshal.PtrToStringAnsi(modId) ?? "unknown", Marshal.PtrToStringAnsi(key) ?? "unknown", val > 0));
_apiTable.config_get_bool = AddDelegate<ConfigGetBoolDelegate>((modId, key, def) =>
GregAPI.ConfigGetBool(Marshal.PtrToStringAnsi(modId) ?? "unknown", Marshal.PtrToStringAnsi(key) ?? "unknown", def > 0) ? 1u : 0u);
_apiTablePtr = Marshal.AllocHGlobal(Marshal.SizeOf<GregCoreAPI>());
Marshal.StructureToPtr(_apiTable, _apiTablePtr, false);
}
private static IntPtr AddDelegate<T>(T del) where T : Delegate
{
_delegateCache.Add(del);
return Marshal.GetFunctionPointerForDelegate(del);
}
private static void LoadPlugins()
{
string gameRoot = global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
string goDir = Path.Combine(gameRoot, "Plugins", "Go");
if (!Directory.Exists(goDir)) Directory.CreateDirectory(goDir);
foreach (string dir in Directory.GetDirectories(goDir))
{
string dllPath = Path.Combine(dir, Path.GetFileName(dir) + ".dll");
if (!File.Exists(dllPath)) continue;
try
{
IntPtr lib = System.Runtime.InteropServices.NativeLibrary.Load(dllPath);
if (lib == IntPtr.Zero) continue;
_loadedLibraries.Add(lib);
IntPtr infoFunc = System.Runtime.InteropServices.NativeLibrary.GetExport(lib, "greg_mod_info");
IntPtr initFunc = System.Runtime.InteropServices.NativeLibrary.GetExport(lib, "greg_mod_init");
if (infoFunc == IntPtr.Zero || initFunc == IntPtr.Zero) continue;
var getInfo = Marshal.GetDelegateForFunctionPointer<Func<GregModInfo>>(infoFunc);
var info = getInfo();
var init = Marshal.GetDelegateForFunctionPointer<Func<IntPtr, bool>>(initFunc);
if (init(_apiTablePtr))
{
var plugin = new GoPlugin
{
Id = Marshal.PtrToStringAnsi(info.id) ?? "Unknown",
Handle = lib,
Update = GetOptionalExport<Action<float>>(lib, "greg_mod_update"),
OnEvent = GetOptionalExport<Action<uint, ulong>>(lib, "greg_mod_event"),
OnSceneLoaded = GetOptionalExport<Action<IntPtr>>(lib, "greg_mod_scene_loaded"),
Shutdown = GetOptionalExport<Action>(lib, "greg_mod_shutdown")
};
_plugins.Add(plugin);
GregAPI.LogInfo($"Go Plugin loaded: {plugin.Id}");
}
}
catch (Exception ex)
{
GregAPI.LogError($"Error loading Go Plugin {dllPath}: {ex.Message}");
}
}
}
private static T? GetOptionalExport<T>(IntPtr lib, string name) where T : Delegate
{
if (System.Runtime.InteropServices.NativeLibrary.TryGetExport(lib, name, out IntPtr ptr))
return Marshal.GetDelegateForFunctionPointer<T>(ptr);
return null;
}
public static void OnUpdate(float dt)
{
foreach (var p in _plugins) p.Update?.Invoke(dt);
}
public static void OnSceneLoaded(string name)
{
IntPtr namePtr = Marshal.StringToHGlobalAnsi(name);
foreach (var p in _plugins) p.OnSceneLoaded?.Invoke(namePtr);
Marshal.FreeHGlobal(namePtr);
}
public static void Shutdown()
{
foreach (var p in _plugins) p.Shutdown?.Invoke();
foreach (var lib in _loadedLibraries) System.Runtime.InteropServices.NativeLibrary.Free(lib);
if (_apiTablePtr != IntPtr.Zero) Marshal.FreeHGlobal(_apiTablePtr);
}
[StructLayout(LayoutKind.Sequential)]
struct GregModInfo
{
public IntPtr id, name, version, author, description;
public uint api_version;
}
[StructLayout(LayoutKind.Sequential)]
struct GregCoreAPI
{
public uint api_version;
public IntPtr log_info, log_warning, log_error;
public IntPtr get_player_money, set_player_money, get_player_xp, set_player_xp, get_player_reputation, set_player_reputation;
public IntPtr get_server_count, get_rack_count, get_switch_count, get_broken_server_count, get_broken_switch_count;
public IntPtr get_free_technician_count, get_total_technician_count, dispatch_repair_server, dispatch_repair_switch;
public IntPtr get_time_of_day, get_day, get_seconds_in_full_day, set_seconds_in_full_day;
public IntPtr get_current_scene, is_game_paused, set_game_paused, get_time_scale, set_time_scale, trigger_save, get_difficulty;
public IntPtr get_player_position, show_notification;
public IntPtr subscribe_event, unsubscribe_event, fire_event;
public IntPtr on_hook, fire_hook;
public IntPtr config_set_bool, config_get_bool, config_set_int, config_get_int, config_set_float, config_get_float, config_set_string, config_get_string;
}
class GoPlugin
{
public string Id = "";
public IntPtr Handle;
public Action<float>? Update;
public Action<uint, ulong>? OnEvent;
public Action<IntPtr>? OnSceneLoaded;
public Action? Shutdown;
}
}
+192
View File
@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.IO;
using MoonSharp.Interpreter;
using MelonLoader;
using gregCore.API;
namespace gregCore.Bridge.LuaFFI;
public static class LuaFFIBridge
{
private static readonly List<LuaPlugin> _plugins = new();
public static void Initialize()
{
GregAPI.LogInfo("LuaFFIBridge initializing...");
string gameRoot = global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
string luaDir = Path.Combine(gameRoot, "Plugins", "Lua");
if (!Directory.Exists(luaDir)) Directory.CreateDirectory(luaDir);
LoadPlugins(luaDir);
}
private static void LoadPlugins(string luaDir)
{
foreach (string dir in Directory.GetDirectories(luaDir))
{
string mainFile = Path.Combine(dir, "main.lua");
if (!File.Exists(mainFile)) continue;
try
{
string id = Path.GetFileName(dir);
var script = new Script(CoreModules.Preset_SoftSandbox);
var gregTable = new Table(script);
RegisterApi(gregTable, script);
script.Globals["greg"] = gregTable;
script.DoFile(mainFile);
var plugin = new LuaPlugin
{
Id = id,
Script = script,
OnInit = script.Globals.Get("on_init").Type == DataType.Function ? script.Globals.Get("on_init").Function : null,
OnUpdate = script.Globals.Get("on_update").Type == DataType.Function ? script.Globals.Get("on_update").Function : null,
OnEvent = script.Globals.Get("on_event").Type == DataType.Function ? script.Globals.Get("on_event").Function : null,
OnSceneLoaded = script.Globals.Get("on_scene_loaded").Type == DataType.Function ? script.Globals.Get("on_scene_loaded").Function : null,
OnShutdown = script.Globals.Get("on_shutdown").Type == DataType.Function ? script.Globals.Get("on_shutdown").Function : null
};
SafeCall(plugin, plugin.OnInit);
_plugins.Add(plugin);
GregAPI.LogInfo($"Lua Plugin loaded: {id}");
}
catch (Exception ex)
{
GregAPI.LogError($"Error loading Lua Mod in {dir}: {ex.Message}");
}
}
}
private static void RegisterApi(Table greg, Script script)
{
// Logging
greg["log_info"] = (Action<string>)GregAPI.LogInfo;
greg["log_warning"] = (Action<string>)GregAPI.LogWarning;
greg["log_error"] = (Action<string>)GregAPI.LogError;
// Economy
greg["get_player_money"] = (Func<double>)GregAPI.GetPlayerMoney;
greg["set_player_money"] = (Action<double>)GregAPI.SetPlayerMoney;
greg["get_player_xp"] = (Func<double>)GregAPI.GetPlayerXp;
greg["set_player_xp"] = (Action<double>)GregAPI.SetPlayerXp;
greg["get_player_reputation"] = (Func<double>)GregAPI.GetPlayerReputation;
greg["set_player_reputation"] = (Action<double>)GregAPI.SetPlayerReputation;
// World
greg["get_server_count"] = (Func<uint>)GregAPI.GetServerCount;
greg["get_rack_count"] = (Func<uint>)GregAPI.GetRackCount;
greg["get_switch_count"] = (Func<uint>)GregAPI.GetSwitchCount;
greg["get_broken_server_count"] = (Func<uint>)GregAPI.GetBrokenServerCount;
greg["get_broken_switch_count"] = (Func<uint>)GregAPI.GetBrokenSwitchCount;
// Technicians
greg["get_free_technician_count"] = (Func<uint>)GregAPI.GetFreeTechnicianCount;
greg["get_total_technician_count"] = (Func<uint>)GregAPI.GetTotalTechnicianCount;
greg["dispatch_repair_server"] = (Func<int>)GregAPI.DispatchRepairServer;
greg["dispatch_repair_switch"] = (Func<int>)GregAPI.DispatchRepairSwitch;
// Time
greg["get_time_of_day"] = (Func<float>)GregAPI.GetTimeOfDay;
greg["get_day"] = (Func<uint>)GregAPI.GetDay;
greg["get_seconds_in_full_day"] = (Func<float>)GregAPI.GetSecondsInFullDay;
greg["set_seconds_in_full_day"] = (Action<float>)GregAPI.SetSecondsInFullDay;
// Game
greg["get_current_scene"] = (Func<string>)GregAPI.GetCurrentScene;
greg["is_game_paused"] = (Func<bool>)GregAPI.IsGamePaused;
greg["set_game_paused"] = (Action<bool>)GregAPI.SetGamePaused;
greg["get_time_scale"] = (Func<float>)GregAPI.GetTimeScale;
greg["set_time_scale"] = (Action<float>)GregAPI.SetTimeScale;
greg["trigger_save"] = (Func<int>)GregAPI.TriggerSave;
greg["get_difficulty"] = (Func<int>)GregAPI.GetDifficulty;
// Player
greg["get_player_position"] = (Func<Table>)(() => {
var p = GregAPI.GetPlayerPosition();
var t = new Table(script);
t["x"] = p.Item1; t["y"] = p.Item2; t["z"] = p.Item3; t["ry"] = p.Item4;
return t;
});
// UI
greg["show_notification"] = (Action<string>)GregAPI.ShowNotification;
// Events
greg["subscribe_event"] = (Action<uint, Closure>)((id, callback) => {
GregAPI.Subscribe((GregEventId)id, data => callback.Call(data));
});
greg["fire_event"] = (Action<uint, ulong>)((id, data) => GregAPI.FireEvent((GregEventId)id, data));
// Hook API (New)
greg["on"] = (Action<string, Closure>)((hookName, callback) => {
GregAPI.Hooks.On(hookName, payload => {
var table = new Table(script);
table["hook_name"] = payload.HookName;
table["trigger"] = payload.Trigger;
var dataTable = new Table(script);
foreach (var kvp in payload.Data) dataTable[kvp.Key] = kvp.Value;
table["data"] = dataTable;
callback.Call(table);
});
});
greg["fire"] = (Action<string, Table>)((hookName, dataTable) => {
var payload = new gregCore.Sdk.Models.GregPayload(hookName, "LuaMod");
foreach (var pair in dataTable.Pairs)
{
payload.Data[pair.Key.String] = pair.Value.ToObject();
}
GregAPI.Hooks.Fire(hookName, payload);
});
// Config
greg["config_set_bool"] = (Action<string, string, bool>)GregAPI.ConfigSetBool;
greg["config_get_bool"] = (Func<string, string, bool, bool>)GregAPI.ConfigGetBool;
greg["config_set_int"] = (Action<string, string, int>)GregAPI.ConfigSetInt;
greg["config_get_int"] = (Func<string, string, int, int>)GregAPI.ConfigGetInt;
greg["config_set_string"] = (Action<string, string, string>)GregAPI.ConfigSetString;
greg["config_get_string"] = (Func<string, string, string, string>)GregAPI.ConfigGetString;
}
public static void OnUpdate(float dt)
{
foreach (var p in _plugins) SafeCall(p, p.OnUpdate, dt);
}
public static void OnSceneLoaded(string name)
{
foreach (var p in _plugins) SafeCall(p, p.OnSceneLoaded, name);
}
public static void Shutdown()
{
foreach (var p in _plugins) SafeCall(p, p.OnShutdown);
_plugins.Clear();
}
private static void SafeCall(LuaPlugin p, Closure? func, params object[] args)
{
if (func == null) return;
try
{
func.Call(args);
}
catch (Exception ex)
{
GregAPI.LogError($"[LuaMod:{p.Id}] error: {ex.Message}");
}
}
class LuaPlugin
{
public string Id = "";
public Script Script = null!;
public Closure? OnInit, OnUpdate, OnEvent, OnSceneLoaded, OnShutdown;
}
}
+207
View File
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.IO;
using Python.Runtime;
using MelonLoader;
using gregCore.API;
namespace gregCore.Bridge.PythonFFI;
public static class PythonFFIBridge
{
private static readonly List<PythonPlugin> _plugins = new();
private static bool _isInitialized = false;
public static void Initialize()
{
GregAPI.LogInfo("PythonFFIBridge initializing...");
string gameRoot = global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
string pythonDir = Path.Combine(gameRoot, "Plugins", "Python");
if (!Directory.Exists(pythonDir)) Directory.CreateDirectory(pythonDir);
try
{
Runtime.PythonDLL = GregAPI.ConfigGetString("gregCore", "PythonDLL", "python310.dll");
PythonEngine.Initialize();
_isInitialized = true;
LoadPlugins(pythonDir);
}
catch (Exception ex)
{
GregAPI.LogError($"Failed to initialize Python engine: {ex.Message}");
}
}
private static void LoadPlugins(string pythonDir)
{
foreach (string dir in Directory.GetDirectories(pythonDir))
{
string mainFile = Path.Combine(dir, "main.py");
if (!File.Exists(mainFile)) continue;
try
{
using (Py.GIL())
{
string id = Path.GetFileName(dir);
var scope = Py.CreateScope();
scope.Set("greg", new GregPythonApi());
string code = File.ReadAllText(mainFile);
scope.Exec(code);
var plugin = new PythonPlugin
{
Id = id,
Scope = scope,
OnInit = GetMethod(scope, "on_init"),
OnUpdate = GetMethod(scope, "on_update"),
OnEvent = GetMethod(scope, "on_event"),
OnSceneLoaded = GetMethod(scope, "on_scene_loaded"),
OnShutdown = GetMethod(scope, "on_shutdown")
};
plugin.OnInit?.Invoke();
_plugins.Add(plugin);
GregAPI.LogInfo($"Python Plugin loaded: {id}");
}
}
catch (Exception ex)
{
GregAPI.LogError($"Error loading Python Mod in {dir}: {ex.Message}");
}
}
}
public static void OnUpdate(float dt)
{
if (!_isInitialized) return;
using (Py.GIL())
{
foreach (var p in _plugins) p.OnUpdate?.Invoke(dt.ToPython());
}
}
public static void OnSceneLoaded(string name)
{
if (!_isInitialized) return;
using (Py.GIL())
{
foreach (var p in _plugins) p.OnSceneLoaded?.Invoke(name.ToPython());
}
}
public static void Shutdown()
{
if (!_isInitialized) return;
using (Py.GIL())
{
foreach (var p in _plugins) p.OnShutdown?.Invoke();
}
PythonEngine.Shutdown();
}
private static PyObject? GetMethod(PyModule scope, string name)
{
if (scope.HasAttr(name))
{
var attr = scope.GetAttr(name);
if (attr.IsCallable()) return attr;
}
return null;
}
class PythonPlugin
{
public string Id = "";
public PyModule Scope = null!;
public PyObject? OnInit, OnUpdate, OnEvent, OnSceneLoaded, OnShutdown;
}
}
public class GregPythonApi
{
public void log_info(string msg) => GregAPI.LogInfo(msg);
public void log_warning(string msg) => GregAPI.LogWarning(msg);
public void log_error(string msg) => GregAPI.LogError(msg);
public double get_player_money() => GregAPI.GetPlayerMoney();
public void set_player_money(double amount) => GregAPI.SetPlayerMoney(amount);
public double get_player_xp() => GregAPI.GetPlayerXp();
public void set_player_xp(double amount) => GregAPI.SetPlayerXp(amount);
public double get_player_reputation() => GregAPI.GetPlayerReputation();
public void set_player_reputation(double amount) => GregAPI.SetPlayerReputation(amount);
public uint get_server_count() => GregAPI.GetServerCount();
public uint get_rack_count() => GregAPI.GetRackCount();
public uint get_switch_count() => GregAPI.GetSwitchCount();
public uint get_broken_server_count() => GregAPI.GetBrokenServerCount();
public uint get_broken_switch_count() => GregAPI.GetBrokenSwitchCount();
public uint get_free_technician_count() => GregAPI.GetFreeTechnicianCount();
public uint get_total_technician_count() => GregAPI.GetTotalTechnicianCount();
public int dispatch_repair_server() => GregAPI.DispatchRepairServer();
public int dispatch_repair_switch() => GregAPI.DispatchRepairSwitch();
public float get_time_of_day() => GregAPI.GetTimeOfDay();
public uint get_day() => GregAPI.GetDay();
public string get_current_scene() => GregAPI.GetCurrentScene();
public bool is_game_paused() => GregAPI.IsGamePaused();
public void set_game_paused(bool val) => GregAPI.SetGamePaused(val);
public float get_time_scale() => GregAPI.GetTimeScale();
public void set_time_scale(float val) => GregAPI.SetTimeScale(val);
public int trigger_save() => GregAPI.TriggerSave();
public object get_player_position()
{
var p = GregAPI.GetPlayerPosition();
return new { x = p.Item1, y = p.Item2, z = p.Item3, ry = p.Item4 };
}
public void show_notification(string text) => GregAPI.ShowNotification(text);
public void subscribe_event(uint id, PyObject callback)
{
GregAPI.Subscribe((GregEventId)id, data => {
using (Py.GIL()) { callback.Invoke(data.ToPython()); }
});
}
public void fire_event(uint id, ulong data) => GregAPI.FireEvent((GregEventId)id, data);
// Hook API (New)
public void on(string hookName, PyObject callback)
{
GregAPI.Hooks.On(hookName, payload => {
using (Py.GIL())
{
var dict = new PyDict();
dict.SetItem("hook_name", payload.HookName.ToPython());
dict.SetItem("trigger", payload.Trigger.ToPython());
var dataDict = new PyDict();
foreach (var kvp in payload.Data) dataDict.SetItem(kvp.Key, kvp.Value.ToPython());
dict.SetItem("data", dataDict);
callback.Invoke(dict);
}
});
}
public void fire(string hookName, PyObject dataDict)
{
var payload = new gregCore.Sdk.Models.GregPayload(hookName, "PythonMod");
using (Py.GIL())
{
var dict = new PyDict(dataDict);
foreach (var key in dict.Keys())
{
payload.Data[key.ToString()] = dict.GetItem(key).AsManagedObject(typeof(object));
}
}
GregAPI.Hooks.Fire(hookName, payload);
}
}
+263
View File
@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MelonLoader;
using gregCore.API;
using gregCore.Core.Models;
namespace gregCore.Bridge.RustFFI;
public static class RustFFIBridge
{
private static readonly List<IntPtr> _loadedLibraries = new();
private static readonly List<RustPlugin> _plugins = new();
private static GregCoreAPI _apiTable;
private static IntPtr _apiTablePtr;
// Delegates for the function table
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void LogDelegate(IntPtr msg);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate double GetDoubleDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetDoubleDelegate(double val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate uint GetUintDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int DispatchDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate float GetFloatDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetFloatDelegate(float val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate IntPtr GetStringDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetStringDelegate(IntPtr str);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int GetIntDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SetIntDelegate(int val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void GetPlayerPosDelegate(out float x, out float y, out float z, out float ry);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void EventActionDelegate(uint eventId, ulong data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void SubscribeDelegate(uint eventId, IntPtr callbackPtr);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void OnHookDelegate(IntPtr hookName, IntPtr callbackPtr);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void FireHookDelegate(IntPtr hookName, IntPtr jsonData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void HookActionDelegate(IntPtr hookName, IntPtr trigger, IntPtr jsonData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void ConfigSetBoolDelegate(IntPtr modId, IntPtr key, uint val);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate uint ConfigGetBoolDelegate(IntPtr modId, IntPtr key, uint def);
// Keeping delegates alive
private static readonly List<Delegate> _delegateCache = new();
public static void Initialize()
{
GregAPI.LogInfo("RustFFIBridge initializing...");
SetupApiTable();
LoadPlugins();
}
private static void SetupApiTable()
{
_apiTable = new GregCoreAPI { api_version = 1 };
// Logging
_apiTable.log_info = AddDelegate<LogDelegate>(ptr => GregAPI.LogInfo(Marshal.PtrToStringAnsi(ptr) ?? ""));
_apiTable.log_warning = AddDelegate<LogDelegate>(ptr => GregAPI.LogWarning(Marshal.PtrToStringAnsi(ptr) ?? ""));
_apiTable.log_error = AddDelegate<LogDelegate>(ptr => GregAPI.LogError(Marshal.PtrToStringAnsi(ptr) ?? ""));
// Economy
_apiTable.get_player_money = AddDelegate<GetDoubleDelegate>(() => GregAPI.GetPlayerMoney());
_apiTable.set_player_money = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetPlayerMoney(val));
_apiTable.get_player_xp = AddDelegate<GetDoubleDelegate>(() => GregAPI.GetPlayerXp());
_apiTable.set_player_xp = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetPlayerXp(val));
_apiTable.get_player_reputation = AddDelegate<GetDoubleDelegate>(() => GregAPI.GetPlayerReputation());
_apiTable.set_player_reputation = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetPlayerReputation(val));
// World
_apiTable.get_server_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetServerCount());
_apiTable.get_rack_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetRackCount());
_apiTable.get_switch_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetSwitchCount());
_apiTable.get_broken_server_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetBrokenServerCount());
_apiTable.get_broken_switch_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetBrokenSwitchCount());
// Technicians
_apiTable.get_free_technician_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetFreeTechnicianCount());
_apiTable.get_total_technician_count = AddDelegate<GetUintDelegate>(() => GregAPI.GetTotalTechnicianCount());
_apiTable.dispatch_repair_server = AddDelegate<DispatchDelegate>(() => GregAPI.DispatchRepairServer());
_apiTable.dispatch_repair_switch = AddDelegate<DispatchDelegate>(() => GregAPI.DispatchRepairSwitch());
// Time
_apiTable.get_time_of_day = AddDelegate<GetFloatDelegate>(() => GregAPI.GetTimeOfDay());
_apiTable.get_day = AddDelegate<GetUintDelegate>(() => GregAPI.GetDay());
_apiTable.get_seconds_in_full_day = AddDelegate<GetFloatDelegate>(() => GregAPI.GetSecondsInFullDay());
_apiTable.set_seconds_in_full_day = AddDelegate<SetFloatDelegate>(val => GregAPI.SetSecondsInFullDay(val));
// Game
_apiTable.get_current_scene = AddDelegate<GetStringDelegate>(() => Marshal.StringToHGlobalAnsi(GregAPI.GetCurrentScene()));
_apiTable.is_game_paused = AddDelegate<GetUintDelegate>(() => GregAPI.IsGamePaused() ? 1u : 0u);
_apiTable.set_game_paused = AddDelegate<SetDoubleDelegate>(val => GregAPI.SetGamePaused(val > 0));
_apiTable.get_time_scale = AddDelegate<GetFloatDelegate>(() => GregAPI.GetTimeScale());
_apiTable.set_time_scale = AddDelegate<SetFloatDelegate>(val => GregAPI.SetTimeScale(val));
_apiTable.trigger_save = AddDelegate<DispatchDelegate>(() => GregAPI.TriggerSave());
_apiTable.get_difficulty = AddDelegate<GetIntDelegate>(() => GregAPI.GetDifficulty());
// Player
_apiTable.get_player_position = AddDelegate<GetPlayerPosDelegate>((out float x, out float y, out float z, out float ry) => {
var pos = GregAPI.GetPlayerPosition();
x = pos.Item1; y = pos.Item2; z = pos.Item3; ry = pos.Item4;
});
// UI
_apiTable.show_notification = AddDelegate<LogDelegate>(ptr => GregAPI.ShowNotification(Marshal.PtrToStringAnsi(ptr) ?? ""));
// Events
_apiTable.subscribe_event = AddDelegate<SubscribeDelegate>((eventId, cbPtr) => {
var callback = Marshal.GetDelegateForFunctionPointer<EventActionDelegate>(cbPtr);
GregAPI.Subscribe((GregEventId)eventId, data => callback(eventId, data));
});
_apiTable.fire_event = AddDelegate<EventActionDelegate>((id, data) => GregAPI.FireEvent((GregEventId)id, data));
// Hook API (New)
_apiTable.on_hook = AddDelegate<OnHookDelegate>((hookPtr, cbPtr) => {
string hookName = Marshal.PtrToStringAnsi(hookPtr) ?? "";
var callback = Marshal.GetDelegateForFunctionPointer<HookActionDelegate>(cbPtr);
GregAPI.Hooks.On(hookName, payload => {
string json = Newtonsoft.Json.JsonConvert.SerializeObject(payload.Data);
IntPtr hPtr = Marshal.StringToHGlobalAnsi(payload.HookName);
IntPtr tPtr = Marshal.StringToHGlobalAnsi(payload.Trigger);
IntPtr jPtr = Marshal.StringToHGlobalAnsi(json);
callback(hPtr, tPtr, jPtr);
Marshal.FreeHGlobal(hPtr);
Marshal.FreeHGlobal(tPtr);
Marshal.FreeHGlobal(jPtr);
});
});
_apiTable.fire_hook = AddDelegate<FireHookDelegate>((hookPtr, jsonPtr) => {
string hookName = Marshal.PtrToStringAnsi(hookPtr) ?? "";
string json = Marshal.PtrToStringAnsi(jsonPtr) ?? "{}";
var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(json) ?? new();
var payload = new gregCore.Sdk.Models.GregPayload(hookName, "RustMod") { Data = data };
GregAPI.Hooks.Fire(hookName, payload);
});
// Config
_apiTable.config_set_bool = AddDelegate<ConfigSetBoolDelegate>((modId, key, val) =>
GregAPI.ConfigSetBool(Marshal.PtrToStringAnsi(modId) ?? "unknown", Marshal.PtrToStringAnsi(key) ?? "unknown", val > 0));
_apiTable.config_get_bool = AddDelegate<ConfigGetBoolDelegate>((modId, key, def) =>
GregAPI.ConfigGetBool(Marshal.PtrToStringAnsi(modId) ?? "unknown", Marshal.PtrToStringAnsi(key) ?? "unknown", def > 0) ? 1u : 0u);
// Alloc and store pointer
_apiTablePtr = Marshal.AllocHGlobal(Marshal.SizeOf<GregCoreAPI>());
Marshal.StructureToPtr(_apiTable, _apiTablePtr, false);
}
private static IntPtr AddDelegate<T>(T del) where T : Delegate
{
_delegateCache.Add(del);
return Marshal.GetFunctionPointerForDelegate(del);
}
private static void LoadPlugins()
{
string gameRoot = global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
string rustDir = Path.Combine(gameRoot, "Plugins", "Rust");
if (!Directory.Exists(rustDir)) Directory.CreateDirectory(rustDir);
foreach (string file in Directory.GetFiles(rustDir, "*.dll"))
{
try
{
IntPtr lib = System.Runtime.InteropServices.NativeLibrary.Load(file);
if (lib == IntPtr.Zero) continue;
_loadedLibraries.Add(lib);
IntPtr infoFunc = System.Runtime.InteropServices.NativeLibrary.GetExport(lib, "greg_mod_info");
IntPtr initFunc = System.Runtime.InteropServices.NativeLibrary.GetExport(lib, "greg_mod_init");
if (infoFunc == IntPtr.Zero || initFunc == IntPtr.Zero)
{
GregAPI.LogWarning($"Plugin {Path.GetFileName(file)} does not export core functions.");
continue;
}
var getInfo = Marshal.GetDelegateForFunctionPointer<Func<GregModInfo>>(infoFunc);
var info = getInfo();
var init = Marshal.GetDelegateForFunctionPointer<Func<IntPtr, bool>>(initFunc);
if (init(_apiTablePtr))
{
var plugin = new RustPlugin
{
Id = Marshal.PtrToStringAnsi(info.id) ?? "Unknown",
Handle = lib,
Update = GetOptionalExport<Action<float>>(lib, "greg_mod_update"),
OnEvent = GetOptionalExport<Action<uint, ulong>>(lib, "greg_mod_event"),
OnSceneLoaded = GetOptionalExport<Action<IntPtr>>(lib, "greg_mod_scene_loaded"),
Shutdown = GetOptionalExport<Action>(lib, "greg_mod_shutdown")
};
_plugins.Add(plugin);
GregAPI.LogInfo($"Rust Plugin loaded: {plugin.Id}");
}
}
catch (Exception ex)
{
GregAPI.LogError($"Error loading {Path.GetFileName(file)}: {ex.Message}");
}
}
}
private static T? GetOptionalExport<T>(IntPtr lib, string name) where T : Delegate
{
if (System.Runtime.InteropServices.NativeLibrary.TryGetExport(lib, name, out IntPtr ptr))
return Marshal.GetDelegateForFunctionPointer<T>(ptr);
return null;
}
public static void OnUpdate(float dt)
{
foreach (var p in _plugins) p.Update?.Invoke(dt);
}
public static void OnSceneLoaded(string name)
{
IntPtr namePtr = Marshal.StringToHGlobalAnsi(name);
foreach (var p in _plugins) p.OnSceneLoaded?.Invoke(namePtr);
Marshal.FreeHGlobal(namePtr);
}
public static void Shutdown()
{
foreach (var p in _plugins) p.Shutdown?.Invoke();
foreach (var lib in _loadedLibraries) System.Runtime.InteropServices.NativeLibrary.Free(lib);
if (_apiTablePtr != IntPtr.Zero) Marshal.FreeHGlobal(_apiTablePtr);
}
[StructLayout(LayoutKind.Sequential)]
struct GregModInfo
{
public IntPtr id;
public IntPtr name;
public IntPtr version;
public IntPtr author;
public IntPtr description;
public uint api_version;
}
[StructLayout(LayoutKind.Sequential)]
struct GregCoreAPI
{
public uint api_version;
public IntPtr log_info, log_warning, log_error;
public IntPtr get_player_money, set_player_money, get_player_xp, set_player_xp, get_player_reputation, set_player_reputation;
public IntPtr get_server_count, get_rack_count, get_switch_count, get_broken_server_count, get_broken_switch_count;
public IntPtr get_free_technician_count, get_total_technician_count, dispatch_repair_server, dispatch_repair_switch;
public IntPtr get_time_of_day, get_day, get_seconds_in_full_day, set_seconds_in_full_day;
public IntPtr get_current_scene, is_game_paused, set_game_paused, get_time_scale, set_time_scale, trigger_save, get_difficulty;
public IntPtr get_player_position, show_notification;
public IntPtr subscribe_event, unsubscribe_event, fire_event;
public IntPtr on_hook, fire_hook;
public IntPtr config_set_bool, config_get_bool, config_set_int, config_get_int, config_set_float, config_get_float, config_set_string, config_get_string;
}
class RustPlugin
{
public string Id = "";
public IntPtr Handle;
public Action<float>? Update;
public Action<uint, ulong>? OnEvent;
public Action<IntPtr>? OnSceneLoaded;
public Action? Shutdown;
}
}
+1
View File
@@ -10,6 +10,7 @@ public interface IGregLogger
{
void Debug(string message);
void Info(string message);
void Success(string message);
void Warning(string message);
void Error(string message, Exception? ex = null);
IGregLogger ForContext(string context);
+73
View File
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using gregCore.Core.Abstractions;
using gregCore.Core.Models;
namespace gregCore.Core.Events;
/// <summary>
/// Synchronous Hook Bus for Game Interception.
/// Manages all 1771 native hooks and ensures safe dispatching.
/// </summary>
public sealed class GregHookBus
{
private readonly IGregLogger _logger;
private readonly Dictionary<string, List<Action<EventPayload>>> _hooks = new();
private readonly HashSet<string> _disabledHooks = new();
public GregHookBus(IGregLogger logger)
{
_logger = logger.ForContext("HookBus");
}
/// <summary>
/// Subscribes to a specific hook.
/// </summary>
public void On(string hookName, Action<EventPayload> handler)
{
if (!_hooks.ContainsKey(hookName))
_hooks[hookName] = new List<Action<EventPayload>>();
_hooks[hookName].Add(handler);
_logger.Info($"Listener registriert für Hook: {hookName}");
}
/// <summary>
/// Dispatches a hook synchronously.
/// </summary>
public void Dispatch(string hookName, EventPayload payload)
{
if (_disabledHooks.Contains(hookName))
return;
if (_hooks.TryGetValue(hookName, out var handlers))
{
foreach (var handler in handlers)
{
try
{
handler(payload);
}
catch (Exception ex)
{
_logger.Error($"Fehler in Hook-Handler für {hookName}: {ex.Message}", ex);
}
}
}
}
/// <summary>
/// Disables a hook temporarily (e.g., due to performance or stability issues).
/// </summary>
public void SetHookStatus(string hookName, bool enabled)
{
if (enabled)
_disabledHooks.Remove(hookName);
else
_disabledHooks.Add(hookName);
_logger.Warning($"Hook-Status geändert: {hookName} -> {(enabled ? "Enabled" : "Disabled")}");
}
public bool IsHookEnabled(string hookName) => !_disabledHooks.Contains(hookName);
}
+79
View File
@@ -0,0 +1,79 @@
using System;
using MelonLoader;
using gregCore.Core.Abstractions;
using gregCore.GameLayer.Bootstrap;
using gregCore.Infrastructure.Logging;
[assembly: MelonInfo(typeof(gregCore.Core.GregCoreMod), "gregCore", "1.1.0", "TeamGreg")]
[assembly: MelonGame("", "Data Center")]
namespace gregCore.Core;
/// <summary>
/// Der zentrale Einstiegspunkt des Frameworks (Prod-Layer: Core).
/// Verantwortlich für Lifecycle, Service-Orchestrierung und globale Initialisierung.
/// </summary>
public sealed class GregCoreMod : MelonMod
{
private GregServiceContainer? _container;
private IGregLogger? _logger;
public override void OnInitializeMelon()
{
// 1. Bootstrapping
_container = GregBootstrapper.Build(LoggerInstance);
_logger = _container.GetRequired<IGregLogger>();
_logger.Info("gregCore Core-Modus wird initialisiert...");
// 2. Global API Init
gregCore.API.GregAPI.Initialize();
// 3. Plugin Loading
_container.GetRequired<IGregPluginRegistry>().LoadAll();
_logger.Success("gregCore v1.1.0 (Production-Grade) erfolgreich geladen.");
}
public override void OnUpdate()
{
float dt = UnityEngine.Time.deltaTime;
// Update core services
_container?.Get<Infrastructure.Performance.GregPerformanceGovernor>()?.OnUpdate();
_container?.Get<Core.Events.GregEventBus>()?.FlushDeferredEvents();
_container?.Get<Infrastructure.Settings.Services.GregInputBindingService>()?.OnUpdate();
// Update language bridges
gregCore.Bridge.RustFFI.RustFFIBridge.OnUpdate(dt);
gregCore.Bridge.LuaFFI.LuaFFIBridge.OnUpdate(dt);
gregCore.Bridge.GoFFI.GoFFIBridge.OnUpdate(dt);
gregCore.Bridge.PythonFFI.PythonFFIBridge.OnUpdate(dt);
}
public override void OnGUI()
{
// Debug Console & HUDs
Infrastructure.UI.GregDevConsole.Instance.OnGUI();
_container?.Get<Infrastructure.Settings.Services.GregHudService>()?.OnGUI();
_container?.Get<Infrastructure.Settings.Services.GregNotificationService>()?.OnGUI();
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
_logger?.Info($"Szene geladen: {sceneName} (Index: {buildIndex})");
// Notify Event Bus
_container?.GetRequired<IGregEventBus>()
.Publish("greg.lifecycle.SceneLoaded",
Core.Events.EventPayloadBuilder.ForScene(buildIndex, sceneName));
gregCore.API.GregAPI.FireEvent(gregCore.API.GregEventId.GameLoaded);
}
public override void OnApplicationQuit()
{
_logger?.Info("gregCore wird beendet...");
_container?.Dispose();
}
}
@@ -0,0 +1,37 @@
using System;
using gregCore.Core.Abstractions;
namespace gregCore.Core.Services;
/// <summary>
/// Validiert API-Eingaben und Framework-Zustände (Core Layer).
/// </summary>
public sealed class GregValidationService
{
private readonly IGregLogger _logger;
public GregValidationService(IGregLogger logger)
{
_logger = logger.ForContext("ValidationService");
}
public bool ValidateModId(string modId)
{
if (string.IsNullOrWhiteSpace(modId))
{
_logger.Error("Validierungsfehler: ModId darf nicht leer sein.");
return false;
}
return true;
}
public bool ValidateHookName(string hookName)
{
if (string.IsNullOrWhiteSpace(hookName) || !hookName.StartsWith("greg."))
{
_logger.Error($"Validierungsfehler: Ungültiger Hook-Name '{hookName}'. Muss mit 'greg.' beginnen.");
return false;
}
return true;
}
}
+68 -8
View File
@@ -1,4 +1,4 @@
/// <file-summary>
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Erstellt und konfiguriert den GregServiceContainer.
/// Maintainer: Einzige Stelle wo Implementierungen an Interfaces gebunden werden. Validiert den Startup.
@@ -12,6 +12,8 @@ using gregCore.Infrastructure.Scripting.Lua;
using gregCore.Infrastructure.Scripting.Js;
using gregCore.GameLayer.Hooks;
using gregCore.Core.Abstractions;
using gregCore.Infrastructure.Settings;
using gregCore.Infrastructure.Settings.Services;
namespace gregCore.GameLayer.Bootstrap;
@@ -20,20 +22,79 @@ internal static class GregBootstrapper
public static GregServiceContainer Build(global::MelonLoader.MelonLogger.Instance melonLogger)
{
var container = new GregServiceContainer();
var logger = new MelonLoggerAdapter(melonLogger);
// Initialize static Logger and CLI Config
GregLogger.Configure(new ConsoleConfig());
var logger = new ConsoleLogger(melonLogger);
container.Register<IGregLogger>(logger);
logger.Info("gregCore v1.0.0 Bootstrap gestartet");
GregLogger.Box(new[] {
"gregCore v1.0.0",
"MelonLoader Framework initialized",
"PRO-Edition Active"
});
var bus = new GregEventBus(logger);
var hookBus = new GregHookBus(logger);
var catalog = new Sdk.Metadata.GregHookCatalog();
var catalogService = new Sdk.Services.GregHookCatalogService(logger, catalog);
catalogService.Initialize();
var validationService = new Core.Services.GregValidationService(logger);
container.Register<IGregEventBus>(bus);
container.Register<GregHookBus>(hookBus);
container.Register<Sdk.Metadata.GregHookCatalog>(catalog);
container.Register<Sdk.Services.GregHookCatalogService>(catalogService);
container.Register<Core.Services.GregValidationService>(validationService);
container.Register<IGregConfigService>(new GregConfigService(logger));
container.Register<IGregPersistenceService>(new GregPersistenceService(logger));
container.Register<IGregHookRegistry>(new GregHookRegistry(logger));
// --- Settings Subsystem ---
var keybindRegistry = new GregKeybindRegistry(logger);
var modSettingsService = new GregModSettingsService(logger);
var settingsPersistence = new GregSettingsPersistenceService(logger, keybindRegistry, modSettingsService, bus);
modSettingsService.SetPersistence(settingsPersistence); // Link back for lazy injection
settingsPersistence.Load();
var settingsConflict = new GregSettingsConflictService(logger, keybindRegistry, modSettingsService);
var inputBinding = new GregInputBindingService(logger, keybindRegistry);
inputBinding.SetPersistence(settingsPersistence);
var pluginRegistry = new GregPluginRegistry(new AssemblyScanner(), logger, bus);
var uiBridge = new GregSettingsUiBridge(logger, modSettingsService, keybindRegistry, inputBinding, pluginRegistry);
var hudService = new GregHudService(logger, keybindRegistry);
var notificationService = new GregNotificationService(logger);
var sdkApi = new Sdk.GregAPI(logger, hookBus, modSettingsService, keybindRegistry, pluginRegistry, notificationService, validationService);
container.Register<GregKeybindRegistry>(keybindRegistry);
container.Register<GregModSettingsService>(modSettingsService);
container.Register<GregSettingsPersistenceService>(settingsPersistence);
container.Register<GregSettingsConflictService>(settingsConflict);
container.Register<GregInputBindingService>(inputBinding);
container.Register<IGregPluginRegistry>(pluginRegistry);
container.Register<GregSettingsUiBridge>(uiBridge);
container.Register<GregHudService>(hudService);
container.Register<GregNotificationService>(notificationService);
container.Register<Sdk.IGregAPI>(sdkApi);
// --- Harmony Initialization ---
Hooks.GregNativeEventHooks.Install(logger, hookBus);
// Link globally for legacy/mod compatibility
gregCore.API.GregAPI._keybindReg = keybindRegistry;
gregCore.API.GregAPI._modSettingsService = modSettingsService;
// --------------------------
var apiContext = new global::gregCore.PublicApi.GregApiContext {
Logger = logger,
EventBus = bus,
HookBus = hookBus,
Config = container.GetRequired<IGregConfigService>(),
Persist = container.GetRequired<IGregPersistenceService>()
};
@@ -48,19 +109,18 @@ internal static class GregBootstrapper
container.Register<IGregLanguageBridge>("js", new JsBridge(logger, bus));
container.Register<IAssemblyScanner>(new AssemblyScanner());
container.Register<IGregPluginRegistry>(new GregPluginRegistry(container.GetRequired<IAssemblyScanner>(), logger, bus));
HookIntegration.Install(bus, logger);
global::gregCore.PublicApi.greg._context = apiContext;
global::gregCore.PublicApi.greg._governor = governor;
ValidateStartup(container);
logger.Info("Alle Services registriert");
return container;
}
private static void ValidateStartup(GregServiceContainer container)
{
container.GetRequired<IGregLogger>();
-55
View File
@@ -1,55 +0,0 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: MelonMod Entry Point.
/// Maintainer: So klein wie möglich halten. Max 50 Zeilen. Kein Business-Logic.
/// </file-summary>
using MelonLoader;
[assembly: MelonInfo(typeof(gregCore.GameLayer.Bootstrap.GregCoreLoader), "gregCore", "1.0.0", "TeamGreg")]
[assembly: MelonGame("", "Data Center")]
namespace gregCore.GameLayer.Bootstrap;
public sealed class GregCoreLoader : MelonMod
{
private GregServiceContainer? _container;
private IGregLogger? _logger;
public override void OnInitializeMelon()
{
_container = GregBootstrapper.Build(LoggerInstance);
_logger = _container.GetRequired<IGregLogger>();
_container.GetRequired<IGregPluginRegistry>().LoadAll();
}
public override void OnUpdate()
{
if (global::UnityEngine.InputSystem.Keyboard.current != null && global::UnityEngine.InputSystem.Keyboard.current.f8Key.wasPressedThisFrame)
{
global::gregCore.Infrastructure.UI.GregDevConsole.Instance.Toggle();
}
_container?.Get<gregCore.Infrastructure.Performance.GregPerformanceGovernor>()?.OnUpdate();
_container?.Get<GregEventBus>()?.FlushDeferredEvents();
}
public override void OnGUI()
{
global::gregCore.Infrastructure.UI.GregDevConsole.Instance.OnGUI();
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName) =>
_container?.GetRequired<IGregEventBus>()
.Publish(HookName.Create("lifecycle", "SceneLoaded").Full,
EventPayloadBuilder.ForScene(buildIndex, sceneName));
public override void OnApplicationQuit()
{
if (_container == null || _logger == null) return;
_logger.Info("Führe Graceful Shutdown durch...");
_container.Dispose();
_logger.Info("Shutdown abgeschlossen.");
}
}
@@ -9,6 +9,14 @@ namespace gregCore.GameLayer.Bootstrap;
public sealed class GregServiceContainer : IDisposable
{
private readonly Dictionary<string, object> _services = new();
private static GregServiceContainer? _instance;
public static GregServiceContainer? Instance => _instance;
public GregServiceContainer()
{
_instance = this;
}
public void Register<T>(T instance) where T : notnull
=> _services[typeof(T).FullName!] = instance;
@@ -16,16 +24,17 @@ public sealed class GregServiceContainer : IDisposable
public void Register<T>(string key, T instance) where T : notnull
=> _services[$"{typeof(T).FullName}:{key}"] = instance;
public static T? Get<T>() where T : class
=> Instance?._services.TryGetValue(typeof(T).FullName!, out var s) == true ? (T)s : null;
public T GetRequired<T>() where T : notnull
=> _services.TryGetValue(typeof(T).FullName!, out var s)
? (T)s
: throw new GregInitException($"Service {typeof(T).Name} nicht registriert!");
public T? Get<T>() where T : class
=> _services.TryGetValue(typeof(T).FullName!, out var s) ? (T)s : null;
public void Dispose()
{
if (_instance == this) _instance = null;
foreach (var s in _services.Values.OfType<IDisposable>())
s.Dispose();
_services.Clear();
+58
View File
@@ -0,0 +1,58 @@
using System;
using System.Reflection;
using gregCore.Core.Abstractions;
namespace gregCore.GameLayer.Hooks;
/// <summary>
/// Prüft die Existenz von Methoden vor dem Harmony-Patching (Harmony Layer).
/// Schützt vor Game-Version-Drift und IL2CPP-Inkompatibilitäten.
/// </summary>
public sealed class GregCompatBridge
{
private readonly IGregLogger _logger;
public GregCompatBridge(IGregLogger logger)
{
_logger = logger.ForContext("CompatBridge");
}
/// <summary>
/// Prüft, ob eine Methode in der Assembly existiert.
/// </summary>
public bool VerifyMethod(string ns, string className, string methodName)
{
try
{
var fullName = $"{ns}.{className}";
var type = Type.GetType(fullName);
if (type == null)
{
// Versuche über Assembly-CSharp zu finden
var assembly = Assembly.Load("Assembly-CSharp");
type = assembly?.GetType(fullName);
}
if (type == null)
{
_logger.Warning($"Klasse nicht gefunden: {fullName}");
return false;
}
var method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (method == null)
{
_logger.Warning($"Methode nicht gefunden: {fullName}.{methodName}");
return false;
}
_logger.Success($"Methode verifiziert: {fullName}.{methodName}");
return true;
}
catch (Exception ex)
{
_logger.Error($"Fehler bei der Verifizierung von {ns}.{className}.{methodName}: {ex.Message}");
return false;
}
}
}
@@ -0,0 +1,53 @@
using System;
using HarmonyLib;
using gregCore.Core.Abstractions;
using gregCore.Core.Events;
namespace gregCore.GameLayer.Hooks;
/// <summary>
/// Die Harmony-Brücke zwischen dem Spiel und gregCore (Harmony Layer).
/// Enthält die Harmony-Patches für alle 1771 Hooks.
/// </summary>
[HarmonyPatch]
public sealed class GregNativeEventHooks : SafePatch
{
private static bool _isInstalled = false;
public static void Install(IGregLogger logger, GregHookBus hookBus)
{
if (_isInstalled) return;
Setup(logger, hookBus);
_isInstalled = true;
_logger?.Success("GregNativeEventHooks Harmony Bridge installiert.");
}
// --- Domäne: Economy ---
[HarmonyPatch(typeof(global::Il2Cpp.Player), nameof(global::Il2Cpp.Player.UpdateCoin))]
[HarmonyPostfix]
public static void Postfix_PlayerCoinChanged(global::Il2Cpp.Player __instance, float _amount)
{
TriggerHook("greg.PLAYER.CoinChanged", "Amount", _amount, "Total", __instance.coin);
}
// --- Domäne: Persistence ---
[HarmonyPatch(typeof(global::Il2Cpp.SaveSystem), nameof(global::Il2Cpp.SaveSystem.SaveGame))]
[HarmonyPostfix]
public static void Postfix_GameSaved()
{
TriggerHook("greg.SYSTEM.GameSaved", "Timestamp", DateTime.Now.ToString());
}
// --- Domäne: UI ---
[HarmonyPatch(typeof(global::Il2Cpp.PauseMenu), nameof(global::Il2Cpp.PauseMenu.OnEnable))]
[HarmonyPostfix]
public static void Postfix_PauseMenuOpened()
{
TriggerHook("greg.UI.PauseMenu.Opened", "InstanceId", 1);
}
// --- Generisches Hooking für die restlichen 1771 Hooks (Platzhalter) ---
// In einer vollwertigen Produktion würde hier ein Generator-Tool (z.B. Source Generator)
// alle 1771 Harmony-Methoden basierend auf game_hooks.json generieren.
}
+20 -1
View File
@@ -29,7 +29,26 @@ internal static class HookIntegration
internal static void Emit(HookName hook, EventPayload payload)
{
try { _bus.Publish(hook.Full, payload); }
try
{
_bus.Publish(hook.Full, payload);
// Legacy Support Trigger
if (hook.Domain == "economy" && hook.Event == "PlayerCoinUpdated")
{
if (payload.Data.TryGetValue("NewValue", out var val) && val is float f)
global::greg.Sdk.gregNativeEventHooks.ByEventId.MoneyChanged(f);
}
else if (hook.Domain == "economy" && hook.Event == "PlayerReputationUpdated")
{
if (payload.Data.TryGetValue("NewValue", out var val) && val is float f)
global::greg.Sdk.gregNativeEventHooks.ByEventId.ReputationChanged(f);
}
else if (hook.Domain == "system" && hook.Event == "GameLoaded")
global::greg.Sdk.gregNativeEventHooks.ByEventId.GameLoaded();
else if (hook.Domain == "system" && hook.Event == "GameSaved")
global::greg.Sdk.gregNativeEventHooks.ByEventId.GameSaved();
}
catch (Exception ex)
{
_logger.Error($"Emit fehlgeschlagen für {hook.Full}", ex);
+45
View File
@@ -0,0 +1,45 @@
using System;
using HarmonyLib;
using gregCore.Core.Abstractions;
namespace gregCore.GameLayer.Hooks;
/// <summary>
/// Basisklasse für alle Framework-Patches (Harmony Layer).
/// Stellt sicher, dass bei Fehlern im Patch das Spiel nicht abstürzt (Prefix returns true).
/// </summary>
public abstract class SafePatch
{
protected static IGregLogger? _logger;
protected static Core.Events.GregHookBus? _hookBus;
public static void Setup(IGregLogger logger, Core.Events.GregHookBus hookBus)
{
_logger = logger.ForContext("HarmonyPatch");
_hookBus = hookBus;
}
/// <summary>
/// Sichere Methode zur Auslösung eines Hooks.
/// </summary>
protected static void TriggerHook(string hookName, params object[] data)
{
try
{
if (_hookBus == null) return;
var payload = new Sdk.Models.GregPayload(hookName, "NativePatch");
for (int i = 0; i < data.Length; i += 2)
{
if (i + 1 < data.Length)
payload.Data[data[i].ToString()!] = data[i + 1];
}
_hookBus.Dispatch(hookName, payload);
}
catch (Exception ex)
{
_logger?.Error($"Fehler beim Auslösen von Hook {hookName}: {ex.Message}");
}
}
}
+40 -7
View File
@@ -1,13 +1,46 @@
/// <file-summary>
/// Schicht: GameLayer
/// Zweck: Extrahiert Server-Status-Änderungen.
/// Maintainer: Kein Business-Logic, reines Dispatch.
/// </file-summary>
using HarmonyLib;
using gregCore.GameLayer.Hooks;
using gregCore.Core.Models;
using gregCore.API;
namespace gregCore.GameLayer.Patches.Hardware;
// [GREG_SYNC_INSERT_PATCHES]
[HarmonyPatch]
internal static class ServerPatch
{
[HarmonyPatch(typeof(global::Il2Cpp.Server), nameof(global::Il2Cpp.Server.ItIsBroken))]
[HarmonyPostfix]
internal static void OnServerBroken(global::Il2Cpp.Server __instance)
{
try
{
var payload = new EventPayload
{
HookName = "hardware.ServerStatusChanged",
Data = new System.Collections.Generic.Dictionary<string, object> { { "status", "broken" } }
};
HookIntegration.Emit(HookName.Create("hardware", "ServerStatusChanged"), payload);
GregAPI.FireEvent(GregEventId.ServerBroken);
}
catch (System.Exception ex)
{
HookIntegration.LogPatchError(nameof(OnServerBroken), ex);
}
}
// NOTE: Server.DeviceRepaired does not exist in the game assembly.
// Patching RepairDevice instead which IS confirmed via reference dump.
[HarmonyPatch(typeof(global::Il2Cpp.Server), nameof(global::Il2Cpp.Server.RepairDevice))]
[HarmonyPostfix]
internal static void OnServerRepaired(global::Il2Cpp.Server __instance)
{
try
{
GregAPI.FireEvent(GregEventId.ServerRepaired);
}
catch (System.Exception ex)
{
HookIntegration.LogPatchError(nameof(OnServerRepaired), ex);
}
}
}
@@ -0,0 +1,27 @@
using HarmonyLib;
using UnityEngine;
namespace gregCore.GameLayer.Patches.Input;
/// <summary>
/// Patches für Input-Handling.
/// Vormals genutzte Console-Blocking-Logik wurde entfernt,
/// da der Fokus nun auf dem MelonLoader-Terminal liegt.
/// </summary>
[HarmonyPatch]
internal static class KeybindPatches
{
// Must specify ArgumentTypes to disambiguate GetKeyDown(KeyCode) from GetKeyDown(string)
[HarmonyPatch(typeof(UnityEngine.Input), nameof(UnityEngine.Input.GetKeyDown),
argumentTypes: new[] { typeof(UnityEngine.KeyCode) })]
[HarmonyPrefix]
private static bool BlockPWhenConsoleOpen(ref bool __result, KeyCode key)
{
if (key == KeyCode.P && global::gregCore.Infrastructure.UI.GregDevConsole.Instance.IsOpen)
{
__result = false;
return false;
}
return true;
}
}
@@ -0,0 +1,109 @@
using gregCore.Infrastructure.Settings.Services;
using gregCore.Core.Abstractions;
using gregCore.GameLayer.Bootstrap;
namespace gregCore.GameLayer.Patches.UI;
[HarmonyPatch]
internal static class SettingsUiBridgePatch
{
private static bool _tabInjected = false;
private static GregSettingsUiBridge _uiBridge;
private static GregSettingsUiBridge GetBridge()
{
if (_uiBridge == null)
{
_uiBridge = GregServiceContainer.Get<GregSettingsUiBridge>();
}
return _uiBridge;
}
// We hook into PauseMenu_TabGroup.Start or PauseMenu.Awake. Let's patch PauseMenu.Awake for setup.
[HarmonyPatch(typeof(global::Il2Cpp.PauseMenu), nameof(global::Il2Cpp.PauseMenu.Awake))]
[HarmonyPostfix]
internal static void OnPauseMenuAwake(global::Il2Cpp.PauseMenu __instance)
{
try
{
if (_tabInjected) return; // Prevent multiple injections on scene reloads if it persists
// Find the tab group (we assume mainPauseMenuTabGroup or systemPauseMenuTabGroup is setup)
var tabGroup = __instance.systemPauseMenuTabGroup; // usually contains Gameplay, Graphics, Volume, Controls
if (tabGroup != null)
{
// We need to clone an existing tab button
if (tabGroup.tabButtons != null && tabGroup.tabButtons.Count > 0)
{
var sourceTab = tabGroup.tabButtons[0];
var newTabObj = UnityEngine.Object.Instantiate(sourceTab.gameObject, sourceTab.transform.parent);
newTabObj.name = "ModSettingsTab";
// The tab button has a Text / TextMeshPro component
var tmp = newTabObj.GetComponentInChildren<global::Il2CppTMPro.TextMeshProUGUI>();
if (tmp != null)
{
tmp.text = "Mods";
}
var newTabButton = newTabObj.GetComponent<global::Il2Cpp.PauseMenu_TabButton>();
// We need a corresponding panel object to swap to
var sourcePanel = tabGroup.objectsToSwap[0];
var newPanelObj = new GameObject("ModSettingsPanel");
newPanelObj.transform.SetParent(sourcePanel.transform.parent, false);
// Setup UI (we will build this via code or a separate prefab)
GetBridge()?.BuildModSettingsPanel(newPanelObj);
// Add to lists
tabGroup.tabButtons.Add(newTabButton);
tabGroup.objectsToSwap.Add(newPanelObj);
gregCore.Infrastructure.Logging.GregLogger.Success("UIBridge", "Injected 'Mods' Tab into Settings Menu");
_tabInjected = true;
}
}
}
catch (Exception ex)
{
gregCore.Infrastructure.Logging.GregLogger.Error("UIBridge", $"Failed to inject Mod Settings UI: {ex.Message}");
}
}
[HarmonyPatch(typeof(global::Il2Cpp.PauseMenu), nameof(global::Il2Cpp.PauseMenu.OnEnable))]
[HarmonyPostfix]
internal static void OnPauseMenuOpened()
{
try
{
// Publish Event
API.GregAPI.FireEvent(0); // We will define a real event ID later, or string based
// Reload and check conflicts
gregCore.PublicApi.greg._context?.EventBus.Publish("greg.SYSTEM.SettingsOpened", new Core.Models.EventPayload());
// Trigger Refresh
GetBridge()?.RefreshUi();
}
catch (Exception ex)
{
gregCore.Infrastructure.Logging.GregLogger.Error("UIBridge", $"Error on open: {ex.Message}");
}
}
[HarmonyPatch(typeof(global::Il2Cpp.PauseMenu), nameof(global::Il2Cpp.PauseMenu.OnDisable))]
[HarmonyPostfix]
internal static void OnPauseMenuClosed()
{
try
{
// Save state
gregCore.PublicApi.greg._context?.EventBus.Publish("greg.SYSTEM.SettingsClosed", new Core.Models.EventPayload());
}
catch (Exception ex)
{
gregCore.Infrastructure.Logging.GregLogger.Error("UIBridge", $"Error on close: {ex.Message}");
}
}
}
@@ -0,0 +1,20 @@
namespace gregCore.Infrastructure.Logging;
/// <summary>
/// Konfiguration für das gregCore Logging-System.
/// </summary>
public sealed class ConsoleConfig
{
public bool ShowTimestamps { get; set; } = true;
public bool UseBoxDrawing { get; set; } = true;
public LogLevel MinLogLevel { get; set; } = LogLevel.Debug;
}
public enum LogLevel
{
Debug,
Info,
Warning,
Error,
Status
}
@@ -0,0 +1,56 @@
using System;
using System.Text;
namespace gregCore.Infrastructure.Logging;
/// <summary>
/// Hilfsklasse zur Erzeugung der gregCore-spezifischen CLI-Strings.
/// </summary>
public static class ConsoleFormatters
{
public static string CreatePrefix(string level, string component, bool showTimestamp)
{
var sb = new StringBuilder();
if (showTimestamp)
{
sb.Append($"[{DateTime.Now:HH:mm:ss}]");
}
sb.Append("[gregCore]");
if (!string.IsNullOrEmpty(component))
{
sb.Append($"[{component}]");
}
sb.Append($"[{ConsoleTheme.GetLevelPrefix(level)}]");
return sb.ToString();
}
public static string FormatStatusLine(string statusContent)
{
return $"[gregCore][STATUS] {statusContent}";
}
public static string[] CreateBox(string[] lines)
{
int maxWidth = 0;
foreach (var line in lines) maxWidth = Math.Max(maxWidth, line.Length);
int boxWidth = maxWidth + 4;
var result = new string[lines.Length + 2];
result[0] = ConsoleTheme.BoxTopLeft + new string(ConsoleTheme.BoxHorizontal, boxWidth - 2) + ConsoleTheme.BoxTopRight;
for (int i = 0; i < lines.Length; i++)
{
result[i + 1] = ConsoleTheme.BoxVertical + " " + lines[i].PadRight(maxWidth) + " " + ConsoleTheme.BoxVertical;
}
result[result.Length - 1] = ConsoleTheme.BoxBottomLeft + new string(ConsoleTheme.BoxHorizontal, boxWidth - 2) + ConsoleTheme.BoxBottomRight;
return result;
}
}
@@ -0,0 +1,38 @@
using System;
using MelonLoader;
using gregCore.Core.Abstractions;
using gregCore.Infrastructure.UI;
namespace gregCore.Infrastructure.Logging;
/// <summary>
/// Haupt-Logger für gregCore. Formatiert Logs einheitlich für Terminal und In-Game Console.
/// </summary>
public sealed class ConsoleLogger : IGregLogger
{
private readonly MelonLogger.Instance _melonLogger;
private readonly string _context;
public ConsoleLogger(MelonLogger.Instance melonLogger, string context = "")
{
_melonLogger = melonLogger ?? throw new ArgumentNullException(nameof(melonLogger));
_context = context;
}
public void Info(string message) => GregLogger.Info(_context, message);
public void Warning(string message) => GregLogger.Warning(_context, message);
public void Error(string message, Exception? ex = null)
{
string fullMessage = ex != null ? $"{message}\n{ex}" : message;
GregLogger.Error(_context, fullMessage);
}
public void Debug(string message) => GregLogger.Debug(_context, message);
public void Success(string message) => GregLogger.Success(_context, message);
public void Bridge(string bridgeName, string message) => GregLogger.BridgeInfo(bridgeName, message);
public IGregLogger ForContext(string context)
{
return new ConsoleLogger(_melonLogger, string.IsNullOrEmpty(_context) ? context : $"{_context}::{context}");
}
}
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace gregCore.Infrastructure.Logging;
/// <summary>
/// Definiert das visuelle Design der gregCore CLI.
/// </summary>
public static class ConsoleTheme
{
// Terminal Colors (MelonLoader)
public static ConsoleColor ColorGregCore = ConsoleColor.Cyan;
public static ConsoleColor ColorComponent = ConsoleColor.Gray;
public static ConsoleColor ColorMessage = ConsoleColor.White;
// Box Drawing Characters
public const char BoxHorizontal = '═';
public const char BoxVertical = '║';
public const char BoxTopLeft = '╔';
public const char BoxTopRight = '╗';
public const char BoxBottomLeft = '╚';
public const char BoxBottomRight = '╝';
public static string GetLevelPrefix(string level) => level.ToLower() switch
{
"info" => "INFO",
"success" or "ok" => "OK",
"warn" or "warning" or "wrn" => "WRN",
"error" or "err" => "ERR",
"debug" or "dbg" => "DBG",
"status" => "STATUS",
_ => "INFO"
};
public static ConsoleColor GetLevelColor(string level) => level.ToLower() switch
{
"info" => ConsoleColor.Cyan,
"warn" or "warning" or "wrn" => ConsoleColor.Yellow,
"error" or "err" => ConsoleColor.Red,
"success" or "ok" => ConsoleColor.Green,
"debug" or "dbg" => ConsoleColor.DarkGray,
"status" => ConsoleColor.Magenta,
_ => ConsoleColor.Gray
};
}
+85
View File
@@ -0,0 +1,85 @@
using System;
using MelonLoader;
namespace gregCore.Infrastructure.Logging;
/// <summary>
/// Zentraler statischer Logger für gregCore.
/// Routet alle Ausgaben einheitlich formatiert an den MelonLogger.
/// </summary>
public static class GregLogger
{
private static ConsoleConfig _config = new();
private static bool _isInitialized = false;
public static void Configure(ConsoleConfig config)
{
_config = config;
_isInitialized = true;
}
public static void Info(string component, string message) => Log("info", component, message);
public static void Success(string component, string message) => Log("success", component, message);
public static void Warning(string component, string message) => Log("warn", component, message);
public static void Error(string component, string message) => Log("error", component, message);
public static void Debug(string component, string message) => Log("debug", component, message);
public static void Status(string message)
{
if (_config.MinLogLevel > LogLevel.Status) return;
// Status wird immer in Magenta ausgegeben
string formatted = ConsoleFormatters.FormatStatusLine(message);
MelonLogger.Msg(ConsoleColor.Magenta, formatted);
}
public static void BridgeInfo(string bridgeName, string message) => Info(bridgeName, message);
public static void BridgeError(string bridgeName, string message) => Error(bridgeName, message);
public static void Box(string[] lines)
{
string[] boxed = ConsoleFormatters.CreateBox(lines);
foreach (var line in boxed)
{
MelonLogger.Msg(ConsoleColor.Cyan, line);
}
}
private static void Log(string level, string component, string message)
{
// LogLevel check
if (!IsLevelEnabled(level)) return;
// Wir nutzen hier eine vereinfachte Version für die MelonLoader Terminal-Farben.
// Um echte mehrfarbige Zeilen zu haben, müsste man direkt auf System.Console zugreifen,
// was aber das MelonLoader-Logging (Log-Files) umgehen würde.
// Daher nutzen wir die Level-Farbe für die gesamte Zeile, wie es in ML üblich ist.
string prefix = ConsoleFormatters.CreatePrefix(level, component, _config.ShowTimestamps);
string fullMsg = $"{prefix} {message}";
var color = ConsoleTheme.GetLevelColor(level);
if (level == "error" || level == "err")
MelonLogger.Error(fullMsg);
else if (level == "warn" || level == "warning")
MelonLogger.Warning(fullMsg);
else
MelonLogger.Msg(color, fullMsg);
}
private static bool IsLevelEnabled(string level)
{
var current = level.ToLower() switch
{
"debug" or "dbg" => LogLevel.Debug,
"info" => LogLevel.Info,
"warn" or "warning" or "wrn" => LogLevel.Warning,
"error" or "err" => LogLevel.Error,
"status" => LogLevel.Status,
_ => LogLevel.Info
};
return current >= _config.MinLogLevel;
}
}
@@ -22,6 +22,7 @@ public sealed class MelonLoggerAdapter : IGregLogger
public void Debug(string message) => _melonLogger.Msg(ConsoleColor.Gray, $"{_prefix}{message}");
public void Info(string message) => _melonLogger.Msg(ConsoleColor.White, $"{_prefix}{message}");
public void Success(string message) => _melonLogger.Msg(ConsoleColor.Green, $"{_prefix}{message}");
public void Warning(string message) => _melonLogger.Warning($"{_prefix}{message}");
public void Error(string message, Exception? ex = null)
{
+1
View File
@@ -10,6 +10,7 @@ public sealed class NullLogger : IGregLogger
{
public void Debug(string message) { }
public void Info(string message) { }
public void Success(string message) { }
public void Warning(string message) { }
public void Error(string message, Exception? ex = null) { }
public IGregLogger ForContext(string context) => this;
@@ -2,7 +2,7 @@ using gregCore.PublicApi;
namespace gregCore.Infrastructure.Performance;
internal sealed class GregPerformanceGovernor : IGregPerformanceGovernor, IDisposable
public sealed class GregPerformanceGovernor : IGregPerformanceGovernor, IDisposable
{
private readonly GregFrameRateLimiter _fpsLimiter;
private readonly GregRequestThrottler _throttler;
@@ -12,6 +12,7 @@ public sealed class GregPluginRegistry : IGregPluginRegistry
private readonly IGregLogger _logger;
private readonly IGregEventBus _eventBus;
private readonly List<PluginInfo> _loadedPlugins = new();
private readonly Dictionary<string, ModMetadata> _registeredMods = new();
public GregPluginRegistry(IAssemblyScanner scanner, IGregLogger logger, IGregEventBus eventBus)
{
@@ -20,6 +21,26 @@ public sealed class GregPluginRegistry : IGregPluginRegistry
_eventBus = eventBus;
}
public void RegisterMod(ModMetadata metadata)
{
if (string.IsNullOrEmpty(metadata.ModId))
{
_logger.Error("Mod-Registrierung fehlgeschlagen: ModId ist leer.");
return;
}
_registeredMods[metadata.ModId] = metadata;
_logger.Info($"Mod registriert: {metadata.Name} ({metadata.Version}) [ID: {metadata.ModId}]");
}
public ModMetadata GetModMetadata(string modId)
{
_registeredMods.TryGetValue(modId, out var metadata);
return metadata;
}
public IEnumerable<ModMetadata> GetAllRegisteredMods() => _registeredMods.Values;
public void LoadAll()
{
_logger.Info("Lade alle Plugins...");
+14
View File
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace gregCore.Infrastructure.Plugins;
public class ModMetadata
{
public string ModId { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public object ApiObject { get; set; }
public bool HasSettings { get; set; }
public bool HasKeybinds { get; set; }
public List<string> CustomTabs { get; set; } = new();
}
+47 -8
View File
@@ -1,8 +1,9 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: JavaScript Skripting Bridge.
/// Maintainer: Ermöglicht Modding via JS (Jint).
/// </file-summary>
using System;
using System.IO;
using System.Collections.Generic;
using Jint;
using gregCore.Core.Abstractions;
using gregCore.API;
namespace gregCore.Infrastructure.Scripting.Js;
@@ -10,27 +11,65 @@ public sealed class JsBridge : IGregLanguageBridge
{
private readonly IGregLogger _logger;
private readonly IGregEventBus _eventBus;
private readonly Engine _engine;
public JsBridge(IGregLogger logger, IGregEventBus eventBus)
{
_logger = logger.ForContext("JsBridge");
_eventBus = eventBus;
_engine = new Engine(options => {
options.AllowClr();
});
}
public void Initialize()
{
_logger.Info("JS Bridge initialisiert.");
_logger.Info("JS Bridge initializing...");
// Register API
var greg = new Dictionary<string, object>();
RegisterApi(greg);
_engine.SetValue("greg", greg);
string gameRoot = global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory;
string jsDir = Path.Combine(gameRoot, "Plugins", "Js");
if (!Directory.Exists(jsDir)) Directory.CreateDirectory(jsDir);
foreach (var file in Directory.GetFiles(jsDir, "*.js"))
{
ExecuteScript(File.ReadAllText(file));
}
}
private void RegisterApi(Dictionary<string, object> greg)
{
greg["logInfo"] = (Action<string>)GregAPI.LogInfo;
greg["logWarning"] = (Action<string>)GregAPI.LogWarning;
greg["logError"] = (Action<string>)GregAPI.LogError;
greg["on"] = (Action<string, Jint.Native.JsValue>)((hookName, callback) => {
GregAPI.Hooks.On(hookName, payload => {
callback.Call(Jint.Native.JsValue.FromObject(_engine, payload));
});
});
greg["fire"] = (Action<string, IDictionary<string, object>>)((hookName, data) => {
var payload = new gregCore.Sdk.Models.GregPayload(hookName, "JsMod");
foreach (var kvp in data) payload.Data[kvp.Key] = kvp.Value;
GregAPI.Hooks.Fire(hookName, payload);
});
}
public void ExecuteScript(string scriptContent)
{
try
{
_engine.Execute(scriptContent);
_logger.Debug("JS-Skript ausgeführt.");
}
catch (GregBridgeException ex)
catch (Exception ex)
{
_logger.Error($"[JsBridge] Bridge-Fehler: {ex.Message}", ex);
_logger.Error($"[JsBridge] JS-Fehler: {ex.Message}", ex);
}
}
}
@@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Linq;
using gregCore.Infrastructure.Settings.Models;
using UnityEngine;
namespace gregCore.Infrastructure.Settings;
public class GregKeybindRegistry
{
// ModId.ActionId -> KeybindEntry
private readonly Dictionary<string, KeybindEntry> _keybinds = new();
private readonly IGregLogger _logger;
public GregKeybindRegistry(IGregLogger logger)
{
_logger = logger.ForContext("KeybindRegistry");
}
public void Register(KeybindEntry entry)
{
var id = entry.GetFullId();
// If it already exists, meaning it was loaded from persistence earlier,
// we just update its callbacks and metadata, but preserve CurrentKey.
if (_keybinds.TryGetValue(id, out var existing))
{
existing.DisplayName = entry.DisplayName;
existing.Description = entry.Description;
existing.OnPress = entry.OnPress;
existing.DefaultKey = entry.DefaultKey;
existing.Category = entry.Category;
}
else
{
_keybinds[id] = entry;
if (entry.CurrentKey == KeyCode.None)
{
entry.CurrentKey = entry.DefaultKey;
}
}
_logger.Info($"Keybind registriert: {entry.DisplayName} [Mod: {entry.ModId}, Key: {entry.CurrentKey}]");
CheckConflicts();
}
public void Unregister(string modId, string actionId)
{
var id = $"{modId}.{actionId}";
if (_keybinds.Remove(id))
{
_logger.Info($"Keybind entfernt: {id}");
CheckConflicts();
}
}
public KeybindEntry Get(string modId, string actionId)
{
_keybinds.TryGetValue($"{modId}.{actionId}", out var entry);
return entry;
}
public IEnumerable<KeybindEntry> GetAll() => _keybinds.Values;
public IEnumerable<KeybindEntry> GetByMod(string modId) => _keybinds.Values.Where(k => k.ModId == modId);
public void CheckConflicts()
{
// Reset conflict status
foreach (var entry in _keybinds.Values)
{
entry.HasConflict = false;
}
// Group by CurrentKey (excluding KeyCode.None)
var groups = _keybinds.Values
.Where(k => k.CurrentKey != KeyCode.None)
.GroupBy(k => k.CurrentKey)
.Where(g => g.Count() > 1);
bool foundConflict = false;
foreach (var group in groups)
{
foundConflict = true;
foreach (var entry in group)
{
entry.HasConflict = true;
}
}
if (foundConflict)
{
_logger.Warning("Keybind-Konflikte erkannt! Bitte im Settings-Menü prüfen.");
}
}
}
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using gregCore.Core.Abstractions;
using gregCore.Infrastructure.Settings.Models;
using gregCore.Infrastructure.Settings.Services;
namespace gregCore.Infrastructure.Settings;
public class GregModSettingsService
{
private readonly Dictionary<string, BaseSettingEntry> _settings = new();
private readonly IGregLogger _logger;
private GregSettingsPersistenceService _persistence;
public GregModSettingsService(IGregLogger logger)
{
_logger = logger.ForContext("ModSettingsService");
}
public void SetPersistence(GregSettingsPersistenceService persistence)
{
_persistence = persistence;
}
public void Register<T>(SettingEntry<T> entry)
{
var id = entry.GetFullId();
if (_settings.TryGetValue(id, out var existingBase) && existingBase is SettingEntry<T> existing)
{
existing.DisplayName = entry.DisplayName;
existing.Description = entry.Description;
existing.Category = entry.Category;
existing.OnValueChanged = entry.OnValueChanged;
existing.DefaultValue = entry.DefaultValue;
}
else
{
_settings[id] = entry;
// First time registration, so value is default
if (EqualityComparer<T>.Default.Equals(entry.Value, default(T)))
{
entry.Value = entry.DefaultValue;
}
_persistence?.ApplyLoadedSettingsTo(entry);
}
_logger.Info($"Setting registriert: {entry.DisplayName} [Mod: {entry.ModId}, Wert: {entry.Value}]");
}
public SettingEntry<T> Get<T>(string modId, string settingId)
{
if (_settings.TryGetValue($"{modId}.{settingId}", out var entry) && entry is SettingEntry<T> typedEntry)
{
return typedEntry;
}
return null;
}
public void UpdateSetting<T>(string modId, string settingId, T newValue)
{
var entry = Get<T>(modId, settingId);
if (entry != null)
{
entry.Value = newValue;
entry.OnValueChanged?.Invoke(newValue);
_persistence?.SaveAll();
_logger.Info($"Setting aktualisiert: {modId}.{settingId} -> {newValue}");
}
}
public void ResetToDefault(string modId, string settingId)
{
var id = $"{modId}.{settingId}";
if (_settings.TryGetValue(id, out var entry))
{
// We need to use reflection here because we don't know the type T
var entryType = entry.GetType();
var defaultValueField = entryType.GetProperty("DefaultValue");
var valueField = entryType.GetProperty("Value");
if (defaultValueField != null && valueField != null)
{
var defaultValue = defaultValueField.GetValue(entry);
valueField.SetValue(entry, defaultValue);
var onValueChangedField = entryType.GetProperty("OnValueChanged");
if (onValueChangedField != null)
{
var callback = onValueChangedField.GetValue(entry) as Delegate;
callback?.DynamicInvoke(defaultValue);
}
_persistence?.SaveAll();
_logger.Info($"Setting auf Default zurückgesetzt: {id}");
}
}
}
public IEnumerable<BaseSettingEntry> GetAll() => _settings.Values;
public IEnumerable<BaseSettingEntry> GetByMod(string modId) => _settings.Values.Where(s => s.ModId == modId);
public IEnumerable<BaseSettingEntry> Search(string query)
{
if (string.IsNullOrEmpty(query)) return _settings.Values;
var q = query.ToLowerInvariant();
return _settings.Values.Where(s =>
s.DisplayName.ToLowerInvariant().Contains(q) ||
s.ModId.ToLowerInvariant().Contains(q) ||
(s.Category != null && s.Category.ToLowerInvariant().Contains(q)));
}
}
@@ -0,0 +1,22 @@
using System;
using UnityEngine;
namespace gregCore.Infrastructure.Settings.Models;
public class KeybindEntry
{
public string ModId { get; set; }
public string ActionId { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public KeyCode CurrentKey { get; set; }
public KeyCode DefaultKey { get; set; }
public string Category { get; set; }
public bool HasConflict { get; set; }
// Ignored in JSON, used at runtime
[Newtonsoft.Json.JsonIgnore]
public Action OnPress { get; set; }
public string GetFullId() => $"{ModId}.{ActionId}";
}
@@ -0,0 +1,31 @@
using System;
namespace gregCore.Infrastructure.Settings.Models;
public abstract class BaseSettingEntry
{
public string ModId { get; set; }
public string SettingId { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public string TypeName { get; set; }
public string GetFullId() => $"{ModId}.{SettingId}";
}
public class SettingEntry<T> : BaseSettingEntry
{
public T Value { get; set; }
public T DefaultValue { get; set; }
// Ignored in JSON
[Newtonsoft.Json.JsonIgnore]
public Action<T> OnValueChanged { get; set; }
public SettingEntry()
{
TypeName = typeof(T).Name;
}
}
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using gregCore.Core.Abstractions;
namespace gregCore.Infrastructure.Settings.Services;
public class GregHudService
{
private readonly IGregLogger _logger;
private readonly GregKeybindRegistry _keybindRegistry;
private bool _showHud = false;
public GregHudService(IGregLogger logger, GregKeybindRegistry keybindRegistry)
{
_logger = logger.ForContext("HudService");
_keybindRegistry = keybindRegistry;
}
public void Toggle() => _showHud = !_showHud;
public void OnGUI()
{
if (!_showHud) return;
var conflicts = _keybindRegistry.GetAll().Where(k => k.HasConflict).ToList();
if (conflicts.Count == 0) return;
GUI.Box(new Rect(10, 10, 300, 40 + (conflicts.Count * 20)), "gregCore: Keybind-Konflikte!");
int y = 40;
foreach (var conflict in conflicts)
{
GUI.Label(new Rect(20, y, 280, 20), $"<color=red>{conflict.DisplayName} ({conflict.CurrentKey})</color>");
y += 20;
}
}
}
@@ -0,0 +1,76 @@
using System;
using UnityEngine;
using gregCore.Core.Abstractions;
using gregCore.Infrastructure.Settings.Models;
namespace gregCore.Infrastructure.Settings.Services;
public class GregInputBindingService
{
private readonly IGregLogger _logger;
private readonly GregKeybindRegistry _keybindRegistry;
private GregSettingsPersistenceService _persistence;
public GregInputBindingService(IGregLogger logger, GregKeybindRegistry keybindRegistry)
{
_logger = logger.ForContext("InputBindingService");
_keybindRegistry = keybindRegistry;
}
public void SetPersistence(GregSettingsPersistenceService persistence)
{
_persistence = persistence;
}
public bool Rebind(string modId, string actionId, KeyCode newKey)
{
var entry = _keybindRegistry.Get(modId, actionId);
if (entry == null)
{
_logger.Error($"Rebind fehlgeschlagen: Keybind {modId}.{actionId} nicht gefunden.");
return false;
}
var oldKey = entry.CurrentKey;
entry.CurrentKey = newKey;
_logger.Info($"Keybind geändert: {modId}.{actionId} von {oldKey} zu {newKey}");
_keybindRegistry.CheckConflicts();
_persistence?.SaveAll();
return true;
}
public void ResetToDefault(string modId, string actionId)
{
var entry = _keybindRegistry.Get(modId, actionId);
if (entry != null)
{
Rebind(modId, actionId, entry.DefaultKey);
}
}
public void OnUpdate()
{
try
{
foreach (var keybind in _keybindRegistry.GetAll())
{
if (keybind.CurrentKey != KeyCode.None && keybind.OnPress != null)
{
// Fallback using standard input manager as configured in gregCore.
// If the game restricts legacy input entirely, this would be swapped to InputSystem mapping.
if (Input.GetKeyDown(keybind.CurrentKey))
{
keybind.OnPress.Invoke();
}
}
}
}
catch (Exception ex)
{
// _logger.Error("Error checking keybinds", ex); // Too spammy for Update loop
}
}
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using gregCore.Core.Abstractions;
namespace gregCore.Infrastructure.Settings.Services;
public class GregNotificationService
{
private readonly IGregLogger _logger;
private readonly List<Notification> _activeNotifications = new();
public GregNotificationService(IGregLogger logger)
{
_logger = logger.ForContext("NotificationService");
}
public void Show(string title, string message, float duration = 5f)
{
_activeNotifications.Add(new Notification { Title = title, Message = message, Expiration = Time.time + duration });
_logger.Info($"Notification: {title} - {message}");
}
public void OnGUI()
{
int y = Screen.height - 100;
foreach (var notification in _activeNotifications.ToArray())
{
if (Time.time > notification.Expiration)
{
_activeNotifications.Remove(notification);
continue;
}
GUI.Box(new Rect(Screen.width - 320, y, 300, 60), $"{notification.Title}\n{notification.Message}");
y -= 70;
}
}
private class Notification
{
public string Title { get; set; }
public string Message { get; set; }
public float Expiration { get; set; }
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using gregCore.Infrastructure.Settings.Models;
using gregCore.Core.Abstractions;
namespace gregCore.Infrastructure.Settings.Services;
public class GregSettingsConflictService
{
private readonly IGregLogger _logger;
private readonly GregKeybindRegistry _keybindRegistry;
private readonly GregModSettingsService _modSettingsService;
public GregSettingsConflictService(IGregLogger logger, GregKeybindRegistry keybindRegistry, GregModSettingsService modSettingsService)
{
_logger = logger.ForContext("SettingsConflictService");
_keybindRegistry = keybindRegistry;
_modSettingsService = modSettingsService;
}
public void ValidateAll()
{
_logger.Info("Validiere Einstellungen und Keybinds...");
_keybindRegistry.CheckConflicts();
// 1. Keybind Conflicts
var conflicts = _keybindRegistry.GetAll().Where(k => k.HasConflict).ToList();
if (conflicts.Any())
{
var groups = conflicts.GroupBy(k => k.CurrentKey);
foreach (var group in groups)
{
var actions = string.Join(", ", group.Select(x => x.GetFullId()));
_logger.Warning($"[Keybind-Konflikt] Taste '{group.Key}' wird mehrfach verwendet: {actions}");
}
}
// 2. Missing Defaults or IDs
foreach (var keybind in _keybindRegistry.GetAll())
{
if (string.IsNullOrEmpty(keybind.ModId) || string.IsNullOrEmpty(keybind.ActionId))
{
_logger.Error($"[Registrierungsfehler] Keybind ohne ModId oder ActionId gefunden: {keybind.DisplayName}");
}
}
foreach (var setting in _modSettingsService.GetAll())
{
if (string.IsNullOrEmpty(setting.ModId) || string.IsNullOrEmpty(setting.SettingId))
{
_logger.Error($"[Registrierungsfehler] Setting ohne ModId oder SettingId gefunden: {setting.DisplayName}");
}
}
_logger.Info("Validierung abgeschlossen.");
}
}
@@ -0,0 +1,128 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using gregCore.Core.Abstractions;
using gregCore.Infrastructure.Settings.Models;
namespace gregCore.Infrastructure.Settings.Services;
public class GregSettingsPersistenceService
{
private readonly IGregLogger _logger;
private readonly GregKeybindRegistry _keybindRegistry;
private readonly GregModSettingsService _modSettingsService;
private readonly string _keybindsFile;
private readonly string _settingsFile;
public GregSettingsPersistenceService(
IGregLogger logger,
GregKeybindRegistry keybindRegistry,
GregModSettingsService modSettingsService,
IGregEventBus eventBus = null)
{
_logger = logger.ForContext("SettingsPersistence");
_keybindRegistry = keybindRegistry;
_modSettingsService = modSettingsService;
var userData = global::MelonLoader.Utils.MelonEnvironment.UserDataDirectory;
_keybindsFile = Path.Combine(userData, "gregCore_Keybinds.json");
_settingsFile = Path.Combine(userData, "gregCore_ModSettings.json");
if (eventBus != null)
{
eventBus.Subscribe("greg.SYSTEM.SettingsClosed", _ => SaveAll());
}
}
public void SaveAll() => Save();
public void Load()
{
try
{
_logger.Info("Lade Einstellungen und Keybinds...");
if (File.Exists(_keybindsFile))
{
var content = File.ReadAllText(_keybindsFile);
var entries = JsonConvert.DeserializeObject<List<KeybindEntry>>(content);
if (entries != null)
{
foreach (var entry in entries)
{
_keybindRegistry.Register(entry);
}
_logger.Info($"[Settings] {entries.Count} Keybinds geladen.");
}
}
if (File.Exists(_settingsFile))
{
// Settings are polymorphic, handled via ApplyLoadedSettingsTo during registration
_logger.Info("[Settings] Mod-Settings Persistence bereit.");
}
}
catch (Exception ex)
{
_logger.Error("[Settings] Fehler beim Laden der Persistence.", ex);
}
}
public void Save()
{
try
{
var keybinds = _keybindRegistry.GetAll().ToList();
var kbContent = JsonConvert.SerializeObject(keybinds, Formatting.Indented);
File.WriteAllText(_keybindsFile, kbContent);
var settings = _modSettingsService.GetAll().ToDictionary(k => k.GetFullId(), v => GetValueObject(v));
var stContent = JsonConvert.SerializeObject(settings, Formatting.Indented);
File.WriteAllText(_settingsFile, stContent);
_logger.Info($"[Settings] {keybinds.Count} Keybinds und {settings.Count} Settings gespeichert.");
}
catch (Exception ex)
{
_logger.Error("[Settings] Fehler beim Speichern der Persistence.", ex);
}
}
private object GetValueObject(BaseSettingEntry entry)
{
var type = entry.GetType();
var prop = type.GetProperty("Value");
return prop?.GetValue(entry);
}
public void ApplyLoadedSettingsTo(BaseSettingEntry newEntry)
{
// When a mod registers a setting AFTER load, we inject the Value from the JSON.
try
{
if (File.Exists(_settingsFile))
{
var content = File.ReadAllText(_settingsFile);
var dict = JsonConvert.DeserializeObject<Dictionary<string, Newtonsoft.Json.Linq.JToken>>(content);
if (dict != null && dict.TryGetValue(newEntry.GetFullId(), out var jval))
{
var type = newEntry.GetType();
var genericArgs = type.GetGenericArguments();
if (genericArgs.Length > 0)
{
var tgtType = genericArgs[0];
var finalVal = jval.ToObject(tgtType);
var prop = type.GetProperty("Value");
prop?.SetValue(newEntry, finalVal);
}
}
}
}
catch (Exception ex)
{
_logger.Error($"[Settings] Failed to apply delayed setting: {newEntry?.GetFullId()}", ex);
}
}
}
@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using gregCore.Infrastructure.Settings.Models;
using gregCore.Core.Abstractions;
using Il2CppTMPro;
namespace gregCore.Infrastructure.Settings.Services;
public class GregSettingsUiBridge
{
private readonly IGregLogger _logger;
private readonly GregModSettingsService _settingsService;
private readonly GregKeybindRegistry _keybindRegistry;
private readonly GregInputBindingService _inputBindingService;
private readonly GregPluginRegistry _pluginRegistry;
private GameObject _mainPanel;
private InputField _searchInput;
private Transform _contentContainer;
public GregSettingsUiBridge(
IGregLogger logger,
GregModSettingsService settingsService,
GregKeybindRegistry keybindRegistry,
GregInputBindingService inputBindingService,
GregPluginRegistry pluginRegistry)
{
_logger = logger.ForContext("SettingsUiBridge");
_settingsService = settingsService;
_keybindRegistry = keybindRegistry;
_inputBindingService = inputBindingService;
_pluginRegistry = pluginRegistry;
}
public void BuildModSettingsPanel(GameObject panel)
{
_mainPanel = panel;
_logger.Info("Baue Mod-Settings UI...");
// 1. Setup ScrollView
var scrollObj = new GameObject("ModSettingsScrollView");
scrollObj.transform.SetParent(panel.transform, false);
var scrollRect = scrollObj.AddComponent<ScrollRect>();
var viewport = new GameObject("Viewport");
viewport.transform.SetParent(scrollObj.transform, false);
viewport.AddComponent<Image>().color = new Color(0, 0, 0, 0.5f);
viewport.AddComponent<Mask>().showMaskGraphic = false;
var content = new GameObject("Content");
content.transform.SetParent(viewport.transform, false);
_contentContainer = content.transform;
var vlg = content.AddComponent<VerticalLayoutGroup>();
vlg.childControlHeight = true;
vlg.childControlWidth = true;
vlg.childForceExpandHeight = false;
vlg.spacing = 10;
vlg.padding = new RectOffset(20, 20, 20, 20);
var csf = content.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scrollRect.viewport = viewport.GetComponent<RectTransform>();
scrollRect.content = content.GetComponent<RectTransform>();
// 2. Add Search Bar
AddSearchBar(content.transform);
// 3. Populate Mods
RefreshUi();
}
private void AddSearchBar(Transform parent)
{
var searchObj = new GameObject("SearchBar");
searchObj.transform.SetParent(parent, false);
_searchInput = searchObj.AddComponent<InputField>();
_searchInput.onValueChanged.AddListener(new Action<string>(query => RefreshUi(query)));
var placeholderObj = new GameObject("Placeholder");
placeholderObj.transform.SetParent(searchObj.transform, false);
var placeholderText = placeholderObj.AddComponent<Text>();
placeholderText.text = "Suche nach Mods oder Keybinds...";
placeholderText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
placeholderText.color = Color.gray;
_searchInput.placeholder = placeholderText;
}
public void RefreshUi(string query = "")
{
if (_contentContainer == null) return;
// Clear existing (except search bar)
foreach (Transform child in _contentContainer)
{
if (child.name == "SearchBar") continue;
UnityEngine.Object.Destroy(child.gameObject);
}
var mods = _pluginRegistry.GetAllRegisteredMods();
foreach (var mod in mods)
{
if (!string.IsNullOrEmpty(query) && !mod.Name.ToLowerInvariant().Contains(query.ToLowerInvariant()))
continue;
AddModHeader(mod);
// Add Settings
var settings = _settingsService.GetByMod(mod.ModId);
foreach (var setting in settings)
{
AddSettingEntry(setting);
}
// Add Keybinds
var keybinds = _keybindRegistry.GetByMod(mod.ModId);
foreach (var keybind in keybinds)
{
AddKeybindEntry(keybind);
}
}
}
private void AddModHeader(ModMetadata mod)
{
var headerObj = new GameObject($"Header_{mod.ModId}");
headerObj.transform.SetParent(_contentContainer, false);
var text = headerObj.AddComponent<Text>();
text.text = $"{mod.Name} (v{mod.Version})";
text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
text.fontSize = 24;
text.fontStyle = FontStyle.Bold;
text.color = Color.white;
}
private void AddSettingEntry(BaseSettingEntry setting)
{
var entryObj = new GameObject($"Setting_{setting.GetFullId()}");
entryObj.transform.SetParent(_contentContainer, false);
var text = entryObj.AddComponent<Text>();
text.text = $" {setting.DisplayName}: {GetValue(setting)}";
text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
text.fontSize = 18;
text.color = Color.cyan;
}
private void AddKeybindEntry(KeybindEntry keybind)
{
var entryObj = new GameObject($"Keybind_{keybind.GetFullId()}");
entryObj.transform.SetParent(_contentContainer, false);
var text = entryObj.AddComponent<Text>();
var conflictText = keybind.HasConflict ? " <color=red>[KONFLIKT]</color>" : "";
text.text = $" {keybind.DisplayName}: {keybind.CurrentKey}{conflictText}";
text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
text.fontSize = 18;
text.color = keybind.HasConflict ? Color.red : Color.yellow;
text.supportRichText = true;
}
private string GetValue(BaseSettingEntry entry)
{
var type = entry.GetType();
var prop = type.GetProperty("Value");
return prop?.GetValue(entry)?.ToString() ?? "N/A";
}
}
+35 -109
View File
@@ -1,140 +1,66 @@
/// <file-summary>
/// Schicht: Infrastructure
/// Zweck: In-Game DevConsole Overlay mittels Unity IMGUI.
/// Maintainer: Läuft im Unity Main Thread. Nutzt UnityEngine.GUI & GUILayout.
/// </file-summary>
using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEngine;
using MelonLoader;
namespace gregCore.Infrastructure.UI;
internal sealed class GregDevConsole
public sealed class GregDevConsole
{
private static GregDevConsole? _instance;
public static GregDevConsole Instance => _instance ??= new GregDevConsole();
private static readonly Lazy<GregDevConsole> _instance = new(() => new GregDevConsole());
public static GregDevConsole Instance => _instance.Value;
public bool IsOpen { get; private set; }
private string _input = "";
private readonly List<string> _logs = new();
private readonly Dictionary<string, Action<string[]>> _commands = new();
private Vector2 _scroll;
private bool _isOpen = false;
public bool IsOpen => _isOpen;
private GregDevConsole()
private Rect _windowRect = new Rect(20f, 20f, 600f, 400f);
private string _inputCommand = "";
private readonly List<LogEntry> _logs = new();
private Vector2 _scrollPosition;
public void Toggle() => _isOpen = !_isOpen;
public void AddLog(string message, LogType type)
{
RegisterCommand("help", _ => Log("Verfügbare Befehle: " + string.Join(", ", _commands.Keys)));
RegisterCommand("clear", _ => _logs.Clear());
RegisterCommand("exit", _ => Toggle());
Log("gregCore DevConsole initialisiert. Tippe 'help' für Befehle.");
}
public void RegisterCommand(string name, Action<string[]> action)
{
_commands[name.ToLower()] = action;
}
public void Toggle()
{
IsOpen = !IsOpen;
var mgm = global::Il2Cpp.MainGameManager.instance;
if (IsOpen)
{
_input = "";
if (mgm != null) mgm.isPauseMenuDisallowed = true;
global::UnityEngine.Cursor.visible = true;
global::UnityEngine.Cursor.lockState = global::UnityEngine.CursorLockMode.None;
}
else
{
if (mgm != null) mgm.isPauseMenuDisallowed = false;
global::UnityEngine.Cursor.visible = false;
global::UnityEngine.Cursor.lockState = global::UnityEngine.CursorLockMode.Locked;
}
}
public void Log(string message)
{
_logs.Add($"[{DateTime.Now:HH:mm:ss}] {message}");
_logs.Add(new LogEntry { Message = message, Type = type, Time = DateTime.Now });
if (_logs.Count > 100) _logs.RemoveAt(0);
_scroll.y = float.MaxValue;
_scrollPosition.y = float.MaxValue;
}
public void OnGUI()
{
if (!IsOpen) return;
if (!_isOpen) return;
_windowRect = GUILayout.Window(1337, _windowRect, (Action<int>)DrawWindow, "gregCore DevConsole");
}
float width = Screen.width * 0.8f;
float height = Screen.height * 0.4f;
float x = (Screen.width - width) / 2;
GUILayout.BeginArea(new Rect(x, 10, width, height), GUI.skin.box);
GUILayout.Label("<b>gregCore DevConsole</b>");
_scroll = GUILayout.BeginScrollView(_scroll);
private void DrawWindow(int windowId)
{
_scrollPosition = GUILayout.BeginScrollView(_scrollPosition);
foreach (var log in _logs)
{
GUILayout.Label(log);
GUILayout.Label($"[{log.Time:HH:mm:ss}] {log.Message}");
}
GUILayout.EndScrollView();
GUILayout.BeginHorizontal();
Rect inputRect = GUILayoutUtility.GetRect(200, 20, GUILayout.ExpandWidth(true));
GUI.Box(inputRect, _input + (Time.time % 1f < 0.5f ? "_" : ""));
var e = Event.current;
if (e.isKey && e.type == EventType.KeyDown)
_inputCommand = GUILayout.TextField(_inputCommand);
if (GUILayout.Button("Send", GUILayout.Width(60f)))
{
if (e.keyCode == KeyCode.Return)
if (!string.IsNullOrWhiteSpace(_inputCommand))
{
// return handled below
}
else if (e.keyCode == KeyCode.Backspace)
{
if (_input.Length > 0) _input = _input.Substring(0, _input.Length - 1);
}
else if (e.character >= 32 && e.character <= 126)
{
_input += e.character;
}
}
if (GUILayout.Button("Run", GUILayout.Width(80)) ||
(e.isKey && e.type == EventType.KeyDown && e.keyCode == KeyCode.Return))
{
if (!string.IsNullOrWhiteSpace(_input))
{
Execute(_input);
_input = "";
AddLog($"> {_inputCommand}", LogType.Log);
_inputCommand = "";
}
}
GUILayout.EndHorizontal();
GUILayout.EndArea();
GUI.DragWindow();
}
private void Execute(string input)
private struct LogEntry
{
Log($"> {input}");
var parts = input.Split(' ', System.StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0) return;
var cmd = parts[0].ToLower();
var args = parts.Length > 1 ? parts[1..] : System.Array.Empty<string>();
if (_commands.TryGetValue(cmd, out var action))
{
try { action(args); }
catch (System.Exception ex) { Log($"<color=red>Fehler: {ex.Message}</color>"); }
}
else
{
Log($"<color=yellow>Unbekannter Befehl: {cmd}</color>");
}
public string Message;
public LogType Type;
public DateTime Time;
}
}
+52
View File
@@ -0,0 +1,52 @@
using System;
using UnityEngine;
using UnityEngine.EventSystems;
namespace gregCore.Infrastructure.UI;
internal static class GregUiManager
{
private static EventSystem? _disabledEventSystem;
private static int _reenableCounter;
public static bool IsAnyUiOpen { get; private set; }
public static void RegisterUiOpen()
{
IsAnyUiOpen = true;
DisableGameInput();
}
public static void RegisterUiClosed()
{
IsAnyUiOpen = false;
_reenableCounter = 2; // Defer by 2 frames to catch mouse-up
}
private static void DisableGameInput()
{
try
{
var es = EventSystem.current;
if (es != null && es.enabled)
{
_disabledEventSystem = es;
es.enabled = false;
}
}
catch { }
}
public static void OnUpdate()
{
if (_reenableCounter > 0)
{
_reenableCounter--;
if (_reenableCounter <= 0 && _disabledEventSystem != null)
{
try { _disabledEventSystem.enabled = true; } catch { }
_disabledEventSystem = null;
}
}
}
}
+5 -4
View File
@@ -8,8 +8,9 @@ namespace gregCore.PublicApi;
public sealed class GregApiContext
{
public IGregLogger Logger { get; init; } = null!;
public IGregEventBus EventBus { get; init; } = null!;
public IGregConfigService Config { get; init; } = null!;
public IGregPersistenceService Persist { get; init; } = null!;
public required IGregLogger Logger { get; init; }
public required IGregEventBus EventBus { get; init; }
public required Core.Events.GregHookBus HookBus { get; init; }
public required IGregConfigService Config { get; init; }
public required IGregPersistenceService Persist { get; init; }
}
+4 -2
View File
@@ -1,3 +1,5 @@
using UnityEngine;
namespace gregCore.PublicApi.Modules;
public sealed class GregFacilityModule
@@ -5,5 +7,5 @@ public sealed class GregFacilityModule
private readonly GregApiContext _ctx;
internal GregFacilityModule(GregApiContext ctx) => _ctx = ctx;
public bool UnlockRoom(string roomId) => true; // API Logic
}
public int GetRackCount() => UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.Rack>().Length;
}
+10 -26
View File
@@ -1,3 +1,5 @@
using UnityEngine;
namespace gregCore.PublicApi.Modules;
public sealed class GregNetworkModule
@@ -5,30 +7,12 @@ 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 int GetSwitchCount() => UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.NetworkSwitch>().Length;
public int GetBrokenSwitchCount() {
int count = 0;
foreach (var s in UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.NetworkSwitch>()) {
if (s.isBroken) count++;
}
return count;
}
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"]));
}
}
}
+32 -2
View File
@@ -1,8 +1,38 @@
using UnityEngine;
using System.Reflection;
namespace gregCore.PublicApi.Modules;
public sealed class GregNpcModule
{
private readonly GregApiContext _ctx;
internal GregNpcModule(GregApiContext ctx) => _ctx = ctx;
public bool HireEmployee(string techId) => true;
}
public int GetFreeTechnicianCount() {
int count = 0;
foreach (var t in UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.Technician>()) {
if (!t.isBusy) count++;
}
return count;
}
public int GetTotalTechnicianCount() => UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.Technician>().Length;
public bool DispatchRepairServer(global::Il2Cpp.Server server) {
try {
var tm = global::Il2Cpp.TechnicianManager.instance;
if (tm == null) return false;
// Use reflection if direct call fails
tm.GetType().GetMethod("DispatchRepairServer")?.Invoke(tm, new object[] { server });
return true;
} catch { return false; }
}
public bool DispatchRepairSwitch(global::Il2Cpp.NetworkSwitch sw) {
try {
var tm = global::Il2Cpp.TechnicianManager.instance;
if (tm == null) return false;
tm.GetType().GetMethod("DispatchRepairSwitch")?.Invoke(tm, new object[] { sw });
return true;
} catch { return false; }
}
}
+6 -2
View File
@@ -1,3 +1,5 @@
using UnityEngine;
namespace gregCore.PublicApi.Modules;
public sealed class GregPlayerModule
@@ -8,5 +10,7 @@ public sealed class GregPlayerModule
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
}
public Vector3 GetPosition() => Player?.transform.position ?? Vector3.zero;
public Vector3 GetRotation() => Player?.transform.eulerAngles ?? Vector3.zero;
}
+5 -4
View File
@@ -5,9 +5,10 @@ public sealed class GregSaveModule
private readonly GregApiContext _ctx;
internal GregSaveModule(GregApiContext ctx) => _ctx = ctx;
public void TriggerSave() => global::Il2Cpp.SaveSystem.SaveGame();
public string GetCurrentScene() => global::UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
public void TriggerSave() => global::Il2Cpp.SaveSystem.SaveGameData();
public int GetDifficulty() => 1; // Default fallback
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);
}
}
+11 -2
View File
@@ -1,3 +1,5 @@
using UnityEngine;
namespace gregCore.PublicApi.Modules;
public sealed class GregServerModule
@@ -5,5 +7,12 @@ public sealed class GregServerModule
private readonly GregApiContext _ctx;
internal GregServerModule(GregApiContext ctx) => _ctx = ctx;
public bool Spawn(string serverId, int rackId) => true; // API Logic
}
public int GetCount() => UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.Server>().Length;
public int GetBrokenCount() {
int count = 0;
foreach (var s in UnityEngine.Object.FindObjectsOfType<global::Il2Cpp.Server>()) {
if (s.isBroken) count++;
}
return count;
}
}
+10 -17
View File
@@ -5,21 +5,14 @@ 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 float GetTimeOfDay() => global::Il2Cpp.TimeController.instance?.currentTimeOfDay ?? 0f;
public int GetDay() => global::Il2Cpp.TimeController.instance?.day ?? 1;
public float GetSecondsInFullDay() => global::Il2Cpp.TimeController.instance?.secondsInFullDay ?? 1200f;
public void SetSecondsInFullDay(float s) {
if (global::Il2Cpp.TimeController.instance != null) global::Il2Cpp.TimeController.instance.secondsInFullDay = s;
}
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());
}
}
public bool IsPaused() => global::UnityEngine.Time.timeScale == 0;
public void SetPaused(bool paused) => global::UnityEngine.Time.timeScale = paused ? 0 : 1;
public float GetTimeScale() => global::UnityEngine.Time.timeScale;
public void SetTimeScale(float scale) => global::UnityEngine.Time.timeScale = scale;
}
+7 -2
View File
@@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using gregCore.Core.Models;
namespace gregCore.PublicApi.Modules;
public sealed class GregUIModule
@@ -7,9 +11,10 @@ public sealed class GregUIModule
public void ShowToast(string message, float durationSeconds = 3f)
=> _ctx.EventBus.Publish("greg.ui.ShowToast", new EventPayload {
HookName = "greg.ui.ShowToast",
OccurredAtUtc = DateTime.UtcNow,
Data = new Dictionary<string, object> { ["message"] = message, ["duration"] = durationSeconds }
});
public void ShowError(string message) => ShowToast($"⚠ {message}", 5f);
}
public void ShowNotification(string message) => ShowToast(message, 5f);
}
+2 -2
View File
@@ -4,10 +4,10 @@ namespace gregCore.PublicApi;
public static class greg
{
internal static GregApiContext? _context;
public static GregApiContext? _context;
private static GregApiContext Context => _context ?? throw new InvalidOperationException("gregCore nicht initialisiert.");
internal static gregCore.Infrastructure.Performance.GregPerformanceGovernor? _governor;
public static gregCore.Infrastructure.Performance.GregPerformanceGovernor? _governor;
private static GregEconomyModule? _economy;
private static GregNetworkModule? _network;
+110
View File
@@ -0,0 +1,110 @@
using System;
using gregCore.Sdk.Models;
using gregCore.Core.Abstractions;
using gregCore.Core.Events;
using gregCore.Infrastructure.Settings;
using gregCore.Infrastructure.Settings.Services;
using gregCore.Infrastructure.Plugins;
using gregCore.GameLayer.Bootstrap;
namespace gregCore.Sdk;
/// <summary>
/// Die zentrale Implementierung der öffentlichen SDK-API (SDK Layer).
/// Dient als stabiler Brückenkopf zwischen Mods und den internen Framework-Services.
/// </summary>
public sealed class GregAPI : IGregAPI
{
private readonly IGregLogger _logger;
private readonly GregHookBus _hookBus;
private readonly GregModSettingsService _settingsService;
private readonly GregKeybindRegistry _keybindRegistry;
private readonly GregPluginRegistry _pluginRegistry;
private readonly GregNotificationService _notificationService;
private readonly Core.Services.GregValidationService _validationService;
public string Version => "1.1.0";
public GregAPI(
IGregLogger logger,
GregHookBus hookBus,
GregModSettingsService settingsService,
GregKeybindRegistry keybindRegistry,
IGregPluginRegistry pluginRegistry,
GregNotificationService notificationService,
Core.Services.GregValidationService validationService)
{
_logger = logger.ForContext("SDK_API");
_hookBus = hookBus;
_settingsService = settingsService;
_keybindRegistry = keybindRegistry;
_pluginRegistry = (GregPluginRegistry)pluginRegistry;
_notificationService = notificationService;
_validationService = validationService;
}
// --- Hooks & Events ---
public void On(string hookName, Action<GregPayload> handler)
{
if (!_validationService.ValidateHookName(hookName)) return;
_hookBus.On(hookName, (payload) => {
// Umwandlung in SDK-Payload für saubere Abstraktion
var sdkPayload = new GregPayload(payload.HookName, payload.Trigger) {
Data = payload.Data
};
handler(sdkPayload);
});
}
public void Fire(string hookName, GregPayload payload)
{
var corePayload = new Core.Models.EventPayload {
HookName = payload.HookName,
Trigger = payload.Trigger,
Data = payload.Data
};
_hookBus.Dispatch(hookName, corePayload);
}
// --- Mod Registration ---
public void RegisterMod(string modId, string name, string version, object? apiObject = null)
{
if (!_validationService.ValidateModId(modId)) return;
_pluginRegistry.RegisterMod(new ModMetadata {
ModId = modId, Name = name, Version = version, ApiObject = apiObject
});
}
// --- Settings & Input ---
public void RegisterToggle(string modId, string settingId, string displayName, bool defaultValue, Action<bool>? onChanged = null, string category = "General", string description = "")
{
var entry = new Infrastructure.Settings.Models.SettingEntry<bool> {
ModId = modId, SettingId = settingId, DisplayName = displayName, DefaultValue = defaultValue, OnValueChanged = onChanged, Category = category, Description = description
};
_settingsService.Register(entry);
}
public void RegisterSlider(string modId, string settingId, string displayName, float defaultValue, Action<float>? onChanged = null, string category = "General", string description = "")
{
var entry = new Infrastructure.Settings.Models.SettingEntry<float> {
ModId = modId, SettingId = settingId, DisplayName = displayName, DefaultValue = defaultValue, OnValueChanged = onChanged, Category = category, Description = description
};
_settingsService.Register(entry);
}
public void RegisterKeybind(string modId, string actionId, string displayName, UnityEngine.KeyCode defaultKey, Action onPress, string category = "Controls", string description = "")
{
var entry = new Infrastructure.Settings.Models.KeybindEntry {
ModId = modId, ActionId = actionId, DisplayName = displayName, DefaultKey = defaultKey, CurrentKey = defaultKey, Category = category, OnPress = onPress, Description = description
};
_keybindRegistry.Register(entry);
}
// --- Notifications ---
public void ShowNotification(string title, string message, float duration = 5f)
{
_notificationService.Show(title, message, duration);
}
}
+28
View File
@@ -0,0 +1,28 @@
using System;
using gregCore.Sdk.Models;
namespace gregCore.Sdk;
/// <summary>
/// Das öffentliche Interface für alle Mod-Entwickler (SDK Layer).
/// Stellt eine stabile, versionierte API bereit.
/// </summary>
public interface IGregAPI
{
string Version { get; }
// --- Hooks & Events ---
void On(string hookName, Action<GregPayload> handler);
void Fire(string hookName, GregPayload payload);
// --- Mod Registration ---
void RegisterMod(string modId, string name, string version, object? apiObject = null);
// --- Settings & Input ---
void RegisterToggle(string modId, string settingId, string displayName, bool defaultValue, Action<bool>? onChanged = null, string category = "General", string description = "");
void RegisterSlider(string modId, string settingId, string displayName, float defaultValue, Action<float>? onChanged = null, string category = "General", string description = "");
void RegisterKeybind(string modId, string actionId, string displayName, UnityEngine.KeyCode defaultKey, Action onPress, string category = "Controls", string description = "");
// --- Notifications ---
void ShowNotification(string title, string message, float duration = 5f);
}
+46
View File
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Linq;
namespace gregCore.Sdk.Metadata;
public enum HookStatus { ENABLED, DISABLED, DEPRECATED }
public enum HookLayer { CORE, SDK, HARMONY, PLUGIN }
/// <summary>
/// Metadaten für einen einzelnen Hook im Framework-Katalog.
/// </summary>
public sealed record HookMetadata(
string Name,
HookStatus Status,
HookLayer Layer,
string Trigger,
string PayloadType,
string SinceVersion,
string KnownIssues = ""
);
/// <summary>
/// Der zentrale Hook-Katalog (SDK Layer).
/// Dient als Source of Truth für alle 1771 Hooks.
/// </summary>
public sealed class GregHookCatalog
{
private readonly Dictionary<string, HookMetadata> _hooks = new();
public void Register(HookMetadata metadata)
{
_hooks[metadata.Name] = metadata;
}
public HookMetadata? Get(string hookName)
{
_hooks.TryGetValue(hookName, out var metadata);
return metadata;
}
public IEnumerable<HookMetadata> GetAll() => _hooks.Values;
public IEnumerable<HookMetadata> GetByStatus(HookStatus status) => _hooks.Values.Where(h => h.Status == status);
public int TotalCount => _hooks.Count;
}
+29
View File
@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace gregCore.Sdk.Models;
/// <summary>
/// Einheitliches Payload-Modell für alle Framework-Events und Hooks (SDK Layer).
/// </summary>
public sealed class GregPayload
{
public string HookName { get; init; } = string.Empty;
public string Trigger { get; init; } = string.Empty;
public Dictionary<string, object> Data { get; init; } = new();
public string Version { get; init; } = "1.0.0";
public GregPayload() { }
public GregPayload(string hookName, string trigger)
{
HookName = hookName;
Trigger = trigger;
}
public T? GetValue<T>(string key)
{
if (Data.TryGetValue(key, out var value) && value is T typedValue)
return typedValue;
return default;
}
}
@@ -0,0 +1,78 @@
using System;
using System.IO;
using System.Collections.Generic;
using Newtonsoft.Json;
using gregCore.Core.Abstractions;
using gregCore.Sdk.Metadata;
namespace gregCore.Sdk.Services;
/// <summary>
/// Service zur Verwaltung und Initialisierung des Hook-Katalogs (SDK Layer).
/// </summary>
public sealed class GregHookCatalogService
{
private readonly IGregLogger _logger;
private readonly GregHookCatalog _catalog;
public GregHookCatalogService(IGregLogger logger, GregHookCatalog catalog)
{
_logger = logger.ForContext("HookCatalogService");
_catalog = catalog;
}
/// <summary>
/// Lädt alle 1771 Hooks aus der game_hooks.json Datei.
/// </summary>
public void Initialize()
{
try
{
var filePath = Path.Combine(global::MelonLoader.Utils.MelonEnvironment.GameRootDirectory, "game_hooks.json");
if (!File.Exists(filePath))
{
// Fallback, falls Datei im Mod-Ordner liegt
filePath = Path.Combine(global::MelonLoader.Utils.MelonEnvironment.ModsDirectory, "game_hooks.json");
}
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath);
var rawHooks = JsonConvert.DeserializeObject<List<RawHookData>>(json);
if (rawHooks != null)
{
foreach (var raw in rawHooks)
{
var hookName = $"greg.{raw.Group.ToUpper()}.{raw.ClassName}.{raw.MethodName}";
_catalog.Register(new HookMetadata(
hookName,
HookStatus.ENABLED,
HookLayer.HARMONY,
$"Triggered on {raw.ClassName}.{raw.MethodName}",
raw.IsVoid ? "None" : raw.ReturnType,
"1.1.0"
));
}
_logger.Success($"{_catalog.TotalCount} Hooks erfolgreich aus game_hooks.json initialisiert.");
}
}
else
{
_logger.Warning("game_hooks.json nicht gefunden. Hook-Katalog ist leer.");
}
}
catch (Exception ex)
{
_logger.Error("Fehler beim Initialisieren des Hook-Katalogs", ex);
}
}
private class RawHookData
{
public string Group { get; set; } = string.Empty;
public string ClassName { get; set; } = string.Empty;
public string MethodName { get; set; } = string.Empty;
public string ReturnType { get; set; } = string.Empty;
public bool IsVoid { get; set; }
}
}
+61
View File
@@ -0,0 +1,61 @@
using System;
using UnityEngine;
using Il2Cpp;
namespace greg.Sdk;
public static class gregNativeEventHooks
{
// Legacy support for older mods expecting static actions - MUST be nullable fields to match older binaries exactly
public static Action? SystemGameLoaded;
public static Action? SystemGameSaved;
public static Action<float>? PlayerCoinChanged;
public static Action<float>? PlayerReputationChanged;
public static Action<float>? PlayerXpChanged;
public static Action<int>? DayEnded;
public static Action<int>? MonthEnded;
public static Action<object>? CustomerAccepted;
public static Action<object>? ServerInstalled;
public static Action<object>? ServerBroken;
public static Action<object>? ServerRepaired;
public static Action<float>? ShopCheckout;
public static class ByEventId
{
public static void MoneyChanged(float newAmount) => PlayerCoinChanged?.Invoke(newAmount);
public static void XpChanged(float newXp) => PlayerXpChanged?.Invoke(newXp);
public static void ReputationChanged(float newRep) => PlayerReputationChanged?.Invoke(newRep);
public static void ServerPowered(object server) { }
public static void ServerBroken(object server) => gregNativeEventHooks.ServerBroken?.Invoke(server);
public static void ServerRepaired(object server) => gregNativeEventHooks.ServerRepaired?.Invoke(server);
public static void ServerInstalled(object server) => gregNativeEventHooks.ServerInstalled?.Invoke(server);
public static void CableConnected(object cable) { }
public static void CableDisconnected(object cable) { }
public static void ServerCustomerChanged(object server, int customers) { }
public static void ServerAppChanged(object server, int appId) { }
public static void DayEnded(int day) => gregNativeEventHooks.DayEnded?.Invoke(day);
public static void MonthEnded(int month) => gregNativeEventHooks.MonthEnded?.Invoke(month);
public static void CustomerAccepted(object customer) => gregNativeEventHooks.CustomerAccepted?.Invoke(customer);
public static void CustomerSatisfied(object customer) { }
public static void CustomerUnsatisfied(object customer) { }
public static void ShopCheckout(float total) => gregNativeEventHooks.ShopCheckout?.Invoke(total);
public static void ShopItemAdded(int itemId) { }
public static void ShopCartCleared() { }
public static void EmployeeHired(object tech) { }
public static void EmployeeFired(object tech) { }
public static void GameSaved() => gregNativeEventHooks.SystemGameSaved?.Invoke();
public static void GameLoaded() => gregNativeEventHooks.SystemGameLoaded?.Invoke();
public static void GameAutoSaved() { }
}
public static class ByName
{
public static float GetPlayerMoney() => 0f;
public static float GetPlayerXp() => 0f;
public static float GetPlayerReputation() => 0f;
public static int GetTimeOfDay() => (int)(Il2Cpp.TimeController.instance?.currentTimeOfDay ?? 0f);
public static int GetDay() => 1;
public static Transform? GetPlayerCamera() => Camera.main?.transform;
}
}
+635
View File
@@ -0,0 +1,635 @@
[
{
"Group": "Uncategorized",
"Namespace": "Il2Cpp",
"ClassName": "AutoDisable",
"MethodName": "OnEnable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2Cpp",
"ClassName": "AutoDisable",
"MethodName": "TurnOffAfterXseconds",
"ReturnType": "IEnumerator",
"IsVoid": false,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "Benchmark01",
"MethodName": "Start",
"ReturnType": "IEnumerator",
"IsVoid": false,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "Benchmark02",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "Benchmark03",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "Benchmark03",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "Benchmark04",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2Cpp",
"ClassName": "GetCurrentVersion",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2Cpp",
"ClassName": "LocalisedText",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "ClampRotationAroundXAxis",
"ReturnType": "Quaternion",
"IsVoid": false,
"Parameters": [
{
"Name": "q",
"Type": "Quaternion"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "Init",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "character",
"Type": "Transform"
},
{
"Name": "camera",
"Type": "Transform"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "InternalLockUpdate",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "ResetRotation",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "character",
"Type": "Transform"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "SetCursorLock",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "value",
"Type": "Boolean"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "SittingClampRotation",
"ReturnType": "Vector2",
"IsVoid": false,
"Parameters": [
{
"Name": "q",
"Type": "Vector2"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "UpdateCursorLock",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "_Init_b__22_0",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "MouseLook",
"MethodName": "_Init_b__22_1",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "ObjectSpin",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "ObjectSpin",
"MethodName": "Update",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2Cpp",
"ClassName": "PositionIndicator",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2Cpp",
"ClassName": "PositionIndicator",
"MethodName": "Update",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "Cleanup",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "HideItemNameOrSiluete",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "Init",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "ResetHold",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_0",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_1",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_2",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_3",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_4",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_5",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "UnityStandardAssets.Characters.FirstPerson",
"ClassName": "RayLookAt",
"MethodName": "_Init_b__20_6",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": [
{
"Name": "ctx",
"Type": "CallbackContext"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "SimpleScript",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "SimpleScript",
"MethodName": "Update",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextConsoleSimulator",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextConsoleSimulator",
"MethodName": "OnDisable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextConsoleSimulator",
"MethodName": "OnEnable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextConsoleSimulator",
"MethodName": "RevealCharacters",
"ReturnType": "IEnumerator",
"IsVoid": false,
"Parameters": [
{
"Name": "textComponent",
"Type": "TMP_Text"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextConsoleSimulator",
"MethodName": "RevealWords",
"ReturnType": "IEnumerator",
"IsVoid": false,
"Parameters": [
{
"Name": "textComponent",
"Type": "TMP_Text"
}
]
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextConsoleSimulator",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextMeshProFloatingText",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextMeshProFloatingText",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextMeshSpawner",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "TextMeshSpawner",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexJitter",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexJitter",
"MethodName": "OnDisable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexJitter",
"MethodName": "OnEnable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexJitter",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeA",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeA",
"MethodName": "OnDisable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeA",
"MethodName": "OnEnable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeA",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeB",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeB",
"MethodName": "OnDisable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeB",
"MethodName": "OnEnable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexShakeB",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexZoom",
"MethodName": "Awake",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexZoom",
"MethodName": "OnDisable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexZoom",
"MethodName": "OnEnable",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
},
{
"Group": "Uncategorized",
"Namespace": "Il2CppTMPro.Examples",
"ClassName": "VertexZoom",
"MethodName": "Start",
"ReturnType": "Void",
"IsVoid": true,
"Parameters": []
}
]