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:
@@ -0,0 +1,3 @@
|
||||
[submodule "plugins/DataCenter-RustBridge"]
|
||||
path = plugins/DataCenter-RustBridge
|
||||
url = https://github.com/mleem97/DataCenter-RustBridge.git
|
||||
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.
|
||||
@@ -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() {}
|
||||
@@ -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
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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.")
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+10
-9
@@ -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 fü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>
|
||||
|
||||
@@ -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.
Submodule
+1
Submodule plugins/DataCenter-RustBridge added at 181e10bd52
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user