Version 4.7 - Radial Menu

Add a new button-"Radial Menu (Z)" instead of "Recall (B)"
This commit is contained in:
2025-12-04 03:45:07 +11:00
parent 17dc31dd10
commit b134834170
18 changed files with 789 additions and 169 deletions

View File

@@ -51,6 +51,7 @@ namespace DevourClient
public static float exp = 1000f;
public static bool _walkInLobby = false;
public static bool infinite_mirrors = false;
static bool radialMenuEnabled = true;
static bool player_esp = false;
static bool player_skel_esp = false;
static bool player_snapline = false;
@@ -188,11 +189,9 @@ namespace DevourClient
fly = !fly;
}
// Recall to base position (B key, only if enabled)
if (recallEnabled && Input.GetKeyDown(KeyCode.B) && Player.IsInGameOrLobby())
{
Helpers.RecallHelper.RecallToBase();
}
// Z-key radial menu logic is managed by RadialMenuManager
RadialMenuManager.Enabled = radialMenuEnabled;
RadialMenuManager.HandleUpdate();
if (Player.IsInGameOrLobby())
{
@@ -607,12 +606,15 @@ namespace DevourClient
}
}
if (crosshair && in_game_cache)
{
const float crosshairSize = 4;
// End of EventType.Repaint branch
}
float xMin = (Settings.Settings.width) - (crosshairSize / 2);
float yMin = (Settings.Settings.height) - (crosshairSize / 2);
if (crosshair && in_game_cache)
{
const float crosshairSize = 4f;
float xMin = Settings.Settings.width - (crosshairSize / 2f);
float yMin = Settings.Settings.height - (crosshairSize / 2f);
if (crosshairTexture == null)
{
@@ -622,7 +624,8 @@ namespace DevourClient
GUI.DrawTexture(new Rect(xMin, yMin, crosshairSize, crosshairSize), crosshairTexture);
}
}
// Radial menu rendering
RadialMenuManager.HandleOnGUI();
if (Settings.Settings.menu_enable)
{
@@ -2028,14 +2031,14 @@ namespace DevourClient
_walkInLobby = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 155, 140, 20), _walkInLobby, MultiLanguageSystem.Translate("Walk In Lobby"));
_IsAutoRespawn = GUI.Toggle(new Rect(Settings.Settings.x + 160, Settings.Settings.y + 155, 140, 20), _IsAutoRespawn, MultiLanguageSystem.Translate("Auto Respawn"));
fly = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 190, 100, 20), fly, MultiLanguageSystem.Translate("Fly"));
if (GUI.Button(new Rect(Settings.Settings.x + 120, Settings.Settings.y + 190, 60, 20), Settings.Settings.flyKey.ToString()))
fly = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 180, 100, 20), fly, MultiLanguageSystem.Translate("Fly"));
if (GUI.Button(new Rect(Settings.Settings.x + 120, Settings.Settings.y + 180, 60, 20), Settings.Settings.flyKey.ToString()))
{
Settings.Settings.flyKey = Settings.Settings.GetKey();
}
GUI.Label(new Rect(Settings.Settings.x + 20, Settings.Settings.y + 215, 80, 20), MultiLanguageSystem.Translate("Fly Speed") + ":");
fly_speed = GUI.HorizontalSlider(new Rect(Settings.Settings.x + 100, Settings.Settings.y + 220, 150, 10), fly_speed, 5f, 20f);
GUI.Label(new Rect(Settings.Settings.x + 260, Settings.Settings.y + 215, 50, 20), ((int)fly_speed).ToString());
GUI.Label(new Rect(Settings.Settings.x + 20, Settings.Settings.y + 205, 80, 20), MultiLanguageSystem.Translate("Fly Speed") + ":");
fly_speed = GUI.HorizontalSlider(new Rect(Settings.Settings.x + 100, Settings.Settings.y + 210, 150, 10), fly_speed, 5f, 20f);
GUI.Label(new Rect(Settings.Settings.x + 260, Settings.Settings.y + 205, 50, 20), ((int)fly_speed).ToString());
spoofLevel = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 250, 200, 20), spoofLevel, MultiLanguageSystem.Translate("Spoof Level"));
GUI.Label(new Rect(Settings.Settings.x + 20, Settings.Settings.y + 275, 80, 20), MultiLanguageSystem.Translate("Level") + ":");
@@ -2054,7 +2057,7 @@ namespace DevourClient
showCoordinates = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 430, 200, 20), showCoordinates, MultiLanguageSystem.Translate("Show Coordinates"));
recallEnabled = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 455, 200, 20), recallEnabled, MultiLanguageSystem.Translate("Recall (B)"));
radialMenuEnabled = GUI.Toggle(new Rect(Settings.Settings.x + 10, Settings.Settings.y + 455, 200, 20), radialMenuEnabled, MultiLanguageSystem.Translate("Radial Menu (Z)"));
// Display player coordinates at the bottom of Misc tab
if (showCoordinates && Player.IsInGameOrLobby())

View File

@@ -2,9 +2,7 @@ using System.Collections.Generic;
namespace DevourClient.ESP
{
/// <summary>
/// Item ESP configuration management class - dynamically display different item types based on map
/// </summary>
// Item ESP configuration management class - dynamically display different item types based on map
public static class ItemESPConfig
{
// ESP type enumeration
@@ -260,9 +258,6 @@ namespace DevourClient.ESP
{ ESPType.Collectables, false }
};
/// <summary>
/// Get list of ESP types supported by specified map
/// </summary>
public static List<ESPType> GetMapESPTypes(string sceneName)
{
// If in menu, return empty list
@@ -284,35 +279,23 @@ namespace DevourClient.ESP
};
}
/// <summary>
/// Get ESP type enable status
/// </summary>
public static bool GetESPState(ESPType type)
{
return espStates.ContainsKey(type) ? espStates[type] : false;
}
/// <summary>
/// Set ESP type enable status
/// </summary>
public static void SetESPState(ESPType type, bool enabled)
{
if (espStates.ContainsKey(type))
espStates[type] = enabled;
}
/// <summary>
/// Toggle ESP type enable status
/// </summary>
public static void ToggleESPState(ESPType type)
{
if (espStates.ContainsKey(type))
espStates[type] = !espStates[type];
}
/// <summary>
/// Get ESP type display name (for translation key)
/// </summary>
public static string GetESPTypeName(ESPType type)
{
switch (type)
@@ -420,9 +403,6 @@ namespace DevourClient.ESP
}
}
/// <summary>
/// Get corresponding ESP type based on item name
/// </summary>
public static ESPType? GetESPTypeByItemName(string itemName)
{
if (string.IsNullOrEmpty(itemName))
@@ -617,9 +597,6 @@ namespace DevourClient.ESP
}
}
/// <summary>
/// Check if ESP should be shown for specified item
/// </summary>
public static bool ShouldShowESP(string itemName)
{
ESPType? espType = GetESPTypeByItemName(itemName);
@@ -629,9 +606,6 @@ namespace DevourClient.ESP
return GetESPState(espType.Value);
}
/// <summary>
/// Reset all ESP states
/// </summary>
public static void ResetAllStates()
{
var keys = new List<ESPType>(espStates.Keys);

View File

@@ -1,74 +0,0 @@
using UnityEngine;
using MelonLoader;
namespace DevourClient.Helpers
{
public static class RecallHelper
{
/// <summary>
/// Teleports the local player to the base coordinates of the current map
/// </summary>
public static void RecallToBase()
{
try
{
Il2Cpp.NolanBehaviour nb = Player.GetPlayer();
if (nb == null)
{
MelonLogger.Warning("Player not found!");
return;
}
string sceneName = Map.GetActiveScene();
Vector3 targetPos = Vector3.zero;
string mapName = "";
// Map coordinates based on scene name
switch (sceneName)
{
case "Devour":
case "Anna": // Farmhouse
targetPos = new Vector3(5.03f, 4.20f, -50.02f);
mapName = "Farm";
break;
case "Molly": // Asylum
targetPos = new Vector3(17.52f, 1.38f, 7.04f);
mapName = "Asylum";
break;
case "Inn":
targetPos = new Vector3(3.53f, 0.84f, 2.47f);
mapName = "Inn";
break;
case "Town":
targetPos = new Vector3(-63.51f, 10.88f, -12.32f);
mapName = "Town";
break;
case "Slaughterhouse":
targetPos = new Vector3(6.09f, 0.70f, -17.58f);
mapName = "Slaughterhouse";
break;
case "Manor":
targetPos = new Vector3(3.67f, 1.32f, -23.34f);
mapName = "Manor";
break;
case "Carnival":
targetPos = new Vector3(-91.46f, 8.13f, -24.51f);
mapName = "Carnival";
break;
default:
MelonLogger.Warning($"Teleport not available for scene: {sceneName}");
return;
}
// Teleport player to target position
nb.locomotion.SetPosition(targetPos, false);
MelonLogger.Msg($"Teleported to {mapName} coordinates: X:{targetPos.x:F2} Y:{targetPos.y:F2} Z:{targetPos.z:F2}");
}
catch (System.Exception ex)
{
MelonLogger.Error($"Failed to teleport: {ex.Message}");
}
}
}
}

View File

@@ -5,17 +5,13 @@ using UnityEngine;
namespace DevourClient.Helpers
{
/// <summary>
/// Shared revive utilities. When running as host we mirror DevourX's revive flow.
/// </summary>
// Shared revive utilities. When running as host we mirror DevourX's revive flow.
public static class ReviveHelper
{
private static readonly Vector3 HostFallbackPosition = new Vector3(0f, -150f, 0f);
/// <summary>
/// Try to revive the provided NolanBehaviour using host-specific logic first,
/// then fall back to the standard interactable flow.
/// </summary>
// Try to revive the provided NolanBehaviour using host-specific logic first,
// then fall back to the standard interactable flow.
public static bool TryRevive(Il2Cpp.NolanBehaviour target)
{
if (target == null || target.gameObject == null)
@@ -88,11 +84,9 @@ namespace DevourClient.Helpers
return false;
}
/// <summary>
/// Legacy revive method migrated from StateHelper.BasePlayer.Revive().
/// This method preserves the original implementation from StateHelper.
/// </summary>
/// <param name="targetGameObject">The GameObject of the player to revive</param>
// Legacy revive method migrated from StateHelper.BasePlayer.Revive().
// This method preserves the original implementation from StateHelper.
// targetGameObject: The GameObject of the player to revive.
public static void ReviveLegacy(GameObject targetGameObject)
{
if (targetGameObject == null)

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "老鼠透视" },
{ "Region", "区域" },
{ "Revive", "复活" },
{ "Radial Menu (Z)", "轮盘菜单 (Z)" },
{ "Ritual Book", "仪式书" },
{ "Rose", "玫瑰" },
{ "Ritual Book ESP", "仪式书透视" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "传送到 Azazel" },
{ "TV", "电视" },
{ "Teleport Keys", "传送钥匙" },
{ "Recall (B)", "回城 (B)" },
{ "Teleport to", "传送至" },
{ "TP Base", "传送至基地" },
{ "TP Altar", "传送至祭坛" },
{ "TP Basin", "传送至水池" },
{ "TP Fountain", "传送至喷泉" },
{ "Ticket", "票券" },
{ "Town", "小镇" },
{ "TownDoor", "小镇门" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "Rat ESP" },
{ "Region", "Region" },
{ "Revive", "Revive" },
{ "Radial Menu (Z)", "Radial Menu (Z)" },
{ "Ritual Book", "Ritual Book" },
{ "Rose", "Rose" },
{ "Ritual Book ESP", "Ritual Book ESP" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP to Azazel" },
{ "TV", "TV" },
{ "Teleport Keys", "Teleport Keys" },
{ "Recall (B)", "Recall (B)" },
{ "Teleport to", "Teleport to" },
{ "TP Base", "TP Base" },
{ "TP Altar", "TP Altar" },
{ "TP Basin", "TP Basin" },
{ "TP Fountain", "TP Fountain" },
{ "Ticket", "Ticket" },
{ "Town", "Town" },
{ "TownDoor", "TownDoor" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "ESP rat" },
{ "Region", "Région" },
{ "Revive", "Réanimer" },
{ "Radial Menu (Z)", "Menu radial (Z)" },
{ "Ritual Book", "Livre rituel" },
{ "Rose", "Rose" },
{ "Ritual Book ESP", "ESP livre rituel" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP vers Azazel" },
{ "TV", "Télévision" },
{ "Teleport Keys", "Téléporter clés" },
{ "Recall (B)", "Retour (B)" },
{ "Teleport to", "Téléporter" },
{ "TP Base", "TP base" },
{ "TP Altar", "TP autel" },
{ "TP Basin", "TP bassin" },
{ "TP Fountain", "TP fontaine" },
{ "Ticket", "Billet" },
{ "Town", "Ville" },
{ "TownDoor", "Porte de ville" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "Ratte-ESP" },
{ "Region", "Region" },
{ "Revive", "Wiederbeleben" },
{ "Radial Menu (Z)", "Radiales Menü (Z)" },
{ "Ritual Book", "Ritualbuch" },
{ "Rose", "Rose" },
{ "Ritual Book ESP", "Ritualbuch-ESP" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP zu Azazel" },
{ "TV", "Fernseher" },
{ "Teleport Keys", "Schlüssel teleportieren" },
{ "Recall (B)", "Zurückrufen (B)" },
{ "Teleport to", "Teleportieren" },
{ "TP Base", "TP Basis" },
{ "TP Altar", "TP Altar" },
{ "TP Basin", "TP Becken" },
{ "TP Fountain", "TP Brunnen" },
{ "Ticket", "Ticket" },
{ "Town", "Stadt" },
{ "TownDoor", "Stadttür" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "ESP ratto" },
{ "Region", "Regione" },
{ "Revive", "Rianima" },
{ "Radial Menu (Z)", "Menu radiale (Z)" },
{ "Ritual Book", "Libro rituale" },
{ "Rose", "Rosa" },
{ "Ritual Book ESP", "ESP libro rituale" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP ad Azazel" },
{ "TV", "Televisione" },
{ "Teleport Keys", "Teletrasporta chiavi" },
{ "Recall (B)", "Richiama (B)" },
{ "Teleport to", "Teletrasporta" },
{ "TP Base", "TP Base" },
{ "TP Altar", "TP Altare" },
{ "TP Basin", "TP Bacino" },
{ "TP Fountain", "TP Fontana" },
{ "Ticket", "Biglietto" },
{ "Town", "Città" },
{ "TownDoor", "Porta della città" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "ネズミESP" },
{ "Region", "地域" },
{ "Revive", "蘇生" },
{ "Radial Menu (Z)", "ラジアルメニュー (Z)" },
{ "Ritual Book", "儀式の本" },
{ "Rose", "バラ" },
{ "Ritual Book ESP", "儀式の本ESP" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "Azazelへテレポート" },
{ "TV", "テレビ" },
{ "Teleport Keys", "鍵をテレポート" },
{ "Recall (B)", "リコール (B)" },
{ "Teleport to", "テレポート" },
{ "TP Base", "ベースTP" },
{ "TP Altar", "祭壇TP" },
{ "TP Basin", "水盤TP" },
{ "TP Fountain", "噴水TP" },
{ "Ticket", "チケット" },
{ "Town", "町" },
{ "TownDoor", "町のドア" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "쥐 ESP" },
{ "Region", "지역" },
{ "Revive", "부활" },
{ "Radial Menu (Z)", "방사형 메뉴 (Z)" },
{ "Ritual Book", "의식서" },
{ "Rose", "장미" },
{ "Ritual Book ESP", "의식서 ESP" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "Azazel로 텔레포트" },
{ "TV", "TV" },
{ "Teleport Keys", "열쇠 텔레포트" },
{ "Recall (B)", "리콜 (B)" },
{ "Teleport to", "텔레포트" },
{ "TP Base", "기지 TP" },
{ "TP Altar", "제단 TP" },
{ "TP Basin", "대야 TP" },
{ "TP Fountain", "분수 TP" },
{ "Ticket", "티켓" },
{ "Town", "마을" },
{ "TownDoor", "마을 문" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "ESP rato" },
{ "Region", "Região" },
{ "Revive", "Reviver" },
{ "Radial Menu (Z)", "Menu radial (Z)" },
{ "Ritual Book", "Livro ritual" },
{ "Rose", "Rosa" },
{ "Ritual Book ESP", "ESP livro ritual" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP para Azazel" },
{ "TV", "Televisão" },
{ "Teleport Keys", "Teletransportar chaves" },
{ "Recall (B)", "Recuar (B)" },
{ "Teleport to", "Teletransportar" },
{ "TP Base", "TP base" },
{ "TP Altar", "TP altar" },
{ "TP Basin", "TP bacia" },
{ "TP Fountain", "TP fonte" },
{ "Ticket", "Bilhete" },
{ "Town", "Cidade" },
{ "TownDoor", "Porta da cidade" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "ESP крысы" },
{ "Region", "Регион" },
{ "Revive", "Воскресить" },
{ "Radial Menu (Z)", "Радиальное меню (Z)" },
{ "Ritual Book", "Ритуальная книга" },
{ "Rose", "Роза" },
{ "Ritual Book ESP", "ESP ритуальной книги" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "ТП к Azazel" },
{ "TV", "Телевизор" },
{ "Teleport Keys", "Телепорт ключей" },
{ "Recall (B)", "Возврат (B)" },
{ "Teleport to", "Телепорт" },
{ "TP Base", "ТП база" },
{ "TP Altar", "ТП алтарь" },
{ "TP Basin", "ТП чаша" },
{ "TP Fountain", "ТП фонтан" },
{ "Ticket", "Билет" },
{ "Town", "Город" },
{ "TownDoor", "Дверь города" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "ESP rata" },
{ "Region", "Región" },
{ "Revive", "Revivir" },
{ "Radial Menu (Z)", "Menú radial (Z)" },
{ "Ritual Book", "Libro ritual" },
{ "Rose", "Rosa" },
{ "Ritual Book ESP", "ESP libro ritual" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP a Azazel" },
{ "TV", "Televisión" },
{ "Teleport Keys", "Teletransportar llaves" },
{ "Recall (B)", "Regresar (B)" },
{ "Teleport to", "Teletransportar" },
{ "TP Base", "TP base" },
{ "TP Altar", "TP altar" },
{ "TP Basin", "TP pila" },
{ "TP Fountain", "TP fuente" },
{ "Ticket", "Boleto" },
{ "Town", "Pueblo" },
{ "TownDoor", "Puerta de pueblo" },

View File

@@ -207,6 +207,7 @@ namespace DevourClient.Localization.Translations
{ "Rat ESP", "Chuột ESP" },
{ "Region", "Khu vực" },
{ "Revive", "Hồi sinh" },
{ "Radial Menu (Z)", "Menu vòng tròn (Z)" },
{ "Ritual Book", "Sách nghi lễ" },
{ "Rose", "Hoa hồng" },
{ "Ritual Book ESP", "Sách nghi lễ ESP" },
@@ -253,8 +254,11 @@ namespace DevourClient.Localization.Translations
{ "TP to Azazel", "TP đến Azazel" },
{ "TV", "TV" },
{ "Teleport Keys", "Phím dịch chuyển" },
{ "Recall (B)", "Triệu hồi (B)" },
{ "Teleport to", "Dịch chuyển đến" },
{ "TP Base", "TP căn cứ" },
{ "TP Altar", "TP bàn thờ" },
{ "TP Basin", "TP bể nước" },
{ "TP Fountain", "TP đài phun nước" },
{ "Ticket", "Vé" },
{ "Town", "Town" },
{ "TownDoor", "TownDoor" },

View File

@@ -8,17 +8,13 @@ using UnityEngine;
namespace DevourClient.Network
{
/// <summary>
/// Lobby creation and management class
/// </summary>
// Lobby creation and management class
public static class LobbyManager
{
/// <summary>
/// Create game lobby/room
/// </summary>
/// <param name="regionCode">Region code (e.g.: "eu", "us", "asia", "usw", "sa", "jp", "au", "ru", "in", "kr")</param>
/// <param name="lobbyLimit">Maximum player limit (1-64)</param>
/// <param name="isPrivate">Whether this is a private room</param>
// Create game lobby/room.
// regionCode: Region code (e.g.: "eu", "us", "asia", "usw", "sa", "jp", "au", "ru", "in", "kr").
// lobbyLimit: Maximum player limit (1-64).
// isPrivate: Whether this is a private room.
public static void CreateLobby(string regionCode, int lobbyLimit, bool isPrivate)
{
try
@@ -96,9 +92,6 @@ namespace DevourClient.Network
}
}
/// <summary>
/// Get PhotonRegion object for specified region
/// </summary>
private static PhotonRegion GetPhotonRegion(string regionCode)
{
try
@@ -113,9 +106,6 @@ namespace DevourClient.Network
}
}
/// <summary>
/// Find Menu controller
/// </summary>
private static Il2CppHorror.Menu FindMenuController()
{
try
@@ -142,9 +132,6 @@ namespace DevourClient.Network
}
}
/// <summary>
/// Check if in game
/// </summary>
private static bool IsInGame()
{
try
@@ -159,9 +146,6 @@ namespace DevourClient.Network
}
}
/// <summary>
/// Force start lobby game (host only)
/// </summary>
public static void ForceLobbyStart()
{
try
@@ -189,9 +173,6 @@ namespace DevourClient.Network
}
}
/// <summary>
/// Show message box
/// </summary>
public static void ShowMessageBox(string message)
{
try

View File

@@ -0,0 +1,698 @@
using System;
using System.Collections.Generic;
using MelonLoader;
using UnityEngine;
using DevourClient.Helpers;
using DevourClient.Localization;
using System.Globalization;
namespace DevourClient
{
// Manages the Z-key radial menu: building options, drawing the UI, and executing actions.
// Public static methods are called from the appropriate ClientMain lifecycle hooks.
internal static class RadialMenuManager
{
private static bool _active;
private static int _selectedIndex = -1;
private static readonly List<RadialOption> _options = new List<RadialOption>();
private static bool _cursorStateStored;
private static bool _prevCursorVisible;
private static CursorLockMode _prevCursorLockState;
private static bool _enabled = true; // Controls whether the radial menu is enabled
// Material used to draw radial arcs (GL immediate mode)
private static Material _radialMaterial;
// Public property to enable/disable the radial menu
public static bool Enabled
{
get => _enabled;
set => _enabled = value;
}
private enum RadialActionType
{
None,
SpawnItem,
TeleportBase,
TeleportFixedPoint
}
private class RadialOption
{
public string Label = string.Empty;
public RadialActionType ActionType = RadialActionType.None;
public string Payload = string.Empty;
}
// Called from Update: handles input, builds / closes the radial menu and executes actions.
public static void HandleUpdate()
{
if (!_enabled || !Player.IsInGameOrLobby())
return;
if (Input.GetKeyDown(KeyCode.Z))
{
BuildForCurrentScene();
}
if (Input.GetKeyUp(KeyCode.Z) && _active)
{
if (_selectedIndex >= 0 && _selectedIndex < _options.Count)
{
ExecuteOption(_options[_selectedIndex]);
}
_active = false;
_selectedIndex = -1;
_options.Clear();
RestoreCursorState();
}
}
// Called from OnGUI: draws the radial menu if it is active.
public static void HandleOnGUI()
{
if (!_enabled || !_active || !Player.IsInGameOrLobby())
return;
Draw();
}
private static void BuildForCurrentScene()
{
_options.Clear();
_selectedIndex = -1;
string sceneName = Helpers.Map.GetActiveScene();
if (string.IsNullOrEmpty(sceneName) || sceneName == "Menu")
{
_active = false;
return;
}
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("First aid"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalFirstAid"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Battery"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalBattery"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Base"),
ActionType = RadialActionType.TeleportBase,
Payload = string.Empty
});
switch (sceneName)
{
case "Devour":
case "Anna":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Altar"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "8.57 0.01 -65.19"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Hay"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalHay"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Gasoline"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalGasoline"
});
break;
case "Molly":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Altar"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "18.12 -8.80 21.06"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Fuse"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalFuse"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("RottenFood"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalRottenFood"
});
break;
case "Inn":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Fountain"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "-3.43 0.06 24.31"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Bleach"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalBleach"
});
break;
case "Town":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Altar"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "-56.88 7.17 -34.51"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Matchbox"),
ActionType = RadialActionType.SpawnItem,
Payload = "Matchbox-3"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Gasoline"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalGasoline"
});
break;
case "Slaughterhouse":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Altar"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "26.68 4.01 -9.27"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Bone"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalBone"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Gasoline"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalGasoline"
});
break;
case "Manor":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Basin"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "38.93 -4.62 -3.86"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Bleach"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalBleach"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Cake"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalCake"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Spade"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalSpade"
});
break;
case "Carnival":
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("TP Altar"),
ActionType = RadialActionType.TeleportFixedPoint,
Payload = "-114.65 4.07 -4.12"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("Coin"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalCoin"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("MusicBox"),
ActionType = RadialActionType.SpawnItem,
Payload = "MusicBox-Idle"
});
_options.Add(new RadialOption
{
Label = MultiLanguageSystem.Translate("DollHead"),
ActionType = RadialActionType.SpawnItem,
Payload = "SurvivalDollHead"
});
break;
}
if (_options.Count == 0)
{
_active = false;
return;
}
_active = true;
HideCursorForRadial();
}
private static void Draw()
{
Event e = Event.current;
if (e == null)
return;
bool isRepaint = e.type == EventType.Repaint;
float centerX = Screen.width / 2f;
float centerY = Screen.height / 2f;
Vector2 center = new Vector2(centerX, centerY);
float radiusOuter = 140f;
float radiusInner = 40f;
Vector2 mouse = e.mousePosition;
Vector2 dir = mouse - center;
float dist = dir.magnitude;
_selectedIndex = -1;
if (dist >= radiusInner && dist <= radiusOuter && _options.Count > 0)
{
// Normalize angles: convert GUI coordinates (y down) to math coordinates (y up)
// and treat "up" as 0° increasing clockwise.
Vector2 upDir = new Vector2(dir.x, -dir.y);
float mathAngle = Mathf.Atan2(upDir.y, upDir.x); // [-PI, PI], 0 is on the right, counterclockwise is positive
float logicalAngle = (Mathf.PI / 2f) - mathAngle;
if (logicalAngle < 0f)
{
logicalAngle += Mathf.PI * 2f;
}
float logicalSectorAngle = (Mathf.PI * 2f) / _options.Count;
int index = Mathf.Clamp(Mathf.FloorToInt(logicalAngle / logicalSectorAngle), 0, _options.Count - 1);
_selectedIndex = index;
}
int count = _options.Count;
if (count == 0)
return;
if (isRepaint)
{
EnsureRadialMaterial();
DrawFilledCircle(center, radiusOuter + 6f, new Color(0f, 0f, 0f, 0.55f));
}
// Sector layout and label radius
float logicalAnglePerSector = (Mathf.PI * 2f) / count;
float radiusFactor;
if (count <= 6)
radiusFactor = 0.55f;
else if (count == 7)
radiusFactor = 0.48f;
else if (count <= 8)
radiusFactor = 0.5f;
else
radiusFactor = 0.45f;
float labelRadius = radiusInner + (radiusOuter - radiusInner) * radiusFactor;
if (isRepaint)
{
for (int i = 0; i < count; i++)
{
float logicalStart = logicalAnglePerSector * i;
float logicalEnd = logicalAnglePerSector * (i + 1);
float displayStart = (Mathf.PI / 2f) - logicalStart;
float displayEnd = (Mathf.PI / 2f) - logicalEnd;
Color sectorColor = (i == _selectedIndex)
? new Color(0.15f, 0.7f, 0.3f, 0.8f)
: new Color(0.1f, 0.1f, 0.1f, 0.7f);
DrawFilledSector(center, radiusInner, radiusOuter, displayStart, displayEnd, sectorColor);
}
}
GUIStyle labelStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
normal = { textColor = Color.white },
fontSize = (count <= 6) ? 14 : (count <= 8 ? 12 : 10),
wordWrap = true
};
for (int i = 0; i < count; i++)
{
float logicalMid = logicalAnglePerSector * (i + 0.5f);
float displayMid = (Mathf.PI / 2f) - logicalMid;
float lx = centerX + Mathf.Cos(displayMid) * labelRadius;
float ly = centerY - Mathf.Sin(displayMid) * labelRadius;
float arcLength = logicalAnglePerSector * labelRadius;
float arcFactor;
float minWidth;
float maxWidth;
if (count <= 6)
{
arcFactor = 0.8f;
minWidth = 60f;
maxWidth = 120f;
}
else if (count == 7)
{
arcFactor = 0.6f;
minWidth = 45f;
maxWidth = 75f;
}
else if (count <= 8)
{
arcFactor = 0.65f;
minWidth = 45f;
maxWidth = 80f;
}
else
{
arcFactor = 0.55f;
minWidth = 40f;
maxWidth = 70f;
}
float baseWidth = arcLength * arcFactor;
float labelWidth = Mathf.Clamp(baseWidth, minWidth, maxWidth);
float labelHeight = 32f;
Rect labelRect = new Rect(lx - labelWidth / 2f, ly - labelHeight / 2f, labelWidth, labelHeight);
GUI.Label(labelRect, _options[i].Label, labelStyle);
}
}
private static void EnsureRadialMaterial()
{
if (_radialMaterial != null)
return;
Shader shader = Shader.Find("Hidden/Internal-Colored");
if (shader == null)
return;
_radialMaterial = new Material(shader)
{
hideFlags = HideFlags.HideAndDontSave
};
_radialMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
_radialMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
_radialMaterial.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off);
_radialMaterial.SetInt("_ZWrite", 0);
}
private static void DrawFilledCircle(Vector2 center, float radius, Color color)
{
if (_radialMaterial == null)
return;
_radialMaterial.SetPass(0);
GL.PushMatrix();
GL.LoadPixelMatrix(0, Screen.width, Screen.height, 0);
GL.Begin(GL.TRIANGLES);
GL.Color(color);
const int steps = 64;
for (int i = 0; i < steps; i++)
{
float t0 = (float)i / steps;
float t1 = (float)(i + 1) / steps;
float ang0 = t0 * Mathf.PI * 2f;
float ang1 = t1 * Mathf.PI * 2f;
float x0 = center.x + Mathf.Cos(ang0) * radius;
float y0 = center.y - Mathf.Sin(ang0) * radius;
float x1 = center.x + Mathf.Cos(ang1) * radius;
float y1 = center.y - Mathf.Sin(ang1) * radius;
GL.Vertex3(center.x, center.y, 0f);
GL.Vertex3(x0, y0, 0f);
GL.Vertex3(x1, y1, 0f);
}
GL.End();
GL.PopMatrix();
}
private static void DrawFilledSector(Vector2 center, float innerRadius, float outerRadius,
float startAngle, float endAngle, Color color)
{
if (_radialMaterial == null)
return;
_radialMaterial.SetPass(0);
GL.PushMatrix();
GL.LoadPixelMatrix(0, Screen.width, Screen.height, 0);
GL.Begin(GL.TRIANGLES);
GL.Color(color);
int steps = Mathf.Max(8, Mathf.CeilToInt(Mathf.Abs(endAngle - startAngle) / (Mathf.PI / 24f)));
float delta = (endAngle - startAngle) / steps;
for (int i = 0; i < steps; i++)
{
float a0 = startAngle + delta * i;
float a1 = startAngle + delta * (i + 1);
Vector2 o0 = new Vector2(
center.x + Mathf.Cos(a0) * outerRadius,
center.y - Mathf.Sin(a0) * outerRadius);
Vector2 o1 = new Vector2(
center.x + Mathf.Cos(a1) * outerRadius,
center.y - Mathf.Sin(a1) * outerRadius);
Vector2 i0 = new Vector2(
center.x + Mathf.Cos(a0) * innerRadius,
center.y - Mathf.Sin(a0) * innerRadius);
Vector2 i1 = new Vector2(
center.x + Mathf.Cos(a1) * innerRadius,
center.y - Mathf.Sin(a1) * innerRadius);
GL.Vertex3(o0.x, o0.y, 0f);
GL.Vertex3(o1.x, o1.y, 0f);
GL.Vertex3(i1.x, i1.y, 0f);
GL.Vertex3(o0.x, o0.y, 0f);
GL.Vertex3(i1.x, i1.y, 0f);
GL.Vertex3(i0.x, i0.y, 0f);
}
GL.End();
GL.PopMatrix();
}
private static void ExecuteOption(RadialOption option)
{
if (option == null || option.ActionType == RadialActionType.None)
return;
try
{
switch (option.ActionType)
{
case RadialActionType.SpawnItem:
if (string.IsNullOrEmpty(option.Payload))
return;
ClientMain_HandleItemCarry(option.Payload);
break;
case RadialActionType.TeleportBase:
TeleportToBase();
break;
case RadialActionType.TeleportFixedPoint:
TeleportToFixedPoint(option.Payload);
break;
}
}
catch (Exception ex)
{
MelonLogger.Error($"Radial option execution failed: {ex.Message}");
}
}
private static void HideCursorForRadial()
{
if (!_cursorStateStored)
{
_prevCursorVisible = Cursor.visible;
_prevCursorLockState = Cursor.lockState;
_cursorStateStored = true;
}
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
private static void RestoreCursorState()
{
if (!_cursorStateStored)
return;
Cursor.lockState = _prevCursorLockState;
Cursor.visible = _prevCursorVisible;
_cursorStateStored = false;
}
// Calls ClientMain.HandleItemCarry via reflection to avoid tight coupling,
// and falls back to Hacks.Misc.CarryObject if that fails.
private static void ClientMain_HandleItemCarry(string payload)
{
try
{
var type = typeof(ClientMain);
var method = type.GetMethod("HandleItemCarry",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (method != null)
{
method.Invoke(null, new object[] { payload });
return;
}
}
catch
{
// ignore and fallback
}
Hacks.Misc.CarryObject(payload);
}
// Teleports the player to a fixed world position.
// Payload format: "x y z" using '.' as decimal separator.
private static void TeleportToFixedPoint(string payload)
{
if (string.IsNullOrWhiteSpace(payload))
return;
try
{
string[] parts = payload.Split(new[] { ' ', '\t', ',' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 3)
return;
float x = float.Parse(parts[0], CultureInfo.InvariantCulture);
float y = float.Parse(parts[1], CultureInfo.InvariantCulture);
float z = float.Parse(parts[2], CultureInfo.InvariantCulture);
Il2Cpp.NolanBehaviour nb = Player.GetPlayer();
if (nb == null)
return;
Vector3 target = new Vector3(x, y, z);
nb.TeleportTo(target, Quaternion.identity);
}
catch (Exception ex)
{
MelonLogger.Error($"TeleportToFixedPoint failed: {ex.Message}");
}
}
// Teleports the player to the map-specific base coordinates (former RecallToBase logic).
private static void TeleportToBase()
{
try
{
Il2Cpp.NolanBehaviour nb = Player.GetPlayer();
if (nb == null)
{
MelonLogger.Warning("Player not found!");
return;
}
string sceneName = Helpers.Map.GetActiveScene();
Vector3 targetPos = Vector3.zero;
string mapName = "";
switch (sceneName)
{
case "Devour":
case "Anna": // Farmhouse
targetPos = new Vector3(5.03f, 4.20f, -50.02f);
mapName = "Farm";
break;
case "Molly": // Asylum
targetPos = new Vector3(17.52f, 1.38f, 7.04f);
mapName = "Asylum";
break;
case "Inn":
targetPos = new Vector3(3.53f, 0.84f, 2.47f);
mapName = "Inn";
break;
case "Town":
targetPos = new Vector3(-63.51f, 10.88f, -12.32f);
mapName = "Town";
break;
case "Slaughterhouse":
targetPos = new Vector3(6.09f, 0.70f, -17.58f);
mapName = "Slaughterhouse";
break;
case "Manor":
targetPos = new Vector3(3.67f, 1.32f, -23.34f);
mapName = "Manor";
break;
case "Carnival":
targetPos = new Vector3(-91.46f, 8.13f, -24.51f);
mapName = "Carnival";
break;
default:
MelonLogger.Warning($"Teleport not available for scene: {sceneName}");
return;
}
nb.locomotion.SetPosition(targetPos, false);
MelonLogger.Msg($"Teleported to {mapName} coordinates: X:{targetPos.x:F2} Y:{targetPos.y:F2} Z:{targetPos.z:F2}");
}
catch (Exception ex)
{
MelonLogger.Error($"Failed to teleport to base: {ex.Message}");
}
}
}
}

View File

@@ -103,9 +103,9 @@ If you want to modify and develop the code, please follow the [Building from sou
4、运行devour → 如果安装成功你会看到一个windows窗口进行各类安装提示后自动进入游戏。点击insert键即可打开和关闭devourclient窗口
**注意:**有些电脑在安装melonloader之后会出现fatal error的提示这个我目前并没有碰到过。但是出现这个提示的主要原因基本是melonloader安装过程中提取到devour根目录的melonloader文件夹里的文件出现了问题比较简单的解决办法就是1在别人的同系统同位宽x86x32的电脑里拷贝出来他的melonloader文件夹然后直接粘贴到自己的电脑里。2将melonloader文件夹完全删除然后重装。(3)在直到游戏完全运行菜单正常工作之前保持VPN线路通畅。
注意有些电脑在安装melonloader之后会出现fatal error的提示这个我目前并没有碰到过。但是出现这个提示的主要原因基本是melonloader安装过程中提取到devour根目录的melonloader文件夹里的文件出现了问题比较简单的解决办法就是1在别人的同系统同位宽x86x32的电脑里拷贝出来他的melonloader文件夹然后直接粘贴到自己的电脑里。2将melonloader文件夹完全删除然后重装。(3)在直到游戏完全运行菜单正常工作之前保持VPN线路通畅。
**注意:**如果在加载时提示 “0 mod”请检查你的dll文件是否正常是否已经将dll文件放置到devour的“mods”文件夹中。
注意:如果在加载时提示 “0 mod”请检查你的dll文件是否正常是否已经将dll文件放置到devour的“mods”文件夹中。
如果你想要对代码进行修改和开发,请按照下面的 [building from source](#building-from-source) 的步骤,逐步进行