Commit initial BossHealth, DamageNumbers, Horde, ThirdPerson mods
This commit is contained in:
1 parent
7bd44d1d99
commit
9c123865bc
176 files changed
+10367
No files matched your search
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Skopje.BossHealth
|
||||
{
|
||||
/// <summary>
|
||||
/// Look of the boss health bar. This is the file to edit.
|
||||
///
|
||||
/// Sprites are chosen by the name shown on an Image's Sprite field in the game.
|
||||
/// Leave a name empty to copy the in-game XP progress bar sprite.
|
||||
/// After changing a name, rebuild the plugin.
|
||||
/// </summary>
|
||||
public static class BossBarStyle
|
||||
{
|
||||
public const string HealthSpriteName = "";
|
||||
public const string DelaySpriteName = "";
|
||||
public const string BackgroundSpriteName = "";
|
||||
|
||||
public static readonly Color HealthColor = new Color(0.86f, 0.1f, 0.1f, 1f);
|
||||
public static readonly Color DelayColor = new Color(1f, 0.48f, 0.08f, 1f);
|
||||
public static readonly Color BackgroundColor = new Color(0.04f, 0.04f, 0.04f, 0.8f);
|
||||
public static readonly Color LabelColor = Color.white;
|
||||
|
||||
// The white bar waits this long after the last hit, then drains toward the real health.
|
||||
public const float DelaySeconds = 0.6f;
|
||||
public const float DelayDrainPerSecond = 0.75f;
|
||||
|
||||
public static readonly Vector2 BarSize = new Vector2(392f, 13f);
|
||||
public const float TopClearance = 100f;
|
||||
public const float TrackPadding = 2f;
|
||||
public const float LabelHeight = 26f;
|
||||
public const float LabelFontSize = 20f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,980 @@
|
||||
using System;
|
||||
using BepInEx;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using Doozy.Engine.UI;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using Il2CppInterop.Runtime;
|
||||
using Il2CppInterop.Runtime.Injection;
|
||||
using Skopje.Modding;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Skopje.BossHealth
|
||||
{
|
||||
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||
public class BossHealthPlugin : BasePlugin
|
||||
{
|
||||
public const string PluginGuid = "com.skopje.bosshealth";
|
||||
public const string PluginName = "Skopje Boss Health";
|
||||
public const string PluginVersion = "1.0.0";
|
||||
|
||||
internal static ManualLogSource LogSource;
|
||||
internal static bool ScanEnabled;
|
||||
|
||||
// Il2Cpp delegates are collected if nothing managed keeps them alive.
|
||||
static Il2CppSystem.Action _onClientStarted;
|
||||
static Il2CppSystem.Action _onClientStopped;
|
||||
static Il2CppSystem.Action<SkopjeGameMode> _onGameModeStarted;
|
||||
static Il2CppSystem.Action<SkopjeGameStatus> _onGameStatusChanged;
|
||||
static Il2CppSystem.Action _onGameOver;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
LogSource = Log;
|
||||
ClassInjector.RegisterTypeInIl2Cpp<BossHealthDisplay>();
|
||||
|
||||
// Client events only. A dedicated server never creates the display.
|
||||
_onClientStarted = ToIl2Cpp(new System.Action(OnClientStarted));
|
||||
_onClientStopped = ToIl2Cpp(new System.Action(OnClientStopped));
|
||||
_onGameModeStarted = ToIl2Cpp<SkopjeGameMode>(new System.Action<SkopjeGameMode>(_ => ScanEnabled = true));
|
||||
_onGameStatusChanged = ToIl2Cpp<SkopjeGameStatus>(new System.Action<SkopjeGameStatus>(OnGameStatusChanged));
|
||||
_onGameOver = ToIl2Cpp(new System.Action(() =>
|
||||
{
|
||||
ScanEnabled = false;
|
||||
BossHealthDisplay.ClearFights();
|
||||
}));
|
||||
|
||||
ModAPI.add_OnClientStarted(_onClientStarted);
|
||||
ModAPI.add_OnClientStopped(_onClientStopped);
|
||||
ModAPI.add_OnGameModeStartedClient(_onGameModeStarted);
|
||||
ModAPI.add_OnGameStatusChanged(_onGameStatusChanged);
|
||||
ModAPI.add_OnGameOver(_onGameOver);
|
||||
|
||||
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded.");
|
||||
}
|
||||
|
||||
static void OnClientStarted()
|
||||
{
|
||||
if (BossHealthDisplay.Instance != null)
|
||||
return;
|
||||
|
||||
var overlay = new GameObject("BossHealthDisplay");
|
||||
UnityEngine.Object.DontDestroyOnLoad(overlay);
|
||||
overlay.AddComponent<BossHealthDisplay>();
|
||||
LogSource.LogInfo("Boss health display created.");
|
||||
}
|
||||
|
||||
static void OnClientStopped()
|
||||
{
|
||||
ScanEnabled = false;
|
||||
if (BossHealthDisplay.Instance != null)
|
||||
UnityEngine.Object.Destroy(BossHealthDisplay.Instance.gameObject);
|
||||
}
|
||||
|
||||
static Il2CppSystem.Action ToIl2Cpp(System.Action action)
|
||||
{
|
||||
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(action);
|
||||
}
|
||||
|
||||
static Il2CppSystem.Action<T> ToIl2Cpp<T>(System.Action<T> action)
|
||||
{
|
||||
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action<T>>(action);
|
||||
}
|
||||
|
||||
static void OnGameStatusChanged(SkopjeGameStatus status)
|
||||
{
|
||||
ScanEnabled = status == SkopjeGameStatus.Active;
|
||||
if (!ScanEnabled)
|
||||
BossHealthDisplay.ClearFights();
|
||||
}
|
||||
}
|
||||
|
||||
public class BossHealthDisplay : MonoBehaviour
|
||||
{
|
||||
const float IdleScanSeconds = 2f;
|
||||
const float FightReadSeconds = 0.1f;
|
||||
|
||||
public static BossHealthDisplay Instance { get; private set; }
|
||||
|
||||
readonly Fight[] _fights = new Fight[4];
|
||||
int _fightCount;
|
||||
float _nextScanTime;
|
||||
float _nextReadTime;
|
||||
|
||||
UIView _ingameView;
|
||||
UIView _pauseView;
|
||||
UIView _mpPauseView;
|
||||
IngameMenuController _menu;
|
||||
GameObject _barRoot;
|
||||
RectTransform _barRect;
|
||||
CompassViewController _compass;
|
||||
Image _healthImage;
|
||||
Image _delayImage;
|
||||
TextMeshProUGUI _label;
|
||||
string _bossName = string.Empty;
|
||||
float _health = 1f;
|
||||
float _delay = 1f;
|
||||
float _delayWait;
|
||||
bool _hasSample;
|
||||
bool _loggedParent;
|
||||
bool _loggedMissingView;
|
||||
float _nextViewSearch;
|
||||
|
||||
public BossHealthDisplay(IntPtr pointer) : base(pointer)
|
||||
{
|
||||
}
|
||||
|
||||
public static void ClearFights()
|
||||
{
|
||||
if (Instance == null)
|
||||
return;
|
||||
Instance._fightCount = 0;
|
||||
Instance._hasSample = false;
|
||||
Instance._bossName = string.Empty;
|
||||
Instance.ApplyVisibility();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_barRoot != null)
|
||||
Destroy(_barRoot);
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!BossHealthPlugin.ScanEnabled)
|
||||
{
|
||||
_fightCount = 0;
|
||||
_hasSample = false;
|
||||
_bossName = string.Empty;
|
||||
ApplyVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
if (_fightCount == 0)
|
||||
{
|
||||
if (now >= _nextScanTime)
|
||||
{
|
||||
_nextScanTime = now + IdleScanSeconds;
|
||||
Scan();
|
||||
}
|
||||
}
|
||||
else if (now >= _nextReadTime)
|
||||
{
|
||||
_nextReadTime = now + FightReadSeconds;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
if (_fightCount > 0)
|
||||
AnimateDelay();
|
||||
PlaceBar();
|
||||
ApplyVisibility();
|
||||
}
|
||||
|
||||
void Scan()
|
||||
{
|
||||
_fightCount = 0;
|
||||
ConsiderAna();
|
||||
ConsiderVesna();
|
||||
ConsiderMetalsky();
|
||||
ConsiderTeofan();
|
||||
if (_fightCount > 0)
|
||||
{
|
||||
_nextReadTime = Time.unscaledTime + FightReadSeconds;
|
||||
if (_fights[0].StillGoing(out float ratio))
|
||||
Present(_fights[0].Name, ratio);
|
||||
}
|
||||
else
|
||||
{
|
||||
_hasSample = false;
|
||||
_bossName = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
void Refresh()
|
||||
{
|
||||
int write = 0;
|
||||
for (int i = 0; i < _fightCount; i++)
|
||||
{
|
||||
Fight fight = _fights[i];
|
||||
if (!fight.StillGoing(out _))
|
||||
continue;
|
||||
_fights[write++] = fight;
|
||||
}
|
||||
|
||||
_fightCount = write;
|
||||
if (_fightCount == 0)
|
||||
{
|
||||
_hasSample = false;
|
||||
_bossName = string.Empty;
|
||||
_nextScanTime = Time.unscaledTime + IdleScanSeconds;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fights[0].StillGoing(out float ratio))
|
||||
Present(_fights[0].Name, ratio);
|
||||
}
|
||||
|
||||
void Present(string bossName, float ratio)
|
||||
{
|
||||
EnsureBar();
|
||||
if (!_hasSample || _bossName != bossName)
|
||||
{
|
||||
_bossName = bossName;
|
||||
_health = ratio;
|
||||
_delay = ratio;
|
||||
_delayWait = 0f;
|
||||
_hasSample = true;
|
||||
}
|
||||
else if (ratio < _health - 0.001f)
|
||||
{
|
||||
_delayWait = BossBarStyle.DelaySeconds;
|
||||
_health = ratio;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ratio > _delay)
|
||||
{
|
||||
_delay = ratio;
|
||||
_delayWait = 0f;
|
||||
}
|
||||
_health = ratio;
|
||||
}
|
||||
|
||||
if (_label != null)
|
||||
_label.text = bossName;
|
||||
if (_healthImage != null)
|
||||
_healthImage.fillAmount = _health;
|
||||
if (_delayImage != null)
|
||||
_delayImage.fillAmount = _delay;
|
||||
}
|
||||
|
||||
void AnimateDelay()
|
||||
{
|
||||
if (_delayImage == null || !_hasSample)
|
||||
return;
|
||||
|
||||
if (_delayWait > 0f)
|
||||
_delayWait -= Time.unscaledDeltaTime;
|
||||
else if (_delay > _health)
|
||||
_delay = Mathf.MoveTowards(_delay, _health, BossBarStyle.DelayDrainPerSecond * Time.unscaledDeltaTime);
|
||||
|
||||
_delayImage.fillAmount = _delay;
|
||||
}
|
||||
|
||||
void ApplyVisibility()
|
||||
{
|
||||
if (_barRoot == null)
|
||||
return;
|
||||
|
||||
bool show = _fightCount > 0 && _hasSample && HudAvailable();
|
||||
if (_barRoot.activeSelf != show)
|
||||
_barRoot.SetActive(show);
|
||||
}
|
||||
|
||||
bool HudAvailable()
|
||||
{
|
||||
if (_ingameView == null || !_ingameView.gameObject.activeInHierarchy)
|
||||
return false;
|
||||
return !PauseOpen();
|
||||
}
|
||||
|
||||
bool PauseOpen()
|
||||
{
|
||||
if (ViewShown(_pauseView) || ViewShown(_mpPauseView))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_menu == null)
|
||||
return false;
|
||||
string node = _menu.ActiveNode;
|
||||
string pause = _menu.PauseNodeName;
|
||||
return !string.IsNullOrEmpty(node) && node == pause;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ViewShown(UIView view)
|
||||
{
|
||||
if (view == null || !view.gameObject.activeInHierarchy)
|
||||
return false;
|
||||
try
|
||||
{
|
||||
return view.IsVisible || view.IsShowing;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void EnsureBar()
|
||||
{
|
||||
ResolveViews();
|
||||
if (_ingameView == null)
|
||||
{
|
||||
if (!_loggedMissingView)
|
||||
{
|
||||
_loggedMissingView = true;
|
||||
BossHealthPlugin.LogSource.LogWarning("Boss bar could not find the Ingame view.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_barRoot != null)
|
||||
return;
|
||||
BuildBar();
|
||||
}
|
||||
|
||||
void ResolveViews()
|
||||
{
|
||||
if (_ingameView != null && _menu != null && _compass != null)
|
||||
return;
|
||||
if (Time.unscaledTime < _nextViewSearch)
|
||||
return;
|
||||
|
||||
_nextViewSearch = Time.unscaledTime + 2f;
|
||||
if (_ingameView == null)
|
||||
_ingameView = FindIngameView();
|
||||
if (_pauseView == null || _menu == null)
|
||||
FindPause();
|
||||
if (_compass == null)
|
||||
_compass = FindCompass();
|
||||
}
|
||||
|
||||
static UIView FindIngameView()
|
||||
{
|
||||
UIView inactive = null;
|
||||
var views = Resources.FindObjectsOfTypeAll<UIView>();
|
||||
for (int i = 0; i < views.Length; i++)
|
||||
{
|
||||
UIView view = views[i];
|
||||
if (!IsSceneObject(view) || !IsIngameView(view))
|
||||
continue;
|
||||
if (view.gameObject.activeInHierarchy)
|
||||
return view;
|
||||
inactive ??= view;
|
||||
}
|
||||
|
||||
return inactive;
|
||||
}
|
||||
|
||||
static bool IsIngameView(UIView view)
|
||||
{
|
||||
string objectName = view.name;
|
||||
if (objectName == "View - Ingame" || objectName == "View - Ingam")
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
string viewName = view.ViewName;
|
||||
return viewName == "Ingame" || viewName == "Ingam";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FindPause()
|
||||
{
|
||||
var views = Resources.FindObjectsOfTypeAll<UIView>();
|
||||
for (int i = 0; i < views.Length; i++)
|
||||
{
|
||||
UIView view = views[i];
|
||||
if (!IsSceneObject(view) || !IsPauseView(view))
|
||||
continue;
|
||||
if (_pauseView == null)
|
||||
_pauseView = view;
|
||||
else if (_mpPauseView == null && view != _pauseView)
|
||||
_mpPauseView = view;
|
||||
}
|
||||
|
||||
if (_menu != null)
|
||||
return;
|
||||
|
||||
var menus = Resources.FindObjectsOfTypeAll<IngameMenuController>();
|
||||
for (int i = 0; i < menus.Length; i++)
|
||||
{
|
||||
if (!IsSceneObject(menus[i]))
|
||||
continue;
|
||||
_menu = menus[i];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsPauseView(UIView view)
|
||||
{
|
||||
string objectName = view.name;
|
||||
if (objectName.StartsWith("View - Pause") || objectName.StartsWith("View - MPPaus"))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
string viewName = view.ViewName;
|
||||
return viewName == "Pause" || viewName == "MPPause" || viewName == "MPPaus";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsSceneObject(Component component)
|
||||
{
|
||||
return component != null && component.gameObject.scene.IsValid();
|
||||
}
|
||||
|
||||
void BuildBar()
|
||||
{
|
||||
XpProgressBar xpBar = FindXpBar();
|
||||
Image xpImage = xpBar != null ? xpBar.ProgressBar : null;
|
||||
Sprite xpSprite = xpImage != null ? xpImage.sprite : null;
|
||||
Sprite healthSprite = ResolveSprite(BossBarStyle.HealthSpriteName, xpSprite) ?? SolidSprite();
|
||||
Sprite delaySprite = ResolveSprite(BossBarStyle.DelaySpriteName, healthSprite) ?? healthSprite;
|
||||
Sprite backgroundSprite = ResolveSprite(BossBarStyle.BackgroundSpriteName, FrameSprite(xpImage, healthSprite));
|
||||
|
||||
_barRoot = new GameObject("BossHealthBar");
|
||||
_barRoot.AddComponent<RectTransform>();
|
||||
_barRoot.transform.SetParent(_ingameView.transform, false);
|
||||
|
||||
_barRect = _barRoot.GetComponent<RectTransform>();
|
||||
_barRect.anchorMin = new Vector2(0.5f, 1f);
|
||||
_barRect.anchorMax = new Vector2(0.5f, 1f);
|
||||
_barRect.pivot = new Vector2(0.5f, 1f);
|
||||
_barRect.sizeDelta = BossBarStyle.BarSize;
|
||||
_barRect.localScale = Vector3.one;
|
||||
PlaceBar();
|
||||
|
||||
Color healthColor = BossBarStyle.HealthColor;
|
||||
|
||||
CreateImage("Background", backgroundSprite, Image.Type.Simple, BossBarStyle.BackgroundColor, 0f);
|
||||
_delayImage = CreateImage("DelayFill", delaySprite, Image.Type.Filled, BossBarStyle.DelayColor, BossBarStyle.TrackPadding);
|
||||
_healthImage = CreateImage("HealthFill", healthSprite, Image.Type.Filled, healthColor, BossBarStyle.TrackPadding);
|
||||
if (xpImage != null && xpImage.material != null)
|
||||
{
|
||||
_healthImage.material = xpImage.material;
|
||||
_delayImage.material = xpImage.material;
|
||||
}
|
||||
|
||||
_healthImage.fillAmount = _health;
|
||||
_delayImage.fillAmount = _delay;
|
||||
CreateLabel(xpBar);
|
||||
_barRoot.SetActive(false);
|
||||
|
||||
_barRoot.transform.SetAsLastSibling();
|
||||
if (!_loggedParent)
|
||||
{
|
||||
_loggedParent = true;
|
||||
string spriteName = healthSprite != null ? healthSprite.name : "(solid white)";
|
||||
BossHealthPlugin.LogSource.LogInfo(
|
||||
"Boss bar parented to '" + _ingameView.name + "' using sprite '" + spriteName + "'. Edit BossBarStyle.cs to change sprites.");
|
||||
}
|
||||
}
|
||||
|
||||
Image CreateImage(string name, Sprite sprite, Image.Type type, Color color, float inset)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.AddComponent<RectTransform>();
|
||||
go.transform.SetParent(_barRoot.transform, false);
|
||||
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = new Vector2(inset, inset);
|
||||
rect.offsetMax = new Vector2(-inset, -inset);
|
||||
rect.localScale = Vector3.one;
|
||||
|
||||
Image image = go.AddComponent<Image>();
|
||||
image.sprite = sprite;
|
||||
image.type = type;
|
||||
image.color = color;
|
||||
image.raycastTarget = false;
|
||||
if (type == Image.Type.Filled)
|
||||
{
|
||||
image.fillMethod = Image.FillMethod.Horizontal;
|
||||
image.fillOrigin = (int)Image.OriginHorizontal.Left;
|
||||
image.fillAmount = 1f;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void PlaceBar()
|
||||
{
|
||||
if (_barRect == null)
|
||||
return;
|
||||
|
||||
float y = -(CompassHeight() + BossBarStyle.TopClearance);
|
||||
if (Mathf.Abs(_barRect.anchoredPosition.y - y) < 0.5f)
|
||||
return;
|
||||
_barRect.anchoredPosition = new Vector2(0f, y);
|
||||
}
|
||||
|
||||
float CompassHeight()
|
||||
{
|
||||
if (_compass == null || _barRect == null)
|
||||
return 0f;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_compass.compassEnabled)
|
||||
return 0f;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
RectTransform compass = null;
|
||||
CompassView view = _compass.CompassView;
|
||||
if (view != null)
|
||||
compass = view.Compass;
|
||||
if (compass == null && _compass.CompassGO != null)
|
||||
compass = _compass.CompassGO.GetComponent<RectTransform>();
|
||||
if (compass == null || !compass.gameObject.activeInHierarchy)
|
||||
return 0f;
|
||||
|
||||
float height = Mathf.Abs(compass.rect.height);
|
||||
if (height < 1f)
|
||||
height = Mathf.Abs(compass.sizeDelta.y);
|
||||
if (height < 1f)
|
||||
return 0f;
|
||||
|
||||
float barScale = Mathf.Abs(_barRect.lossyScale.y);
|
||||
float compassScale = Mathf.Abs(compass.lossyScale.y);
|
||||
if (barScale > 0.0001f)
|
||||
height *= compassScale / barScale;
|
||||
return height;
|
||||
}
|
||||
|
||||
static CompassViewController FindCompass()
|
||||
{
|
||||
var controllers = Resources.FindObjectsOfTypeAll<CompassViewController>();
|
||||
CompassViewController inactive = null;
|
||||
for (int i = 0; i < controllers.Length; i++)
|
||||
{
|
||||
CompassViewController controller = controllers[i];
|
||||
if (!IsSceneObject(controller))
|
||||
continue;
|
||||
if (controller.gameObject.activeInHierarchy)
|
||||
return controller;
|
||||
inactive ??= controller;
|
||||
}
|
||||
|
||||
return inactive;
|
||||
}
|
||||
|
||||
void CreateLabel(XpProgressBar xpBar)
|
||||
{
|
||||
TMP_FontAsset font = null;
|
||||
Material fontMaterial = null;
|
||||
if (xpBar != null && xpBar.LevelText != null)
|
||||
{
|
||||
font = xpBar.LevelText.font;
|
||||
fontMaterial = xpBar.LevelText.fontSharedMaterial;
|
||||
}
|
||||
|
||||
if (font == null)
|
||||
{
|
||||
var texts = Resources.FindObjectsOfTypeAll<TextMeshProUGUI>();
|
||||
for (int i = 0; i < texts.Length; i++)
|
||||
{
|
||||
TextMeshProUGUI text = texts[i];
|
||||
if (!IsSceneObject(text) || text.font == null)
|
||||
continue;
|
||||
font = text.font;
|
||||
fontMaterial = text.fontSharedMaterial;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (font == null)
|
||||
return;
|
||||
|
||||
var go = new GameObject("Label");
|
||||
go.AddComponent<RectTransform>();
|
||||
go.transform.SetParent(_barRoot.transform, false);
|
||||
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(0f, 1f);
|
||||
rect.anchorMax = new Vector2(1f, 1f);
|
||||
rect.pivot = new Vector2(0.5f, 0f);
|
||||
rect.anchoredPosition = new Vector2(0f, 2f);
|
||||
rect.sizeDelta = new Vector2(0f, BossBarStyle.LabelHeight);
|
||||
rect.localScale = Vector3.one;
|
||||
|
||||
_label = go.AddComponent<TextMeshProUGUI>();
|
||||
_label.font = font;
|
||||
if (fontMaterial != null)
|
||||
_label.fontSharedMaterial = fontMaterial;
|
||||
_label.fontSize = BossBarStyle.LabelFontSize;
|
||||
_label.alignment = TextAlignmentOptions.Center;
|
||||
_label.color = BossBarStyle.LabelColor;
|
||||
_label.raycastTarget = false;
|
||||
_label.enableWordWrapping = false;
|
||||
_label.text = _bossName;
|
||||
}
|
||||
|
||||
static XpProgressBar FindXpBar()
|
||||
{
|
||||
var bars = Resources.FindObjectsOfTypeAll<XpProgressBar>();
|
||||
XpProgressBar inactive = null;
|
||||
for (int i = 0; i < bars.Length; i++)
|
||||
{
|
||||
XpProgressBar bar = bars[i];
|
||||
if (!IsSceneObject(bar))
|
||||
continue;
|
||||
if (bar.gameObject.activeInHierarchy)
|
||||
return bar;
|
||||
inactive ??= bar;
|
||||
}
|
||||
|
||||
return inactive;
|
||||
}
|
||||
|
||||
static Sprite FrameSprite(Image xpImage, Sprite healthSprite)
|
||||
{
|
||||
if (xpImage == null || xpImage.transform.parent == null)
|
||||
return null;
|
||||
|
||||
Image frame = xpImage.transform.parent.GetComponent<Image>();
|
||||
if (frame == null || frame.sprite == null || frame.sprite == healthSprite)
|
||||
return null;
|
||||
return frame.sprite;
|
||||
}
|
||||
|
||||
static Sprite _solidSprite;
|
||||
|
||||
static Sprite SolidSprite()
|
||||
{
|
||||
if (_solidSprite != null)
|
||||
return _solidSprite;
|
||||
|
||||
var texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||||
texture.SetPixel(0, 0, Color.white);
|
||||
texture.Apply();
|
||||
_solidSprite = Sprite.Create(texture, new Rect(0, 0, 1, 1), new Vector2(0.5f, 0.5f), 1f);
|
||||
return _solidSprite;
|
||||
}
|
||||
|
||||
static Sprite ResolveSprite(string overrideName, Sprite fallback)
|
||||
{
|
||||
if (string.IsNullOrEmpty(overrideName))
|
||||
return fallback;
|
||||
|
||||
var sprites = Resources.FindObjectsOfTypeAll<Sprite>();
|
||||
for (int i = 0; i < sprites.Length; i++)
|
||||
{
|
||||
Sprite sprite = sprites[i];
|
||||
if (sprite != null && sprite.name == overrideName)
|
||||
return sprite;
|
||||
}
|
||||
|
||||
BossHealthPlugin.LogSource.LogWarning(
|
||||
"Boss bar sprite '" + overrideName + "' was not found. Using the XP bar sprite instead.");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void AddFight(Fight fight)
|
||||
{
|
||||
if (fight == null || _fightCount >= _fights.Length)
|
||||
return;
|
||||
_fights[_fightCount++] = fight;
|
||||
BossHealthPlugin.LogSource.LogInfo($"Boss fight started: {fight.Name}");
|
||||
}
|
||||
|
||||
void ConsiderAna()
|
||||
{
|
||||
var bosses = UnityEngine.Object.FindObjectsOfType<AnaBoss>();
|
||||
for (int i = 0; i < bosses.Length; i++)
|
||||
{
|
||||
AnaBoss boss = bosses[i];
|
||||
if (!IsAlive(boss) || !boss.isActiveAndEnabled)
|
||||
continue;
|
||||
if (!TryRatio(boss.EnemyControl, out _))
|
||||
continue;
|
||||
if (IsTrue(boss.Dead))
|
||||
continue;
|
||||
if (!IsTrue(boss.Visibility) && !IsTrue(boss.IsVulnerable))
|
||||
continue;
|
||||
|
||||
AddFight(new AnaFight(boss));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ConsiderVesna()
|
||||
{
|
||||
var bosses = UnityEngine.Object.FindObjectsOfType<VesnaBoss>();
|
||||
for (int i = 0; i < bosses.Length; i++)
|
||||
{
|
||||
VesnaBoss boss = bosses[i];
|
||||
if (!IsAlive(boss) || !boss.isActiveAndEnabled)
|
||||
continue;
|
||||
if (!TryRatio(boss.EnemyControl, out _))
|
||||
continue;
|
||||
if (IsTrue(boss.Dead) || !IsTrue(boss.Visibility))
|
||||
continue;
|
||||
|
||||
AddFight(new VesnaFight(boss));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ConsiderMetalsky()
|
||||
{
|
||||
var bosses = UnityEngine.Object.FindObjectsOfType<MetalskyBoss>();
|
||||
for (int i = 0; i < bosses.Length; i++)
|
||||
{
|
||||
MetalskyBoss boss = bosses[i];
|
||||
if (!IsAlive(boss) || !boss.isActiveAndEnabled)
|
||||
continue;
|
||||
if (!IsTrue(boss.BossBattleState) || SafeDead(boss))
|
||||
continue;
|
||||
if (!TryRatio(boss.PawnStatus, out _))
|
||||
continue;
|
||||
|
||||
AddFight(new MetalskyFight(boss));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ConsiderTeofan()
|
||||
{
|
||||
var brains = UnityEngine.Object.FindObjectsOfType<TeofanBossBrain>();
|
||||
for (int i = 0; i < brains.Length; i++)
|
||||
{
|
||||
TeofanBossBrain brain = brains[i];
|
||||
if (!IsAlive(brain) || !brain.isActiveAndEnabled || SafeDead(brain))
|
||||
continue;
|
||||
|
||||
EnemyControlAStar control = brain.GetComponent<EnemyControlAStar>();
|
||||
if (control == null)
|
||||
control = brain.GetComponentInParent<EnemyControlAStar>();
|
||||
if (control == null)
|
||||
control = brain.GetComponentInChildren<EnemyControlAStar>(true);
|
||||
if (!TryRatio(control, out _))
|
||||
continue;
|
||||
if (!TeofanPhaseStarted())
|
||||
continue;
|
||||
|
||||
AddFight(new TeofanFight(brain));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static bool TeofanPhaseStarted()
|
||||
{
|
||||
var arenas = UnityEngine.Object.FindObjectsOfType<TeofanBossArena>();
|
||||
if (arenas == null || arenas.Length == 0)
|
||||
return true;
|
||||
|
||||
for (int i = 0; i < arenas.Length; i++)
|
||||
{
|
||||
TeofanBossArena arena = arenas[i];
|
||||
if (!IsAlive(arena) || arena.Phase == null)
|
||||
continue;
|
||||
if (arena.Phase.Value > 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool TryRatio(EnemyControlAStar control, out float ratio)
|
||||
{
|
||||
ratio = 0f;
|
||||
if (!IsAlive(control))
|
||||
return false;
|
||||
return TryRatio(control.PawnStatus, out ratio);
|
||||
}
|
||||
|
||||
static bool TryRatio(PawnStatus status, out float ratio)
|
||||
{
|
||||
ratio = 0f;
|
||||
try
|
||||
{
|
||||
if (!IsAlive(status))
|
||||
return false;
|
||||
|
||||
float max = status.InitialHealth;
|
||||
if (max <= 0.01f)
|
||||
return false;
|
||||
|
||||
SyncVar<float> health = status.Health;
|
||||
if (health == null)
|
||||
return false;
|
||||
|
||||
ratio = Mathf.Clamp01(health.Value / max);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsTrue(SyncVar<bool> value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value != null && value.Value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsAlive(UnityEngine.Object obj)
|
||||
{
|
||||
return obj != null;
|
||||
}
|
||||
|
||||
static bool SafeDead(MetalskyBoss boss)
|
||||
{
|
||||
try
|
||||
{
|
||||
return boss.IsDead;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool SafeDead(TeofanBossBrain brain)
|
||||
{
|
||||
try
|
||||
{
|
||||
return brain.dead;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Fight
|
||||
{
|
||||
public string Name;
|
||||
|
||||
public abstract bool StillGoing(out float ratio);
|
||||
}
|
||||
|
||||
sealed class AnaFight : Fight
|
||||
{
|
||||
readonly AnaBoss _boss;
|
||||
bool _engaged;
|
||||
|
||||
public AnaFight(AnaBoss boss)
|
||||
{
|
||||
_boss = boss;
|
||||
Name = "Ana";
|
||||
_engaged = true;
|
||||
}
|
||||
|
||||
public override bool StillGoing(out float ratio)
|
||||
{
|
||||
ratio = 0f;
|
||||
if (!IsAlive(_boss))
|
||||
return false;
|
||||
if (IsTrue(_boss.Dead))
|
||||
return false;
|
||||
|
||||
// Ana disappears between attacks. Keep the counter until she is actually dead.
|
||||
if (IsTrue(_boss.Visibility) || IsTrue(_boss.IsVulnerable))
|
||||
_engaged = true;
|
||||
if (!_engaged)
|
||||
return false;
|
||||
return TryRatio(_boss.EnemyControl, out ratio);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class VesnaFight : Fight
|
||||
{
|
||||
readonly VesnaBoss _boss;
|
||||
bool _engaged;
|
||||
|
||||
public VesnaFight(VesnaBoss boss)
|
||||
{
|
||||
_boss = boss;
|
||||
Name = "Vesna";
|
||||
_engaged = true;
|
||||
}
|
||||
|
||||
public override bool StillGoing(out float ratio)
|
||||
{
|
||||
ratio = 0f;
|
||||
if (!IsAlive(_boss))
|
||||
return false;
|
||||
if (IsTrue(_boss.Dead))
|
||||
return false;
|
||||
if (IsTrue(_boss.Visibility))
|
||||
_engaged = true;
|
||||
if (!_engaged)
|
||||
return false;
|
||||
return TryRatio(_boss.EnemyControl, out ratio);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MetalskyFight : Fight
|
||||
{
|
||||
readonly MetalskyBoss _boss;
|
||||
|
||||
public MetalskyFight(MetalskyBoss boss)
|
||||
{
|
||||
_boss = boss;
|
||||
Name = "Metalsky";
|
||||
}
|
||||
|
||||
public override bool StillGoing(out float ratio)
|
||||
{
|
||||
ratio = 0f;
|
||||
if (!IsAlive(_boss) || SafeDead(_boss) || !IsTrue(_boss.BossBattleState))
|
||||
return false;
|
||||
return TryRatio(_boss.PawnStatus, out ratio);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TeofanFight : Fight
|
||||
{
|
||||
readonly TeofanBossBrain _brain;
|
||||
|
||||
public TeofanFight(TeofanBossBrain brain)
|
||||
{
|
||||
_brain = brain;
|
||||
Name = "Teofan";
|
||||
}
|
||||
|
||||
public override bool StillGoing(out float ratio)
|
||||
{
|
||||
ratio = 0f;
|
||||
if (!IsAlive(_brain) || SafeDead(_brain))
|
||||
return false;
|
||||
|
||||
EnemyControlAStar control = _brain.GetComponent<EnemyControlAStar>();
|
||||
if (control == null)
|
||||
control = _brain.GetComponentInParent<EnemyControlAStar>();
|
||||
if (control == null)
|
||||
control = _brain.GetComponentInChildren<EnemyControlAStar>(true);
|
||||
return TryRatio(control, out ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>Skopje.BossHealth</AssemblyName>
|
||||
<RootNamespace>Skopje.BossHealth</RootNamespace>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<GameDir Condition="'$(GameDir)' == ''">D:\SteamLibrary\steamapps\common\Skopje83</GameDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="BepInEx.Core">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Core.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Unity.IL2CPP">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Unity.IL2CPP.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppInterop.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\core\Il2CppInterop.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2Cppmscorlib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Il2Cppmscorlib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="SkopjeAssembly">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\SkopjeAssembly.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FishNet.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\FishNet.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.UI">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.UI.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Unity.TextMeshPro">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Unity.TextMeshPro.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="DoozyEngineAssembly">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\DoozyEngineAssembly.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Archon.SwissArmyLib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Archon.SwissArmyLib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"Skopje.BossHealth/1.0.0": {
|
||||
"runtime": {
|
||||
"Skopje.BossHealth.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Skopje.BossHealth/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj",
|
||||
"projectName": "Skopje.BossHealth",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
|
||||
@@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Skopje.BossHealth")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Skopje.BossHealth")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Skopje.BossHealth")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0acf9d2f1a6c359332339552562b4ad3180ad1d45517bc93e22ec640b2c677b6
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net6.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v6.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EntryPointFilePath =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = Skopje.BossHealth
|
||||
build_property.ProjectDir = c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 6.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
fa3b81ae49792c7e2809d45957a31d8bfffb4da7db29d70797a1fd96529ce180
|
||||
@@ -0,0 +1,12 @@
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\BossHealthPlugin.csproj.AssemblyReference.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\BossHealthPlugin.GeneratedMSBuildEditorConfig.editorconfig
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\BossHealthPlugin.AssemblyInfoInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\BossHealthPlugin.AssemblyInfo.cs
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\BossHealthPlugin.csproj.CoreCompileInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\bin\Release\net6.0\Skopje.BossHealth.deps.json
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\bin\Release\net6.0\Skopje.BossHealth.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\bin\Release\net6.0\Skopje.BossHealth.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\Skopje.BossHealth.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\refint\Skopje.BossHealth.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\Skopje.BossHealth.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\BossHealthPlugin\obj\Release\net6.0\ref\Skopje.BossHealth.dll
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net6.0": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net6.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj",
|
||||
"projectName": "Skopje.BossHealth",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "H27KblJKjqM=",
|
||||
"success": true,
|
||||
"projectFilePath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\BossHealthPlugin\\BossHealthPlugin.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.netcore.app.ref\\6.0.36\\microsoft.netcore.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.windowsdesktop.app.ref\\6.0.36\\microsoft.windowsdesktop.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.aspnetcore.app.ref\\6.0.36\\microsoft.aspnetcore.app.ref.6.0.36.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
using System;
|
||||
using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
using Il2CppInterop.Runtime.Injection;
|
||||
using Skopje.Modding;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Skopje.DamageNumbers
|
||||
{
|
||||
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||
public class DamageNumbersPlugin : BasePlugin
|
||||
{
|
||||
public const string PluginGuid = "com.skopje.damagenumbers";
|
||||
public const string PluginName = "Skopje Damage Numbers";
|
||||
public const string PluginVersion = "1.0.0";
|
||||
|
||||
internal static ManualLogSource LogSource;
|
||||
internal static bool ClientRunning;
|
||||
|
||||
internal static ConfigEntry<bool> Enabled;
|
||||
internal static ConfigEntry<bool> OnlyYourDamage;
|
||||
internal static ConfigEntry<float> Lifetime;
|
||||
internal static ConfigEntry<float> Size;
|
||||
|
||||
static Harmony _harmony;
|
||||
static SkopjePlayer _localPlayer;
|
||||
static int _errorLogs;
|
||||
static int _traceLogs;
|
||||
|
||||
const int Recent = 32;
|
||||
static readonly int[] _recentEnemy = new int[Recent];
|
||||
static readonly int[] _recentAmount = new int[Recent];
|
||||
static readonly int[] _recentCell = new int[Recent];
|
||||
static readonly int[] _recentFrame = new int[Recent];
|
||||
static int _recentWrite;
|
||||
|
||||
static Il2CppSystem.Action _onClientStarted;
|
||||
static Il2CppSystem.Action _onClientStopped;
|
||||
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStarted;
|
||||
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStopped;
|
||||
static Il2CppSystem.Action _onGameOver;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
LogSource = Log;
|
||||
Enabled = Config.Bind("Display", "Enabled", true, "Show floating damage numbers.");
|
||||
OnlyYourDamage = Config.Bind("Display", "OnlyYourDamage", true, "Only show hits dealt by your pawn. Hits with no owner still show.");
|
||||
Lifetime = Config.Bind("Display", "Lifetime", 0.8f, "Seconds each number stays on screen.");
|
||||
Size = Config.Bind("Display", "Size", 1f, "Size multiplier.");
|
||||
|
||||
ClassInjector.RegisterTypeInIl2Cpp<DamageNumbersDisplay>();
|
||||
|
||||
_onClientStarted = ToIl2Cpp(new System.Action(OnClientStarted));
|
||||
_onClientStopped = ToIl2Cpp(new System.Action(OnClientStopped));
|
||||
_onPlayerStarted = ToIl2Cpp<SkopjePlayer>(new System.Action<SkopjePlayer>(OnPlayerStarted));
|
||||
_onPlayerStopped = ToIl2Cpp<SkopjePlayer>(new System.Action<SkopjePlayer>(OnPlayerStopped));
|
||||
_onGameOver = ToIl2Cpp(new System.Action(OnGameOver));
|
||||
|
||||
ModAPI.add_OnClientStarted(_onClientStarted);
|
||||
ModAPI.add_OnClientStopped(_onClientStopped);
|
||||
ModAPI.add_OnPlayerStartedClient(_onPlayerStarted);
|
||||
ModAPI.add_OnPlayerStoppedClient(_onPlayerStopped);
|
||||
ModAPI.add_OnGameOver(_onGameOver);
|
||||
|
||||
_harmony = new Harmony(PluginGuid);
|
||||
Patch(AccessTools.Method(typeof(EnemyControlAStar), nameof(EnemyControlAStar.TakeDamage)), nameof(AfterTakeDamage));
|
||||
// Observers RPC body. Parameter names are stripped, so the postfix binds them by index.
|
||||
Patch(AccessTools.Method(typeof(EnemyControlAStar), "RpcLogic___RpcDamageTaken_3262335728"), nameof(AfterObservedDamage));
|
||||
|
||||
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded.");
|
||||
}
|
||||
|
||||
public override bool Unload()
|
||||
{
|
||||
ClientRunning = false;
|
||||
if (_harmony != null)
|
||||
_harmony.UnpatchSelf();
|
||||
DamageNumbersDisplay.DestroyHost();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Patch(System.Reflection.MethodInfo method, string postfix)
|
||||
{
|
||||
if (method == null)
|
||||
{
|
||||
LogSource.LogWarning("Damage number hook was not found: " + postfix);
|
||||
return;
|
||||
}
|
||||
|
||||
_harmony.Patch(method, postfix: new HarmonyMethod(typeof(DamageNumbersPlugin), postfix));
|
||||
}
|
||||
|
||||
static void OnClientStarted()
|
||||
{
|
||||
ClientRunning = true;
|
||||
DamageNumbersDisplay.Ensure();
|
||||
LogSource.LogInfo("Damage numbers ready.");
|
||||
}
|
||||
|
||||
static void OnClientStopped()
|
||||
{
|
||||
ClientRunning = false;
|
||||
_localPlayer = null;
|
||||
DamageNumbersDisplay.DestroyHost();
|
||||
}
|
||||
|
||||
static void OnPlayerStarted(SkopjePlayer player)
|
||||
{
|
||||
if (player != null && player.IsOwner)
|
||||
_localPlayer = player;
|
||||
}
|
||||
|
||||
static void OnPlayerStopped(SkopjePlayer player)
|
||||
{
|
||||
if (_localPlayer == player)
|
||||
_localPlayer = null;
|
||||
}
|
||||
|
||||
static void OnGameOver()
|
||||
{
|
||||
DamageNumbersDisplay.Clear();
|
||||
}
|
||||
|
||||
static void AfterTakeDamage(EnemyControlAStar __instance, HitData hitData)
|
||||
{
|
||||
try
|
||||
{
|
||||
Present(__instance, hitData, null, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static void AfterObservedDamage(
|
||||
EnemyControlAStar __instance,
|
||||
[HarmonyArgument(0)] HitData hitData,
|
||||
[HarmonyArgument(1)] DamageablePart part)
|
||||
{
|
||||
try
|
||||
{
|
||||
Present(__instance, hitData, part, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static void Present(EnemyControlAStar enemy, HitData hit, DamageablePart part, bool allowLastPart)
|
||||
{
|
||||
if (!ClientRunning || Enabled == null || !Enabled.Value)
|
||||
return;
|
||||
if (enemy == null || hit == null || hit.isDespawn)
|
||||
return;
|
||||
|
||||
DamageType type = hit.DamageType;
|
||||
if (HasFlag(type, DamageType.Enemy) || HasFlag(type, DamageType.Fall))
|
||||
return;
|
||||
if (!IsYourHit(hit))
|
||||
{
|
||||
Trace("skipped somebody else's hit");
|
||||
return;
|
||||
}
|
||||
|
||||
float raw = hit.damageAmount;
|
||||
if (float.IsNaN(raw) || float.IsInfinity(raw) || raw <= 0.01f)
|
||||
return;
|
||||
|
||||
int rawAmount = Mathf.Max(1, Mathf.RoundToInt(raw));
|
||||
Vector3 pos = HitPosition(enemy, hit);
|
||||
if (!Claim(enemy, rawAmount, pos))
|
||||
return;
|
||||
|
||||
if (part == null && allowLastPart && enemy.lastDamagedPart != null)
|
||||
part = enemy.lastDamagedPart.Value;
|
||||
|
||||
bool crit = IsCrit(part);
|
||||
int shown = ShownDamage(raw, crit);
|
||||
DamageTier.Apply(shown, out Color color, out float tierScale);
|
||||
float punch = PunchFor(rawAmount, crit, type) * tierScale;
|
||||
pos += new Vector3(
|
||||
UnityEngine.Random.Range(-0.22f, 0.22f),
|
||||
UnityEngine.Random.Range(0.05f, 0.28f),
|
||||
UnityEngine.Random.Range(-0.22f, 0.22f));
|
||||
|
||||
DamageNumbersDisplay.Ensure();
|
||||
DamageNumbersDisplay.Spawn(pos, shown, color, punch, crit);
|
||||
Trace("hit " + shown + " " + type + (crit ? " crit" : "") + " at " + pos);
|
||||
}
|
||||
|
||||
static bool IsYourHit(HitData hit)
|
||||
{
|
||||
if (OnlyYourDamage == null || !OnlyYourDamage.Value)
|
||||
return true;
|
||||
|
||||
PawnStatus owner = hit.HitOwner;
|
||||
if (owner == null)
|
||||
return true;
|
||||
|
||||
PawnStatus local = LocalPawn();
|
||||
if (local == null)
|
||||
return true;
|
||||
|
||||
return owner.Pointer == local.Pointer;
|
||||
}
|
||||
|
||||
static PawnStatus LocalPawn()
|
||||
{
|
||||
if (_localPlayer == null)
|
||||
_localPlayer = FindLocalPlayer();
|
||||
if (_localPlayer == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return _localPlayer.PawnStatus;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_localPlayer = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static SkopjePlayer FindLocalPlayer()
|
||||
{
|
||||
var players = UnityEngine.Object.FindObjectsOfType<SkopjePlayer>();
|
||||
if (players == null)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
SkopjePlayer player = players[i];
|
||||
if (player != null && player.IsOwner)
|
||||
return player;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Vector3 HitPosition(EnemyControlAStar enemy, HitData hit)
|
||||
{
|
||||
Vector3 pos = hit.hitPosition;
|
||||
if (pos.sqrMagnitude > 0.0001f)
|
||||
return pos;
|
||||
|
||||
pos = enemy.lastHitPosition;
|
||||
if (pos.sqrMagnitude > 0.0001f)
|
||||
return pos;
|
||||
|
||||
if (enemy.transform != null)
|
||||
return enemy.transform.position + Vector3.up * 1.4f;
|
||||
return Vector3.up;
|
||||
}
|
||||
|
||||
static bool IsCrit(DamageablePart part)
|
||||
{
|
||||
// part.name goes through UnityEngine.Object and can access-violate on a
|
||||
// networked part that is not a live Unity object. Weakspot fields are enough.
|
||||
if (part == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (part.WeakspotType != WeakspotType.None)
|
||||
return true;
|
||||
return part.PartDamageModifier > 1.05f;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Real hits in this game are often 1, 2, or 3. Scale them up so the popup
|
||||
// still tracks the hit, but reads like a chunky score. Crits are doubled.
|
||||
static int ShownDamage(float raw, bool crit)
|
||||
{
|
||||
float scale = crit ? 200f : 100f;
|
||||
float roll = UnityEngine.Random.Range(0.94f, 1.06f);
|
||||
return Mathf.Max(1, Mathf.RoundToInt(raw * scale * roll));
|
||||
}
|
||||
|
||||
static bool Claim(EnemyControlAStar enemy, int amount, Vector3 pos)
|
||||
{
|
||||
int id = enemy.GetInstanceID();
|
||||
int cell = (int)(pos.x * 8f) * 73856093
|
||||
^ (int)(pos.y * 8f) * 19349663
|
||||
^ (int)(pos.z * 8f) * 83492791;
|
||||
int frame = Time.frameCount;
|
||||
for (int i = 0; i < Recent; i++)
|
||||
{
|
||||
if (_recentFrame[i] < frame - 1)
|
||||
continue;
|
||||
if (_recentEnemy[i] == id && _recentAmount[i] == amount && _recentCell[i] == cell)
|
||||
return false;
|
||||
}
|
||||
|
||||
int slot = _recentWrite++ % Recent;
|
||||
if (_recentWrite < 0)
|
||||
_recentWrite = 0;
|
||||
_recentEnemy[slot] = id;
|
||||
_recentAmount[slot] = amount;
|
||||
_recentCell[slot] = cell;
|
||||
_recentFrame[slot] = frame;
|
||||
return true;
|
||||
}
|
||||
|
||||
static float PunchFor(int amount, bool crit, DamageType type)
|
||||
{
|
||||
float punch = 1f + Mathf.Clamp(Mathf.Log10(Mathf.Max(amount, 1)) * 0.14f, 0f, 0.5f);
|
||||
if (crit)
|
||||
punch *= 1.45f;
|
||||
if (HasFlag(type, DamageType.Explosion))
|
||||
punch *= 1.2f;
|
||||
return punch;
|
||||
}
|
||||
|
||||
static bool HasFlag(DamageType value, DamageType flag)
|
||||
{
|
||||
return ((int)value & (int)flag) != 0;
|
||||
}
|
||||
|
||||
static void Fail(Exception ex)
|
||||
{
|
||||
if (_errorLogs++ < 5)
|
||||
LogSource.LogWarning("Damage number failed: " + ex.Message);
|
||||
}
|
||||
|
||||
static void Trace(string message)
|
||||
{
|
||||
if (_traceLogs++ < 8)
|
||||
LogSource.LogInfo(message);
|
||||
}
|
||||
|
||||
static Il2CppSystem.Action ToIl2Cpp(System.Action action)
|
||||
{
|
||||
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(action);
|
||||
}
|
||||
|
||||
static Il2CppSystem.Action<T> ToIl2Cpp<T>(System.Action<T> action)
|
||||
{
|
||||
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action<T>>(action);
|
||||
}
|
||||
}
|
||||
|
||||
public class DamageNumbersDisplay : MonoBehaviour
|
||||
{
|
||||
const int Capacity = 48;
|
||||
|
||||
public static DamageNumbersDisplay Instance { get; private set; }
|
||||
|
||||
readonly Number[] _numbers = new Number[Capacity];
|
||||
GUIStyle _style;
|
||||
Camera _camera;
|
||||
float _nextCameraSearch;
|
||||
|
||||
public DamageNumbersDisplay(IntPtr pointer) : base(pointer)
|
||||
{
|
||||
}
|
||||
|
||||
public static void Ensure()
|
||||
{
|
||||
if (Instance != null)
|
||||
return;
|
||||
|
||||
var overlay = new GameObject("DamageNumbersDisplay");
|
||||
UnityEngine.Object.DontDestroyOnLoad(overlay);
|
||||
overlay.AddComponent<DamageNumbersDisplay>();
|
||||
}
|
||||
|
||||
public static void DestroyHost()
|
||||
{
|
||||
if (Instance != null)
|
||||
UnityEngine.Object.Destroy(Instance.gameObject);
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
if (Instance == null)
|
||||
return;
|
||||
for (int i = 0; i < Instance._numbers.Length; i++)
|
||||
Instance._numbers[i].Used = false;
|
||||
}
|
||||
|
||||
public static void Spawn(Vector3 worldPos, int amount, Color color, float punch, bool crit)
|
||||
{
|
||||
DamageNumbersDisplay display = Instance;
|
||||
if (display == null)
|
||||
return;
|
||||
|
||||
float life = 0.8f;
|
||||
if (DamageNumbersPlugin.Lifetime != null)
|
||||
life = Mathf.Clamp(DamageNumbersPlugin.Lifetime.Value, 0.25f, 2.5f);
|
||||
if (crit)
|
||||
life *= 1.15f;
|
||||
|
||||
int slot = display.FindSlot();
|
||||
display._numbers[slot] = new Number
|
||||
{
|
||||
Used = true,
|
||||
Origin = worldPos,
|
||||
Drift = new Vector3(
|
||||
UnityEngine.Random.Range(-0.55f, 0.55f),
|
||||
UnityEngine.Random.Range(crit ? 1.15f : 0.9f, crit ? 1.65f : 1.35f),
|
||||
UnityEngine.Random.Range(-0.2f, 0.2f)),
|
||||
Age = 0f,
|
||||
Life = life,
|
||||
Punch = punch,
|
||||
Tilt = UnityEngine.Random.Range(crit ? -18f : -14f, crit ? 18f : 14f),
|
||||
Amount = amount,
|
||||
Color = color,
|
||||
Text = crit ? "CRIT\n" + amount : amount.ToString()
|
||||
};
|
||||
Measure(display._numbers[slot].Text, out display._numbers[slot].Lines, out display._numbers[slot].Longest);
|
||||
}
|
||||
|
||||
static void Measure(string text, out int lines, out int longest)
|
||||
{
|
||||
lines = 1;
|
||||
longest = 0;
|
||||
int run = 0;
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (text[i] == '\n')
|
||||
{
|
||||
lines++;
|
||||
if (run > longest)
|
||||
longest = run;
|
||||
run = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
run++;
|
||||
}
|
||||
}
|
||||
|
||||
if (run > longest)
|
||||
longest = run;
|
||||
}
|
||||
|
||||
int FindSlot()
|
||||
{
|
||||
int oldest = 0;
|
||||
float oldestAge = -1f;
|
||||
for (int i = 0; i < _numbers.Length; i++)
|
||||
{
|
||||
if (!_numbers[i].Used)
|
||||
return i;
|
||||
if (_numbers[i].Age > oldestAge)
|
||||
{
|
||||
oldestAge = _numbers[i].Age;
|
||||
oldest = i;
|
||||
}
|
||||
}
|
||||
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
float step = Time.deltaTime;
|
||||
for (int i = 0; i < _numbers.Length; i++)
|
||||
{
|
||||
if (!_numbers[i].Used)
|
||||
continue;
|
||||
_numbers[i].Age += step;
|
||||
if (_numbers[i].Age >= _numbers[i].Life)
|
||||
_numbers[i].Used = false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
Camera cam = CameraNow();
|
||||
if (cam == null)
|
||||
return;
|
||||
|
||||
if (_style == null)
|
||||
{
|
||||
_style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold,
|
||||
richText = false
|
||||
};
|
||||
}
|
||||
|
||||
float size = DamageNumbersPlugin.Size != null
|
||||
? Mathf.Clamp(DamageNumbersPlugin.Size.Value, 0.25f, 4f)
|
||||
: 1f;
|
||||
|
||||
for (int i = 0; i < _numbers.Length; i++)
|
||||
{
|
||||
if (_numbers[i].Used)
|
||||
Draw(cam, _numbers[i], size);
|
||||
}
|
||||
}
|
||||
|
||||
Camera CameraNow()
|
||||
{
|
||||
if (_camera != null && _camera.isActiveAndEnabled)
|
||||
return _camera;
|
||||
if (Time.unscaledTime < _nextCameraSearch)
|
||||
return null;
|
||||
|
||||
_nextCameraSearch = Time.unscaledTime + 0.5f;
|
||||
_camera = Camera.main;
|
||||
if (_camera == null)
|
||||
_camera = UnityEngine.Object.FindObjectOfType<Camera>();
|
||||
return _camera;
|
||||
}
|
||||
|
||||
void Draw(Camera cam, Number number, float size)
|
||||
{
|
||||
float t = Mathf.Clamp01(number.Age / number.Life);
|
||||
float rise = 1f - (1f - t) * (1f - t);
|
||||
Vector3 world = number.Origin + number.Drift * rise;
|
||||
Vector3 screen = cam.WorldToScreenPoint(world);
|
||||
if (screen.z <= 0.05f)
|
||||
return;
|
||||
if (screen.x < -60f || screen.y < -60f || screen.x > Screen.width + 60f || screen.y > Screen.height + 60f)
|
||||
return;
|
||||
|
||||
float dist = Vector3.Distance(cam.transform.position, world);
|
||||
float perspective = Mathf.Clamp(8f / Mathf.Max(dist, 0.75f), 0.4f, 1.8f);
|
||||
float pop = PopScale(t);
|
||||
int font = Mathf.Clamp(Mathf.RoundToInt(36f * perspective * pop * number.Punch * size), 12, 140);
|
||||
|
||||
float alpha = t < 0.62f ? 1f : 1f - (t - 0.62f) / 0.38f;
|
||||
Color color = number.Color;
|
||||
color.a *= alpha;
|
||||
|
||||
float width = font * Mathf.Max(2, number.Longest) * 0.7f + 20f;
|
||||
float height = font * number.Lines + 12f;
|
||||
float x = screen.x - width * 0.5f;
|
||||
float y = Screen.height - screen.y - height * 0.5f;
|
||||
var rect = new Rect(x, y, width, height);
|
||||
var pivot = new Vector2(screen.x, Screen.height - screen.y);
|
||||
|
||||
Matrix4x4 matrix = GUI.matrix;
|
||||
Color previous = GUI.color;
|
||||
_style.fontSize = font;
|
||||
GUIUtility.RotateAroundPivot(number.Tilt * (1f - t), pivot);
|
||||
|
||||
float outline = Mathf.Max(1f, font / 16f);
|
||||
GUI.color = new Color(0f, 0f, 0f, color.a * 0.92f);
|
||||
GUI.Label(new Rect(rect.x - outline, rect.y, rect.width, rect.height), number.Text, _style);
|
||||
GUI.Label(new Rect(rect.x + outline, rect.y, rect.width, rect.height), number.Text, _style);
|
||||
GUI.Label(new Rect(rect.x, rect.y - outline, rect.width, rect.height), number.Text, _style);
|
||||
GUI.Label(new Rect(rect.x, rect.y + outline, rect.width, rect.height), number.Text, _style);
|
||||
GUI.color = color;
|
||||
GUI.Label(rect, number.Text, _style);
|
||||
|
||||
GUI.color = previous;
|
||||
GUI.matrix = matrix;
|
||||
}
|
||||
|
||||
static float PopScale(float t)
|
||||
{
|
||||
if (t < 0.08f)
|
||||
return Mathf.Lerp(0.35f, 1.4f, t / 0.08f);
|
||||
if (t < 0.18f)
|
||||
return Mathf.Lerp(1.4f, 1f, (t - 0.08f) / 0.1f);
|
||||
return 1f;
|
||||
}
|
||||
|
||||
struct Number
|
||||
{
|
||||
public bool Used;
|
||||
public Vector3 Origin;
|
||||
public Vector3 Drift;
|
||||
public float Age;
|
||||
public float Life;
|
||||
public float Punch;
|
||||
public float Tilt;
|
||||
public int Amount;
|
||||
public int Lines;
|
||||
public int Longest;
|
||||
public Color Color;
|
||||
public string Text;
|
||||
}
|
||||
}
|
||||
|
||||
static class DamageTier
|
||||
{
|
||||
// Chosen once per hit. Draw only reads the stored color and punch.
|
||||
static readonly Color White = new Color(1f, 1f, 1f, 1f);
|
||||
static readonly Color LightYellow = new Color(1f, 0.96f, 0.55f, 1f);
|
||||
static readonly Color LightOrange = new Color(1f, 0.74f, 0.38f, 1f);
|
||||
static readonly Color Orange = new Color(1f, 0.48f, 0.08f, 1f);
|
||||
|
||||
public static void Apply(int amount, out Color color, out float sizeScale)
|
||||
{
|
||||
if (amount >= 50)
|
||||
{
|
||||
color = Orange;
|
||||
sizeScale = 1.15f;
|
||||
return;
|
||||
}
|
||||
|
||||
sizeScale = 1f;
|
||||
if (amount >= 40)
|
||||
color = LightOrange;
|
||||
else if (amount >= 25)
|
||||
color = LightYellow;
|
||||
else
|
||||
color = White;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>Skopje.DamageNumbers</AssemblyName>
|
||||
<RootNamespace>Skopje.DamageNumbers</RootNamespace>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<GameDir Condition="'$(GameDir)' == ''">D:\SteamLibrary\steamapps\common\Skopje83</GameDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>$(GameDir)\BepInEx\core\0Harmony.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Core">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Core.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Unity.IL2CPP">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Unity.IL2CPP.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppInterop.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\core\Il2CppInterop.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2Cppmscorlib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Il2Cppmscorlib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="SkopjeAssembly">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\SkopjeAssembly.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FishNet.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\FishNet.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.IMGUIModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.IMGUIModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.TextRenderingModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.TextRenderingModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Archon.SwissArmyLib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Archon.SwissArmyLib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"Skopje.DamageNumbers/1.0.0": {
|
||||
"runtime": {
|
||||
"Skopje.DamageNumbers.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Skopje.DamageNumbers/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj",
|
||||
"projectName": "Skopje.DamageNumbers",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
|
||||
@@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Skopje.DamageNumbers")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Skopje.DamageNumbers")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Skopje.DamageNumbers")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
a6bbf0f8cbba65e2618b81c6321ac40b204cddd43585a4acfc7f7e5c36244fcc
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net6.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v6.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EntryPointFilePath =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = Skopje.DamageNumbers
|
||||
build_property.ProjectDir = c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 6.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
86ccfeda65ab3141bba0482e04b1b5751f6561288b86aad71bf1a392c7e22381
|
||||
@@ -0,0 +1,12 @@
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\DamageNumbersPlugin.csproj.AssemblyReference.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\DamageNumbersPlugin.GeneratedMSBuildEditorConfig.editorconfig
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\DamageNumbersPlugin.AssemblyInfoInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\DamageNumbersPlugin.AssemblyInfo.cs
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\DamageNumbersPlugin.csproj.CoreCompileInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\bin\Release\net6.0\Skopje.DamageNumbers.deps.json
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\bin\Release\net6.0\Skopje.DamageNumbers.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\bin\Release\net6.0\Skopje.DamageNumbers.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\Skopje.DamageNumbers.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\refint\Skopje.DamageNumbers.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\Skopje.DamageNumbers.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\DamageNumbers\obj\Release\net6.0\ref\Skopje.DamageNumbers.dll
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net6.0": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net6.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj",
|
||||
"projectName": "Skopje.DamageNumbers",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "KG8aXDVuUAU=",
|
||||
"success": true,
|
||||
"projectFilePath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\DamageNumbers\\DamageNumbersPlugin.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.netcore.app.ref\\6.0.36\\microsoft.netcore.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.windowsdesktop.app.ref\\6.0.36\\microsoft.windowsdesktop.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.aspnetcore.app.ref\\6.0.36\\microsoft.aspnetcore.app.ref.6.0.36.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BepInEx.Configuration;
|
||||
|
||||
namespace Skopje.Horde
|
||||
{
|
||||
// One row here becomes a setting on Global and on every zone.
|
||||
// Zone values are used only when that zone's Override is true.
|
||||
static class HordeConfig
|
||||
{
|
||||
internal const string PopulationKey = "PopulationMultiplier";
|
||||
internal const string StatKey = "StatMultiplier";
|
||||
|
||||
sealed class FieldDef
|
||||
{
|
||||
public string Key;
|
||||
public bool IsFloat;
|
||||
public int IntDefault = 0;
|
||||
public float FloatDefault;
|
||||
public int IntMin = 0;
|
||||
public int IntMax = 0;
|
||||
public float FloatMin;
|
||||
public float FloatMax;
|
||||
}
|
||||
|
||||
sealed class District
|
||||
{
|
||||
public string Name;
|
||||
public string[] Zones;
|
||||
|
||||
public District(string name, params string[] zones)
|
||||
{
|
||||
Name = name;
|
||||
Zones = zones;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ZoneSettings
|
||||
{
|
||||
public string District;
|
||||
public string Name;
|
||||
public ConfigEntry<bool> Override;
|
||||
public Dictionary<string, ConfigEntry<int>> Ints = new Dictionary<string, ConfigEntry<int>>();
|
||||
public Dictionary<string, ConfigEntry<float>> Floats = new Dictionary<string, ConfigEntry<float>>();
|
||||
}
|
||||
|
||||
const string Explanation =
|
||||
"Global multipliers apply to every zone whose Override is false.\n" +
|
||||
"PopulationMultiplier multiplies that zone's enemy count, remaining kills, and alive cap. Decimal, range 0.01 to 64, default 4.\n" +
|
||||
"StatMultiplier multiplies health and damage of enemies that zone spawns. Range 0.01 to 10, default 0.5.\n" +
|
||||
"Set a zone's Override to true to use that zone's own PopulationMultiplier and StatMultiplier instead of Global.";
|
||||
|
||||
static readonly FieldDef[] Fields =
|
||||
{
|
||||
new FieldDef
|
||||
{
|
||||
Key = PopulationKey,
|
||||
IsFloat = true,
|
||||
FloatDefault = 4f,
|
||||
FloatMin = 0.01f,
|
||||
FloatMax = 64f,
|
||||
},
|
||||
new FieldDef
|
||||
{
|
||||
Key = StatKey,
|
||||
IsFloat = true,
|
||||
FloatDefault = 0.5f,
|
||||
FloatMin = 0.01f,
|
||||
FloatMax = 10f,
|
||||
},
|
||||
};
|
||||
|
||||
// Neighborhoods from the game's zone table, grouped into the district that owns them.
|
||||
static readonly District[] Districts =
|
||||
{
|
||||
new District("Metalsky",
|
||||
"Kale",
|
||||
"Chair - West",
|
||||
"Chair - East",
|
||||
"Chair",
|
||||
"Zhelezara",
|
||||
"Avtokomanda",
|
||||
"Triangla"),
|
||||
new District("Vesna",
|
||||
"Vesna",
|
||||
"Karposh",
|
||||
"Kozle",
|
||||
"Debar Maalo",
|
||||
"Taftalidze",
|
||||
"Treska",
|
||||
"Stadium"),
|
||||
new District("Teofan",
|
||||
"Teofan",
|
||||
"Ostrovo",
|
||||
"Michurin",
|
||||
"Kisela Voda",
|
||||
"Kisela Voda - East",
|
||||
"Crniche",
|
||||
"Aerodrom",
|
||||
"Sandanski"),
|
||||
new District("Eva",
|
||||
"Eva"),
|
||||
new District("Ana",
|
||||
"Ana",
|
||||
"UKIM",
|
||||
"Kapishtec",
|
||||
"Gradski Dzid"),
|
||||
};
|
||||
|
||||
static readonly Dictionary<string, ConfigEntry<int>> GlobalInts = new Dictionary<string, ConfigEntry<int>>();
|
||||
static readonly Dictionary<string, ConfigEntry<float>> GlobalFloats = new Dictionary<string, ConfigEntry<float>>();
|
||||
static readonly Dictionary<string, ZoneSettings> Zones = new Dictionary<string, ZoneSettings>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static float GlobalPopulation
|
||||
{
|
||||
get { return GlobalFloats[PopulationKey].Value; }
|
||||
}
|
||||
|
||||
public static float GlobalStat
|
||||
{
|
||||
get { return GlobalFloats[StatKey].Value; }
|
||||
}
|
||||
|
||||
public static void Bind(ConfigFile config)
|
||||
{
|
||||
GlobalInts.Clear();
|
||||
GlobalFloats.Clear();
|
||||
Zones.Clear();
|
||||
|
||||
for (int i = 0; i < Fields.Length; i++)
|
||||
BindField(config, "Global", Fields[i], null, true);
|
||||
|
||||
for (int d = 0; d < Districts.Length; d++)
|
||||
{
|
||||
District district = Districts[d];
|
||||
for (int z = 0; z < district.Zones.Length; z++)
|
||||
BindZone(config, district.Name, district.Zones[z]);
|
||||
}
|
||||
}
|
||||
|
||||
public static float Population(SpawnZone zone)
|
||||
{
|
||||
return FloatSetting(zone, PopulationKey);
|
||||
}
|
||||
|
||||
public static float Stat(SpawnZone zone)
|
||||
{
|
||||
return FloatSetting(zone, StatKey);
|
||||
}
|
||||
|
||||
// Shared alive budget has to cover the largest multiplier actually in use.
|
||||
public static float BudgetPopulation()
|
||||
{
|
||||
float budget = GlobalPopulation;
|
||||
foreach (KeyValuePair<string, ZoneSettings> pair in Zones)
|
||||
{
|
||||
ZoneSettings zone = pair.Value;
|
||||
if (!zone.Override.Value)
|
||||
continue;
|
||||
|
||||
float population = zone.Floats[PopulationKey].Value;
|
||||
if (population > budget)
|
||||
budget = population;
|
||||
}
|
||||
|
||||
return budget < 0.01f ? 0.01f : budget;
|
||||
}
|
||||
|
||||
public static void CollectStats(List<float> into)
|
||||
{
|
||||
into.Clear();
|
||||
AddStat(into, GlobalStat);
|
||||
foreach (KeyValuePair<string, ZoneSettings> pair in Zones)
|
||||
{
|
||||
ZoneSettings zone = pair.Value;
|
||||
if (zone.Override.Value)
|
||||
AddStat(into, zone.Floats[StatKey].Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static int IntSetting(SpawnZone zone, string key)
|
||||
{
|
||||
ZoneSettings settings;
|
||||
if (TryZone(zone, out settings) && settings.Override.Value)
|
||||
return settings.Ints[key].Value;
|
||||
return GlobalInts[key].Value;
|
||||
}
|
||||
|
||||
public static float FloatSetting(SpawnZone zone, string key)
|
||||
{
|
||||
ZoneSettings settings;
|
||||
if (TryZone(zone, out settings) && settings.Override.Value)
|
||||
return settings.Floats[key].Value;
|
||||
return GlobalFloats[key].Value;
|
||||
}
|
||||
|
||||
public static bool TryZone(SpawnZone zone, out ZoneSettings settings)
|
||||
{
|
||||
settings = null;
|
||||
if (zone == null)
|
||||
return false;
|
||||
|
||||
string name = zone.ZoneName;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
|
||||
return Zones.TryGetValue(name.Trim(), out settings);
|
||||
}
|
||||
|
||||
static void BindZone(ConfigFile config, string district, string zone)
|
||||
{
|
||||
string section = district + "." + zone;
|
||||
var settings = new ZoneSettings
|
||||
{
|
||||
District = district,
|
||||
Name = zone,
|
||||
Override = config.Bind(section, "Override", false, (string)null),
|
||||
};
|
||||
|
||||
for (int i = 0; i < Fields.Length; i++)
|
||||
BindField(config, section, Fields[i], settings, false);
|
||||
|
||||
Zones[zone] = settings;
|
||||
}
|
||||
|
||||
static void BindField(ConfigFile config, string section, FieldDef field, ZoneSettings zone, bool global)
|
||||
{
|
||||
// The explanation is written once, above the first global setting.
|
||||
string description = global && field.Key == PopulationKey ? Explanation : null;
|
||||
ConfigDescription configDescription = field.IsFloat
|
||||
? new ConfigDescription(description, new AcceptableValueRange<float>(field.FloatMin, field.FloatMax))
|
||||
: new ConfigDescription(description, new AcceptableValueRange<int>(field.IntMin, field.IntMax));
|
||||
|
||||
if (!field.IsFloat)
|
||||
{
|
||||
ConfigEntry<int> entry = config.Bind(section, field.Key, field.IntDefault, configDescription);
|
||||
if (zone == null)
|
||||
GlobalInts[field.Key] = entry;
|
||||
else
|
||||
zone.Ints[field.Key] = entry;
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigEntry<float> floating = config.Bind(section, field.Key, field.FloatDefault, configDescription);
|
||||
if (zone == null)
|
||||
GlobalFloats[field.Key] = floating;
|
||||
else
|
||||
zone.Floats[field.Key] = floating;
|
||||
}
|
||||
|
||||
static void AddStat(List<float> into, float value)
|
||||
{
|
||||
for (int i = 0; i < into.Count; i++)
|
||||
{
|
||||
if (Math.Abs(into[i] - value) < 0.0001f)
|
||||
return;
|
||||
}
|
||||
|
||||
into.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using BepInEx;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Skopje.Horde
|
||||
{
|
||||
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||
public class HordePlugin : BasePlugin
|
||||
{
|
||||
public const string PluginGuid = "com.skopje.horde";
|
||||
public const string PluginName = "Skopje Horde";
|
||||
public const string PluginVersion = "1.0.0";
|
||||
|
||||
internal static ManualLogSource LogSource;
|
||||
|
||||
static Harmony _harmony;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
LogSource = Log;
|
||||
HordeConfig.Bind(Config);
|
||||
_harmony = new Harmony(PluginGuid);
|
||||
|
||||
Patch(AccessTools.Method(typeof(SpawnSystem), nameof(SpawnSystem.Awake)), nameof(HordePatches.AfterSpawnSystemAwake), null);
|
||||
Patch(AccessTools.Method(typeof(SpawnSystem), nameof(SpawnSystem.OnStartServer)), nameof(HordePatches.AfterSpawnSystemReady), null);
|
||||
Patch(AccessTools.Method(typeof(SpawnZone), nameof(SpawnZone.OnStartServer)), nameof(HordePatches.AfterZoneServerStart), nameof(HordePatches.BeforeZoneServerStart));
|
||||
Patch(AccessTools.Method(typeof(SpawnZone), nameof(SpawnZone.ResetZone)), null, nameof(HordePatches.BeforeZoneServerStart));
|
||||
Patch(AccessTools.Method(typeof(SpawnZone), "get_EnemiesLeft"), nameof(HordePatches.AfterReadEnemiesLeft), null);
|
||||
Patch(AccessTools.Method(typeof(SpawnZone), "set_EnemiesLeft"), null, nameof(HordePatches.BeforeWriteEnemiesLeft));
|
||||
Patch(AccessTools.Method(typeof(RunSaveManager), "SaveSpawnZones"), nameof(HordePatches.AfterSaveZones), nameof(HordePatches.BeforeSaveZones));
|
||||
Patch(AccessTools.Method(typeof(RunSaveManager), "LoadSpawnZones"), nameof(HordePatches.AfterLoadZones), nameof(HordePatches.BeforeLoadZones));
|
||||
Patch(AccessTools.Method(typeof(EnemyControlAStar), nameof(EnemyControlAStar.OnStartServer)), nameof(HordePatches.AfterEnemyServerStart), null);
|
||||
Patch(AccessTools.Method(typeof(EnemyControlAStar), nameof(EnemyControlAStar.OnStartClient)), nameof(HordePatches.AfterEnemyClientStart), null);
|
||||
Patch(AccessTools.Method(typeof(EnemyControlAStar), nameof(EnemyControlAStar.OnStopServer)), nameof(HordePatches.AfterEnemyServerStop), null);
|
||||
Patch(AccessTools.Method(typeof(PawnStatus), "get_Damage"), nameof(HordePatches.AfterReadDamage), null);
|
||||
|
||||
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Global population x{HordeConfig.GlobalPopulation}, stats {HordeConfig.GlobalStat:0%}. Config: {Config.ConfigFilePath}");
|
||||
}
|
||||
|
||||
public override bool Unload()
|
||||
{
|
||||
if (_harmony != null)
|
||||
_harmony.UnpatchSelf();
|
||||
HordePatches.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Patch(MethodInfo method, string postfix, string prefix)
|
||||
{
|
||||
if (method == null)
|
||||
{
|
||||
LogSource.LogWarning("Horde hook was not found: " + (postfix ?? prefix));
|
||||
return;
|
||||
}
|
||||
|
||||
_harmony.Patch(
|
||||
method,
|
||||
prefix: prefix == null ? null : new HarmonyMethod(typeof(HordePatches), prefix),
|
||||
postfix: postfix == null ? null : new HarmonyMethod(typeof(HordePatches), postfix));
|
||||
}
|
||||
}
|
||||
|
||||
static class HordePatches
|
||||
{
|
||||
sealed class CapState
|
||||
{
|
||||
public float Vanilla;
|
||||
public float Applied;
|
||||
public bool Captured;
|
||||
}
|
||||
|
||||
static readonly Dictionary<int, int> _vanillaInitial = new Dictionary<int, int>();
|
||||
static readonly Dictionary<int, float> _appliedPopulation = new Dictionary<int, float>();
|
||||
static readonly Dictionary<int, int> _loadedLeft = new Dictionary<int, int>();
|
||||
static readonly Dictionary<int, int> _vanillaBudget = new Dictionary<int, int>();
|
||||
static readonly Dictionary<int, SpawnZone> _trackedZones = new Dictionary<int, SpawnZone>();
|
||||
static readonly Dictionary<int, CapState> _caps = new Dictionary<int, CapState>();
|
||||
static readonly HashSet<int> _healthScaled = new HashSet<int>();
|
||||
static readonly Dictionary<int, float> _pawnStats = new Dictionary<int, float>();
|
||||
static readonly Dictionary<int, int> _pawnByEnemy = new Dictionary<int, int>();
|
||||
static readonly HashSet<string> _unknownZones = new HashSet<string>();
|
||||
static readonly List<float> _statCandidates = new List<float>();
|
||||
|
||||
static float _budgetMultiplier;
|
||||
static int _loading;
|
||||
static bool _saving;
|
||||
static bool _applying;
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
_vanillaInitial.Clear();
|
||||
_appliedPopulation.Clear();
|
||||
_loadedLeft.Clear();
|
||||
_vanillaBudget.Clear();
|
||||
_trackedZones.Clear();
|
||||
_caps.Clear();
|
||||
_healthScaled.Clear();
|
||||
_pawnStats.Clear();
|
||||
_pawnByEnemy.Clear();
|
||||
_unknownZones.Clear();
|
||||
_budgetMultiplier = 0;
|
||||
_loading = 0;
|
||||
_saving = false;
|
||||
_applying = false;
|
||||
}
|
||||
|
||||
public static void AfterSpawnSystemAwake(SpawnSystem __instance)
|
||||
{
|
||||
ScaleBudget(__instance);
|
||||
}
|
||||
|
||||
public static void AfterSpawnSystemReady(SpawnSystem __instance)
|
||||
{
|
||||
ScaleBudget(__instance);
|
||||
}
|
||||
|
||||
public static void BeforeZoneServerStart(SpawnZone __instance)
|
||||
{
|
||||
ScaleQuota(__instance);
|
||||
}
|
||||
|
||||
public static void AfterZoneServerStart(SpawnZone __instance)
|
||||
{
|
||||
if (__instance == null)
|
||||
return;
|
||||
|
||||
int id = __instance.GetInstanceID();
|
||||
if (!_loadedLeft.Remove(id, out int left) || __instance.EnemiesLeft == left)
|
||||
return;
|
||||
|
||||
_applying = true;
|
||||
try
|
||||
{
|
||||
__instance.EnemiesLeft = left;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_applying = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void BeforeSaveZones()
|
||||
{
|
||||
_saving = true;
|
||||
}
|
||||
|
||||
public static void AfterSaveZones()
|
||||
{
|
||||
_saving = false;
|
||||
}
|
||||
|
||||
public static void BeforeLoadZones()
|
||||
{
|
||||
_loading++;
|
||||
}
|
||||
|
||||
public static void AfterLoadZones()
|
||||
{
|
||||
if (_loading > 0)
|
||||
_loading--;
|
||||
|
||||
foreach (KeyValuePair<int, SpawnZone> pair in _trackedZones)
|
||||
{
|
||||
if (pair.Value != null)
|
||||
ApplyCap(pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Saves store the vanilla kill count. While a save is reading the property, report that count.
|
||||
public static void AfterReadEnemiesLeft(SpawnZone __instance, ref int __result)
|
||||
{
|
||||
if (!_saving || __instance == null)
|
||||
return;
|
||||
|
||||
int id = __instance.GetInstanceID();
|
||||
if (!_vanillaInitial.ContainsKey(id))
|
||||
return;
|
||||
|
||||
float population = AppliedPopulation(__instance);
|
||||
if (population <= 0f)
|
||||
return;
|
||||
|
||||
__result = Mathf.CeilToInt(__result / population);
|
||||
}
|
||||
|
||||
// Loaded kills are the vanilla remaining count. Stretch them to the horde quota.
|
||||
public static void BeforeWriteEnemiesLeft(SpawnZone __instance, [HarmonyArgument(0)] ref int value)
|
||||
{
|
||||
if (_applying || _loading <= 0)
|
||||
return;
|
||||
|
||||
value = ScaleCount(value, AppliedPopulation(__instance));
|
||||
if (__instance != null)
|
||||
_loadedLeft[__instance.GetInstanceID()] = value;
|
||||
}
|
||||
|
||||
public static void AfterEnemyServerStart(EnemyControlAStar __instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
Weaken(__instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HordePlugin.LogSource.LogWarning("Horde could not weaken a zone enemy: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AfterEnemyClientStart(EnemyControlAStar __instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
MatchClientHealth(__instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HordePlugin.LogSource.LogWarning("Horde could not match client health: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AfterEnemyServerStop(EnemyControlAStar __instance)
|
||||
{
|
||||
if (__instance == null)
|
||||
return;
|
||||
|
||||
int enemyId = __instance.GetInstanceID();
|
||||
_healthScaled.Remove(enemyId);
|
||||
if (_pawnByEnemy.TryGetValue(enemyId, out int pawnId))
|
||||
{
|
||||
_pawnStats.Remove(pawnId);
|
||||
_pawnByEnemy.Remove(enemyId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AfterReadDamage(PawnStatus __instance, ref float __result)
|
||||
{
|
||||
if (__instance == null)
|
||||
return;
|
||||
|
||||
if (_pawnStats.TryGetValue(__instance.GetInstanceID(), out float multiplier))
|
||||
__result *= multiplier;
|
||||
}
|
||||
|
||||
static void ScaleBudget(SpawnSystem system)
|
||||
{
|
||||
if (system == null)
|
||||
return;
|
||||
|
||||
int id = system.GetInstanceID();
|
||||
if (!_vanillaBudget.TryGetValue(id, out int vanilla))
|
||||
{
|
||||
int cap = system.EnemiesCap;
|
||||
if (cap <= 0)
|
||||
return;
|
||||
|
||||
vanilla = cap;
|
||||
_vanillaBudget[id] = vanilla;
|
||||
}
|
||||
|
||||
float budget = HordeConfig.BudgetPopulation();
|
||||
if (budget <= 0f)
|
||||
budget = 1f;
|
||||
|
||||
_budgetMultiplier = budget;
|
||||
int scaled = ScaleCount(vanilla, budget);
|
||||
if (system.EnemiesCap != scaled)
|
||||
{
|
||||
system.EnemiesCap = scaled;
|
||||
HordePlugin.LogSource.LogInfo($"Horde enemy cap {vanilla} -> {scaled}.");
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<int, SpawnZone> pair in _trackedZones)
|
||||
{
|
||||
if (pair.Value != null)
|
||||
ApplyCap(pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
static void ScaleQuota(SpawnZone zone)
|
||||
{
|
||||
if (zone == null)
|
||||
return;
|
||||
|
||||
int id = zone.GetInstanceID();
|
||||
_trackedZones[id] = zone;
|
||||
NoteZone(zone);
|
||||
|
||||
if (!_vanillaInitial.TryGetValue(id, out int vanilla))
|
||||
{
|
||||
vanilla = zone.InitialEnemies;
|
||||
_vanillaInitial[id] = vanilla;
|
||||
}
|
||||
|
||||
float population = HordeConfig.Population(zone);
|
||||
if (population <= 0f)
|
||||
population = 1f;
|
||||
|
||||
_appliedPopulation[id] = population;
|
||||
int scaled = ScaleCount(vanilla, population);
|
||||
if (zone.InitialEnemies != scaled)
|
||||
zone.InitialEnemies = scaled;
|
||||
|
||||
ApplyCap(zone);
|
||||
}
|
||||
|
||||
static float AppliedPopulation(SpawnZone zone)
|
||||
{
|
||||
if (zone != null && _appliedPopulation.TryGetValue(zone.GetInstanceID(), out float applied) && applied > 0f)
|
||||
return applied;
|
||||
|
||||
float population = HordeConfig.Population(zone);
|
||||
return population <= 0f ? 1f : population;
|
||||
}
|
||||
|
||||
static int ScaleCount(int count, float multiplier)
|
||||
{
|
||||
if (count <= 0 || multiplier <= 0f)
|
||||
return 0;
|
||||
|
||||
return Mathf.Max(0, Mathf.RoundToInt(count * multiplier));
|
||||
}
|
||||
|
||||
// The shared cap is one number. A zone's own share is its multiplier over that shared budget.
|
||||
static void ApplyCap(SpawnZone zone)
|
||||
{
|
||||
float population = HordeConfig.Population(zone);
|
||||
if (population <= 0f)
|
||||
population = 1f;
|
||||
|
||||
float budget = _budgetMultiplier > 0f ? _budgetMultiplier : HordeConfig.BudgetPopulation();
|
||||
if (budget <= 0f)
|
||||
budget = 1f;
|
||||
|
||||
int id = zone.GetInstanceID();
|
||||
if (!_caps.TryGetValue(id, out CapState state))
|
||||
{
|
||||
state = new CapState();
|
||||
_caps[id] = state;
|
||||
}
|
||||
|
||||
float current = zone.CapMultiplier;
|
||||
if (!state.Captured)
|
||||
{
|
||||
state.Vanilla = current;
|
||||
state.Captured = true;
|
||||
}
|
||||
else if (Mathf.Abs(current - state.Applied) > 0.0001f)
|
||||
{
|
||||
state.Vanilla = current;
|
||||
}
|
||||
|
||||
float next = state.Vanilla * population / budget;
|
||||
state.Applied = next;
|
||||
if (Mathf.Abs(current - next) > 0.0001f)
|
||||
zone.CapMultiplier = next;
|
||||
}
|
||||
|
||||
static void NoteZone(SpawnZone zone)
|
||||
{
|
||||
if (HordeConfig.TryZone(zone, out _))
|
||||
return;
|
||||
|
||||
string name = zone.ZoneName;
|
||||
if (string.IsNullOrEmpty(name) || !_unknownZones.Add(name))
|
||||
return;
|
||||
|
||||
HordePlugin.LogSource.LogInfo("Horde has no config entry for zone '" + name + "'. Using global multipliers.");
|
||||
}
|
||||
|
||||
static void Weaken(EnemyControlAStar enemy)
|
||||
{
|
||||
if (enemy == null || enemy.SpawnZone == null)
|
||||
return;
|
||||
|
||||
PawnStatus status = enemy.PawnStatus;
|
||||
if (status == null)
|
||||
return;
|
||||
|
||||
int enemyId = enemy.GetInstanceID();
|
||||
if (!_healthScaled.Add(enemyId))
|
||||
return;
|
||||
|
||||
float stat = HordeConfig.Stat(enemy.SpawnZone);
|
||||
if (Mathf.Abs(stat - 1f) < 0.0001f)
|
||||
{
|
||||
_healthScaled.Remove(enemyId);
|
||||
return;
|
||||
}
|
||||
|
||||
status.InitialHealth *= stat;
|
||||
|
||||
SyncVar<float> health = status.Health;
|
||||
if (health != null)
|
||||
health.Value *= stat;
|
||||
|
||||
ScaleParts(enemy.damageableParts, stat);
|
||||
|
||||
int pawnId = status.GetInstanceID();
|
||||
_pawnStats[pawnId] = stat;
|
||||
_pawnByEnemy[enemyId] = pawnId;
|
||||
}
|
||||
|
||||
// The zone reference is not networked. A client sees the replicated health already
|
||||
// halved and has to lower the local max so the bar still starts full.
|
||||
static void MatchClientHealth(EnemyControlAStar enemy)
|
||||
{
|
||||
if (enemy == null)
|
||||
return;
|
||||
|
||||
int enemyId = enemy.GetInstanceID();
|
||||
if (_healthScaled.Contains(enemyId))
|
||||
return;
|
||||
|
||||
PawnStatus status = enemy.PawnStatus;
|
||||
if (status == null)
|
||||
return;
|
||||
|
||||
float stat;
|
||||
if (enemy.SpawnZone != null)
|
||||
{
|
||||
stat = HordeConfig.Stat(enemy.SpawnZone);
|
||||
}
|
||||
else
|
||||
{
|
||||
SyncVar<float> health = status.Health;
|
||||
float initial = status.InitialHealth;
|
||||
if (health == null || initial <= 1f || !TryMatchStat(health.Value, initial, out stat))
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mathf.Abs(stat - 1f) < 0.0001f)
|
||||
return;
|
||||
|
||||
_healthScaled.Add(enemyId);
|
||||
status.InitialHealth *= stat;
|
||||
}
|
||||
|
||||
static bool TryMatchStat(float health, float initial, out float multiplier)
|
||||
{
|
||||
multiplier = 1f;
|
||||
HordeConfig.CollectStats(_statCandidates);
|
||||
float best = 0.05f;
|
||||
bool found = false;
|
||||
for (int i = 0; i < _statCandidates.Count; i++)
|
||||
{
|
||||
float candidate = _statCandidates[i];
|
||||
float delta = Mathf.Abs(health - initial * candidate);
|
||||
if (delta > best)
|
||||
continue;
|
||||
|
||||
best = delta;
|
||||
multiplier = candidate;
|
||||
found = true;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
static void ScaleParts(Il2CppReferenceArray<DamageablePart> parts, float stat)
|
||||
{
|
||||
if (parts == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
DamageablePart part = parts[i];
|
||||
if (part == null || part.MaxHealth <= 0f)
|
||||
continue;
|
||||
|
||||
part.MaxHealth *= stat;
|
||||
SyncVar<float> current = part.CurrentHealth;
|
||||
if (current != null)
|
||||
current.Value *= stat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>Skopje.Horde</AssemblyName>
|
||||
<RootNamespace>Skopje.Horde</RootNamespace>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<GameDir Condition="'$(GameDir)' == ''">D:\SteamLibrary\steamapps\common\Skopje83</GameDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>$(GameDir)\BepInEx\core\0Harmony.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Core">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Core.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Unity.IL2CPP">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Unity.IL2CPP.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppInterop.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\core\Il2CppInterop.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2Cppmscorlib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Il2Cppmscorlib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="SkopjeAssembly">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\SkopjeAssembly.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FishNet.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\FishNet.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Archon.SwissArmyLib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Archon.SwissArmyLib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"Skopje.Horde/1.0.0": {
|
||||
"runtime": {
|
||||
"Skopje.Horde.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Skopje.Horde/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,230 @@
|
||||
## Settings file was created by plugin Skopje Horde v1.0.0
|
||||
## Plugin GUID: com.skopje.horde
|
||||
##
|
||||
## Global multipliers apply to every zone whose Override is false.
|
||||
## PopulationMultiplier multiplies that zone's enemy count, remaining kills, and alive cap. Decimal, range 0.01 to 64, default 4.
|
||||
## StatMultiplier multiplies health and damage of enemies that zone spawns. Range 0.01 to 10, default 0.5.
|
||||
## Set a zone's Override to true to use that zone's own PopulationMultiplier and StatMultiplier instead of Global.
|
||||
|
||||
[Global]
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Kale]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Chair - West]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Chair - East]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Chair]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Zhelezara]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Avtokomanda]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Metalsky.Triangla]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Vesna]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Karposh]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Kozle]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Debar Maalo]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Taftalidze]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Treska]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Vesna.Stadium]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Teofan]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Ostrovo]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Michurin]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Kisela Voda]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Kisela Voda - East]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Crniche]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Aerodrom]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Teofan.Sandanski]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Eva.Eva]
|
||||
|
||||
Override = true
|
||||
|
||||
PopulationMultiplier = 1
|
||||
|
||||
StatMultiplier = 1
|
||||
|
||||
[Ana.Ana]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Ana.UKIM]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Ana.Kapishtec]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
[Ana.Gradski Dzid]
|
||||
|
||||
Override = false
|
||||
|
||||
PopulationMultiplier = 4
|
||||
|
||||
StatMultiplier = 0.5
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj",
|
||||
"projectName": "Skopje.Horde",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
|
||||
@@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Skopje.Horde")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Skopje.Horde")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Skopje.Horde")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
78354d0dbe9c37cdeff6d330f786856a0657639466ace854ec971ba17a86c0d3
|
||||
@@ -0,0 +1,18 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net6.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v6.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EntryPointFilePath =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = Skopje.Horde
|
||||
build_property.ProjectDir = c:\Users\andre\Desktop\SkopjeMods\Horde\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 6.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
c7952710e69e753726d2741a7baa2fed13a39f80a1316f04ebf980f3f88f4dfc
|
||||
@@ -0,0 +1,12 @@
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\HordePlugin.csproj.AssemblyReference.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\HordePlugin.GeneratedMSBuildEditorConfig.editorconfig
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\HordePlugin.AssemblyInfoInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\HordePlugin.AssemblyInfo.cs
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\HordePlugin.csproj.CoreCompileInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\bin\Release\net6.0\Skopje.Horde.deps.json
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\bin\Release\net6.0\Skopje.Horde.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\bin\Release\net6.0\Skopje.Horde.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\Skopje.Horde.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\refint\Skopje.Horde.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\Skopje.Horde.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\Horde\obj\Release\net6.0\ref\Skopje.Horde.dll
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net6.0": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net6.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj",
|
||||
"projectName": "Skopje.Horde",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "6A7ZxIjC660=",
|
||||
"success": true,
|
||||
"projectFilePath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\Horde\\HordePlugin.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.netcore.app.ref\\6.0.36\\microsoft.netcore.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.windowsdesktop.app.ref\\6.0.36\\microsoft.windowsdesktop.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.aspnetcore.app.ref\\6.0.36\\microsoft.aspnetcore.app.ref.6.0.36.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
# Skopje modding (BepInEx 6 IL2CPP)
|
||||
|
||||
This game loads [BepInEx 6](https://docs.bepinex.dev/) plugins for Unity **IL2CPP**. Official hooks live in the interop assembly **`SkopjeAssembly`**, namespace `Skopje.Modding`.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Item | Value |
|
||||
|------|--------|
|
||||
| Unity (game build) | 2022.3.62 |
|
||||
| Loader | BepInEx **6** for Unity **IL2CPP** |
|
||||
| Game assembly to reference | `SkopjeAssembly` (from a built game’s Il2CppInterop / dummy assemblies) |
|
||||
| Hook API | `Skopje.Modding.ModAPI` |
|
||||
|
||||
## Install layout
|
||||
|
||||
Install BepInEx next to the game executable (same folder as the `.exe`). Typical layout:
|
||||
|
||||
```text
|
||||
GameRoot/
|
||||
Game.exe
|
||||
winhttp.dll # Doorstop (Windows)
|
||||
doorstop_config.ini
|
||||
BepInEx/
|
||||
core/
|
||||
config/ # Local settings — NOT compared on join
|
||||
plugins/ # Your mod DLLs (+ shared data files)
|
||||
patchers/ # Optional patcher DLLs (+ shared data files)
|
||||
LogOutput.log
|
||||
```
|
||||
|
||||
Put gameplay plugins in `BepInEx/plugins/`. Shared override tables (JSON/CFG/etc.) must sit under `plugins/` or `patchers/` next to the DLL, **not** under `BepInEx/config`.
|
||||
|
||||
## Minimal plugin
|
||||
|
||||
Subscribe in `BasePlugin.Load` with `+=` (plugins load before the first scene):
|
||||
|
||||
```csharp
|
||||
using BepInEx;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using Skopje.Modding;
|
||||
|
||||
[BepInPlugin("com.example.skopje.hello", "Hello Skopje", "1.0.0")]
|
||||
public class HelloPlugin : BasePlugin
|
||||
{
|
||||
public override void Load()
|
||||
{
|
||||
ModAPI.OnServerStarted += () => Log.LogInfo("Server started");
|
||||
ModAPI.OnGameModeStartedServer += gm => Log.LogInfo($"GameMode server: {gm}");
|
||||
ModAPI.OnModCheckFailed += msg => Log.LogError(msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A full logging sample lives in [`Modding/SamplePlugin/`](../Modding/SamplePlugin/).
|
||||
|
||||
## ModAPI events
|
||||
|
||||
| Event | Where it runs | Notes |
|
||||
|-------|----------------|-------|
|
||||
| `OnServerStarted` / `OnServerStopped` | Dedicated server, listen host | FishNet local server state |
|
||||
| `OnClientStarted` / `OnClientStopped` | Pure client, listen host | FishNet local client state |
|
||||
| `OnPlayerStartedServer` / `OnPlayerStoppedServer` | Server / host | `SkopjePlayer` |
|
||||
| `OnPlayerStartedClient` / `OnPlayerStoppedClient` | Clients / host | `SkopjePlayer` |
|
||||
| `OnGameModeStartedServer` | Server / host | After `SkopjeGameMode.OnStartServer` |
|
||||
| `OnGameModeStartedClient` | Clients / host | After `SkopjeGameMode.OnStartClient` |
|
||||
| `OnGameStatusChanged` | Server/host and client-only | `NotStarted`, `Active`, `GameOver` |
|
||||
| `OnGameOver` | Same peers as status | When status becomes `GameOver` |
|
||||
| `OnNewDay` / `OnDay` / `OnNight` | Server and clients | Day/night cycle |
|
||||
| `OnModCheckFailed` | Local client | Join rejected; message lists path diffs |
|
||||
|
||||
A **listen host** raises both server and client events. Prefer **server** events for gameplay changes so dedicated servers work.
|
||||
|
||||
### ScriptableObject lookup helpers
|
||||
|
||||
```csharp
|
||||
if (ModAPI.TryGetGuidObject(123456, out GuidScriptableObject obj)) { /* ... */ }
|
||||
if (ModAPI.TryGetGuidObject<WeaponData>(123456, out var weapon)) { /* mutate fields */ }
|
||||
```
|
||||
|
||||
Call these after `OnServerStarted` or `OnGameModeStartedServer` so the GUID database is registered.
|
||||
|
||||
## ScriptableObject / item stat overrides
|
||||
|
||||
Items and many definitions are baked `ScriptableObject`s keyed by integer `Guid` in `GuidScriptableObjectDBManager`.
|
||||
|
||||
**Supported today**
|
||||
|
||||
- Mutate **public fields** on existing instances (`ItemData`, `WeaponData`, `UtilityData`, …) at runtime after the DB is ready.
|
||||
- Same plugin must run on the **dedicated server and every client** for gameplay stats.
|
||||
|
||||
**Not supported yet**
|
||||
|
||||
- Adding brand-new items/recipes as new assets.
|
||||
- Replacing loose `.asset` files on disk (IL2CPP does not load those as Mono would).
|
||||
|
||||
Example pattern (also commented in the sample plugin):
|
||||
|
||||
```csharp
|
||||
ModAPI.OnGameModeStartedServer += _ =>
|
||||
{
|
||||
if (ModAPI.TryGetGuidObject<WeaponData>(YOUR_GUID, out var weapon))
|
||||
{
|
||||
// weapon.SomePublicStatField = newValue;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Multiplayer contract
|
||||
|
||||
- FishNet is **server-authoritative**. Client-only stat changes do not define combat/inventory outcomes.
|
||||
- Gameplay mods must be **identical** on the host/dedicated server and every joining client.
|
||||
- Client-only mods (UI, camera, cosmetics) should subscribe only to **client** events and must not change server-owned results.
|
||||
- Put shared override data under `BepInEx/plugins/` (or `patchers/`) so the join check can see it. Local keybinds and BepInEx settings stay in `BepInEx/config/` and are **not** compared.
|
||||
|
||||
## Manifest join check
|
||||
|
||||
Before a remote client is authenticated, the game hashes:
|
||||
|
||||
- `BepInEx/plugins/**` and `BepInEx/patchers/**`
|
||||
- Extensions: `.dll`, `.json`, `.cfg`, `.txt`, `.xml`
|
||||
|
||||
Relative paths (forward slashes) + SHA-256 must match the server’s install. If `BepInEx` is missing on both sides, both look vanilla and match.
|
||||
|
||||
**Not hashed:** `BepInEx/core`, `BepInEx/config`, logs.
|
||||
|
||||
**Note:** A DLL present under `plugins/` still counts even if disabled in BepInEx config. Remove or rename the file to drop it from the check.
|
||||
|
||||
On mismatch the client is disconnected, `ModAPI.OnModCheckFailed` runs with a detail string, and a Doozy game event named **`ModCheckFailed`** is sent (UI can listen for that name).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | What to check |
|
||||
|---------|----------------|
|
||||
| Join rejected / disconnect | Log message and `OnModCheckFailed`; compare listed missing/extra/changed paths under `plugins`/`patchers` |
|
||||
| Plugin never loads | BepInEx 6 IL2CPP pack; `LogOutput.log`; Doorstop `winhttp.dll` next to the exe |
|
||||
| Overrides work solo but not in MP | Same plugin + sidecar files on **server and clients**; use server hooks for gameplay |
|
||||
| Different stats with “matching” mods | Shared data must live under `plugins/`, not only in `BepInEx/config/` |
|
||||
| Disabled mod still blocks join | File still hashed; remove the DLL/data file from `plugins` |
|
||||
|
||||
Logs: `BepInEx/LogOutput.log` and the Unity player log.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Sample BepInEx plugin
|
||||
|
||||
Minimal BepInEx **6 IL2CPP** plugin that logs `Skopje.Modding.ModAPI` events and shows a commented ScriptableObject override pattern.
|
||||
|
||||
Full guide: [`Docs/Modding.md`](../../Docs/Modding.md).
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install BepInEx 6 IL2CPP for the game build.
|
||||
2. Generate or copy Il2CppInterop assemblies from a run of that build (BepInEx `interop` / unhollowed output — folder names vary by pack).
|
||||
3. Edit [`SamplePlugin.csproj`](SamplePlugin.csproj):
|
||||
- Set `GameInteropDir` to that assemblies folder (must contain `SkopjeAssembly.dll` and BepInEx / Il2Cpp references as needed).
|
||||
4. `dotnet build -c Release`
|
||||
5. Copy `bin/Release/net6.0/Skopje.SamplePlugin.dll` to `BepInEx/plugins/`.
|
||||
|
||||
Gameplay mods that change stats must be installed on the dedicated server and every client. Shared JSON/CFG override files belong next to the DLL under `plugins/`, not in `BepInEx/config/`.
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using BepInEx;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using Skopje.Modding;
|
||||
|
||||
namespace Skopje.SamplePlugin
|
||||
{
|
||||
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||
public class SamplePlugin : BasePlugin
|
||||
{
|
||||
public const string PluginGuid = "com.skopje.sampleplugin";
|
||||
public const string PluginName = "Skopje Sample Plugin";
|
||||
public const string PluginVersion = "1.0.0";
|
||||
|
||||
static ManualLogSource LogSource;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
LogSource = Log;
|
||||
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Subscribe to ModAPI from Load().");
|
||||
|
||||
ModAPI.OnServerStarted += () => LogSource.LogInfo("[ModAPI] OnServerStarted");
|
||||
ModAPI.OnServerStopped += () => LogSource.LogInfo("[ModAPI] OnServerStopped");
|
||||
ModAPI.OnClientStarted += () => LogSource.LogInfo("[ModAPI] OnClientStarted");
|
||||
ModAPI.OnClientStopped += () => LogSource.LogInfo("[ModAPI] OnClientStopped");
|
||||
|
||||
ModAPI.OnPlayerStartedServer += player =>
|
||||
LogSource.LogInfo($"[ModAPI] OnPlayerStartedServer idx={player?.PlayerIdx}");
|
||||
ModAPI.OnPlayerStoppedServer += player =>
|
||||
LogSource.LogInfo($"[ModAPI] OnPlayerStoppedServer idx={player?.PlayerIdx}");
|
||||
ModAPI.OnPlayerStartedClient += player =>
|
||||
LogSource.LogInfo($"[ModAPI] OnPlayerStartedClient idx={player?.PlayerIdx}");
|
||||
ModAPI.OnPlayerStoppedClient += player =>
|
||||
LogSource.LogInfo($"[ModAPI] OnPlayerStoppedClient idx={player?.PlayerIdx}");
|
||||
|
||||
ModAPI.OnGameModeStartedServer += OnGameModeStartedServer;
|
||||
ModAPI.OnGameModeStartedClient += gm =>
|
||||
LogSource.LogInfo($"[ModAPI] OnGameModeStartedClient scene={gm?.ActiveSceneName}");
|
||||
ModAPI.OnGameStatusChanged += status =>
|
||||
LogSource.LogInfo($"[ModAPI] OnGameStatusChanged {status}");
|
||||
ModAPI.OnGameOver += () => LogSource.LogInfo("[ModAPI] OnGameOver");
|
||||
ModAPI.OnNewDay += () => LogSource.LogInfo("[ModAPI] OnNewDay");
|
||||
ModAPI.OnDay += () => LogSource.LogInfo("[ModAPI] OnDay");
|
||||
ModAPI.OnNight += () => LogSource.LogInfo("[ModAPI] OnNight");
|
||||
|
||||
ModAPI.OnModCheckFailed += message =>
|
||||
LogSource.LogError($"[ModAPI] OnModCheckFailed\n{message}");
|
||||
}
|
||||
|
||||
static void OnGameModeStartedServer(SkopjeGameMode gameMode)
|
||||
{
|
||||
LogSource.LogInfo($"[ModAPI] OnGameModeStartedServer scene={gameMode?.ActiveSceneName}");
|
||||
|
||||
// Example: mutate an existing baked ScriptableObject after the GUID DB is ready.
|
||||
// Replace YOUR_ITEM_GUID with a real Guid from the game’s ScriptableObject assets.
|
||||
// Gameplay overrides must run on the dedicated server and every client.
|
||||
//
|
||||
// const int YourItemGuid = 0;
|
||||
// if (ModAPI.TryGetGuidObject<ItemData>(YourItemGuid, out ItemData item))
|
||||
// {
|
||||
// item.ItemStackSize = 99;
|
||||
// LogSource.LogInfo($"Overrode stack size for {item.name}");
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>Skopje.SamplePlugin</AssemblyName>
|
||||
<RootNamespace>Skopje.SamplePlugin</RootNamespace>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<!-- Point this at the game's BepInEx interop / unhollowed assembly folder after a run. -->
|
||||
<GameInteropDir Condition="'$(GameInteropDir)' == ''">$(MSBuildThisFileDirectory)..\..\GameInterop</GameInteropDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="BepInEx.Core">
|
||||
<HintPath>$(GameInteropDir)\BepInEx.Core.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Unity.IL2CPP">
|
||||
<HintPath>$(GameInteropDir)\BepInEx.Unity.IL2CPP.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppInterop.Runtime">
|
||||
<HintPath>$(GameInteropDir)\Il2CppInterop.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="SkopjeAssembly">
|
||||
<HintPath>$(GameInteropDir)\SkopjeAssembly.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(GameInteropDir)\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using Cinemachine;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime;
|
||||
using Il2CppInterop.Runtime.Injection;
|
||||
using Skopje.Modding;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Skopje.ThirdPerson
|
||||
{
|
||||
[BepInPlugin(PluginGuid, PluginName, PluginVersion)]
|
||||
public class ThirdPersonPlugin : BasePlugin
|
||||
{
|
||||
public const string PluginGuid = "com.skopje.thirdperson";
|
||||
public const string PluginName = "Skopje Third Person";
|
||||
public const string PluginVersion = "1.0.0";
|
||||
|
||||
internal static ManualLogSource LogSource;
|
||||
|
||||
internal static ConfigEntry<bool> Enabled;
|
||||
internal static ConfigEntry<bool> FaceMovement;
|
||||
internal static ConfigEntry<float> Distance;
|
||||
internal static ConfigEntry<float> Shoulder;
|
||||
internal static ConfigEntry<float> Height;
|
||||
internal static ConfigEntry<float> PivotHeight;
|
||||
internal static ConfigEntry<float> AdsDistance;
|
||||
internal static ConfigEntry<float> AdsShoulder;
|
||||
internal static ConfigEntry<float> FovBonus;
|
||||
internal static ConfigEntry<float> AimDistance;
|
||||
|
||||
static Harmony _harmony;
|
||||
static GameObject _driverHost;
|
||||
|
||||
static Il2CppSystem.Action _onClientStarted;
|
||||
static Il2CppSystem.Action _onClientStopped;
|
||||
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStarted;
|
||||
static Il2CppSystem.Action<SkopjePlayer> _onPlayerStopped;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
LogSource = Log;
|
||||
Enabled = Config.Bind("Camera", "Enabled", true, "Over-the-shoulder camera on the right. The body keeps its own facing so the camera can orbit around to the face.");
|
||||
FaceMovement = Config.Bind("Camera", "FaceMovement", false, "Turn the body toward the direction you move. Leave this off to keep the body still while the camera orbits.");
|
||||
Distance = Config.Bind("Camera", "Distance", 2.7f, "Meters behind the aim axis.");
|
||||
Shoulder = Config.Bind("Camera", "Shoulder", 0.62f, "Meters to the right of the aim axis.");
|
||||
Height = Config.Bind("Camera", "Height", 0.22f, "Meters above the pivot.");
|
||||
PivotHeight = Config.Bind("Camera", "PivotHeight", 1.25f, "Orbit height above the feet, before crouch.");
|
||||
AdsDistance = Config.Bind("Camera", "AdsDistance", 1.55f, "Distance while aiming down sights.");
|
||||
AdsShoulder = Config.Bind("Camera", "AdsShoulder", 0.4f, "Right offset while aiming down sights.");
|
||||
FovBonus = Config.Bind("Camera", "FovBonus", 8f, "Extra field of view. Drops to zero while aiming.");
|
||||
AimDistance = Config.Bind("Camera", "AimDistance", 80f, "Crosshair trace length when nothing is under the reticle.");
|
||||
|
||||
ClassInjector.RegisterTypeInIl2Cpp<ThirdPersonDriver>();
|
||||
|
||||
_onClientStarted = ToIl2Cpp(new System.Action(ThirdPersonRig.OnClientStarted));
|
||||
_onClientStopped = ToIl2Cpp(new System.Action(ThirdPersonRig.OnClientStopped));
|
||||
_onPlayerStarted = ToIl2Cpp<SkopjePlayer>(new System.Action<SkopjePlayer>(ThirdPersonRig.OnPlayerStarted));
|
||||
_onPlayerStopped = ToIl2Cpp<SkopjePlayer>(new System.Action<SkopjePlayer>(ThirdPersonRig.OnPlayerStopped));
|
||||
|
||||
ModAPI.add_OnClientStarted(_onClientStarted);
|
||||
ModAPI.add_OnClientStopped(_onClientStopped);
|
||||
ModAPI.add_OnPlayerStartedClient(_onPlayerStarted);
|
||||
ModAPI.add_OnPlayerStoppedClient(_onPlayerStopped);
|
||||
|
||||
_driverHost = new GameObject("SkopjeThirdPerson");
|
||||
UnityEngine.Object.DontDestroyOnLoad(_driverHost);
|
||||
_driverHost.hideFlags = HideFlags.HideAndDontSave;
|
||||
_driverHost.AddComponent<ThirdPersonDriver>();
|
||||
|
||||
_harmony = new Harmony(PluginGuid);
|
||||
Patch(AccessTools.Method(typeof(SkopjeFPSCamera), "LateUpdate"), nameof(ThirdPersonPatches.AfterCamera));
|
||||
Patch(AccessTools.Method(typeof(CinemachineBrain), "LateUpdate"), nameof(ThirdPersonPatches.AfterBrain));
|
||||
Patch(AccessTools.Method(typeof(SkopjeCharacterController), "UpdateRotation"), nameof(ThirdPersonPatches.AfterBodyRotation), nameof(ThirdPersonPatches.BeforeBodyRotation));
|
||||
|
||||
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Config: {Config.ConfigFilePath}");
|
||||
}
|
||||
|
||||
public override bool Unload()
|
||||
{
|
||||
if (_harmony != null)
|
||||
_harmony.UnpatchSelf();
|
||||
ThirdPersonRig.Shutdown();
|
||||
if (_driverHost != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(_driverHost);
|
||||
_driverHost = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Patch(MethodInfo method, string postfix, string prefix = null)
|
||||
{
|
||||
if (method == null)
|
||||
{
|
||||
LogSource.LogWarning("Third person hook was not found: " + (postfix ?? prefix));
|
||||
return;
|
||||
}
|
||||
|
||||
_harmony.Patch(
|
||||
method,
|
||||
prefix: prefix == null ? null : new HarmonyMethod(typeof(ThirdPersonPatches), prefix),
|
||||
postfix: postfix == null ? null : new HarmonyMethod(typeof(ThirdPersonPatches), postfix));
|
||||
}
|
||||
|
||||
static Il2CppSystem.Action ToIl2Cpp(System.Action action)
|
||||
{
|
||||
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action>(action);
|
||||
}
|
||||
|
||||
static Il2CppSystem.Action<T> ToIl2Cpp<T>(System.Action<T> action)
|
||||
{
|
||||
return DelegateSupport.ConvertDelegate<Il2CppSystem.Action<T>>(action);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ThirdPersonPatches
|
||||
{
|
||||
public static void AfterCamera(SkopjeFPSCamera __instance)
|
||||
{
|
||||
ThirdPersonRig.Apply(__instance);
|
||||
}
|
||||
|
||||
public static void AfterBrain()
|
||||
{
|
||||
ThirdPersonRig.Apply(ThirdPersonRig.LocalCamera);
|
||||
}
|
||||
|
||||
public static void BeforeBodyRotation(SkopjeCharacterController __instance, ref Quaternion currentRotation)
|
||||
{
|
||||
ThirdPersonRig.RememberBody(__instance, currentRotation);
|
||||
}
|
||||
|
||||
// The motor turns the capsule toward the look yaw. Put the previous facing back so mouse-look only orbits the camera.
|
||||
public static void AfterBodyRotation(SkopjeCharacterController __instance, ref Quaternion currentRotation, float deltaTime)
|
||||
{
|
||||
ThirdPersonRig.HoldBodyFacing(__instance, ref currentRotation, deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>Skopje.ThirdPerson</AssemblyName>
|
||||
<RootNamespace>Skopje.ThirdPerson</RootNamespace>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<GameDir Condition="'$(GameDir)' == ''">D:\SteamLibrary\steamapps\common\Skopje83</GameDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="0Harmony">
|
||||
<HintPath>$(GameDir)\BepInEx\core\0Harmony.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Core">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Core.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="BepInEx.Unity.IL2CPP">
|
||||
<HintPath>$(GameDir)\BepInEx\core\BepInEx.Unity.IL2CPP.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2CppInterop.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\core\Il2CppInterop.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Il2Cppmscorlib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Il2Cppmscorlib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="SkopjeAssembly">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\SkopjeAssembly.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FishNet.Runtime">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\FishNet.Runtime.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Cinemachine">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Cinemachine.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="RootMotion">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\RootMotion.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.PhysicsModule">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\UnityEngine.PhysicsModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Archon.SwissArmyLib">
|
||||
<HintPath>$(GameDir)\BepInEx\interop\Archon.SwissArmyLib.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,485 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Skopje.ThirdPerson
|
||||
{
|
||||
public class ThirdPersonDriver : MonoBehaviour
|
||||
{
|
||||
void Update()
|
||||
{
|
||||
ThirdPersonRig.TickBody();
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
ThirdPersonRig.Apply(ThirdPersonRig.LocalCamera);
|
||||
}
|
||||
}
|
||||
|
||||
static class ThirdPersonRig
|
||||
{
|
||||
const float MoveThreshold = 0.2f;
|
||||
const float ExternalTurn = 35f;
|
||||
|
||||
static readonly List<Renderer> Hidden = new List<Renderer>();
|
||||
static readonly RaycastHit[] Hits = new RaycastHit[24];
|
||||
|
||||
internal static SkopjeFPSCamera LocalCamera;
|
||||
|
||||
static SkopjePlayer _player;
|
||||
static bool _holding;
|
||||
static bool _hasFacing;
|
||||
static Quaternion _facing;
|
||||
static Quaternion _incomingFacing;
|
||||
static bool _hasIncoming;
|
||||
static bool _hasLook;
|
||||
static Quaternion _look = Quaternion.identity;
|
||||
static bool _hasPlacement;
|
||||
static Vector3 _placedPos;
|
||||
static float _baseFov;
|
||||
static float _boom = 2.7f;
|
||||
static float _ads;
|
||||
static bool _capturedIk;
|
||||
static bool _aimWasEnabled;
|
||||
static bool _lookWasEnabled;
|
||||
static bool _followWasEnabled;
|
||||
static bool _disabledFollow;
|
||||
static float _nextScan;
|
||||
static int _errors;
|
||||
static bool _logged;
|
||||
|
||||
public static void OnClientStarted()
|
||||
{
|
||||
}
|
||||
|
||||
public static void OnClientStopped()
|
||||
{
|
||||
Release();
|
||||
_player = null;
|
||||
LocalCamera = null;
|
||||
}
|
||||
|
||||
public static void OnPlayerStarted(SkopjePlayer player)
|
||||
{
|
||||
if (player != null && player.IsOwner)
|
||||
_player = player;
|
||||
}
|
||||
|
||||
public static void OnPlayerStopped(SkopjePlayer player)
|
||||
{
|
||||
if (_player == player)
|
||||
{
|
||||
Release();
|
||||
_player = null;
|
||||
LocalCamera = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Shutdown()
|
||||
{
|
||||
Release();
|
||||
_player = null;
|
||||
LocalCamera = null;
|
||||
}
|
||||
|
||||
public static void TickBody()
|
||||
{
|
||||
if (!CanControl(_player))
|
||||
{
|
||||
if (_holding)
|
||||
Release();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HoldPresentation(_player);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Apply(SkopjeFPSCamera fps)
|
||||
{
|
||||
if (fps == null || !CanControl(fps.Player))
|
||||
return;
|
||||
|
||||
Camera cam = fps.mainCamera != null ? fps.mainCamera : Camera.main;
|
||||
if (cam == null)
|
||||
return;
|
||||
|
||||
LocalCamera = fps;
|
||||
if (_player == null)
|
||||
_player = fps.Player;
|
||||
|
||||
try
|
||||
{
|
||||
Place(fps, cam);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RememberBody(SkopjeCharacterController controller, Quaternion currentRotation)
|
||||
{
|
||||
if (!CanControl(controller != null ? controller.Player : null))
|
||||
{
|
||||
_hasIncoming = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_incomingFacing = FlatYaw(currentRotation);
|
||||
_hasIncoming = true;
|
||||
}
|
||||
|
||||
public static void HoldBodyFacing(SkopjeCharacterController controller, ref Quaternion currentRotation, float deltaTime)
|
||||
{
|
||||
if (!_hasIncoming || controller == null || !CanControl(controller.Player))
|
||||
return;
|
||||
|
||||
Quaternion held = _hasFacing ? _facing : _incomingFacing;
|
||||
if (!_hasFacing || Quaternion.Angle(held, _incomingFacing) > ExternalTurn)
|
||||
held = _incomingFacing;
|
||||
|
||||
if (ThirdPersonPlugin.FaceMovement.Value)
|
||||
{
|
||||
Vector3 move = controller.moveInputVector;
|
||||
move.y = 0f;
|
||||
if (move.sqrMagnitude > MoveThreshold * MoveThreshold)
|
||||
{
|
||||
float sharp = controller.OrientationSharpness;
|
||||
if (sharp < 0.01f)
|
||||
sharp = 12f;
|
||||
float blend = 1f - Mathf.Exp(-sharp * Mathf.Max(0f, deltaTime));
|
||||
held = Quaternion.Slerp(held, Quaternion.LookRotation(move.normalized, Vector3.up), blend);
|
||||
}
|
||||
}
|
||||
|
||||
_facing = FlatYaw(held);
|
||||
_hasFacing = true;
|
||||
currentRotation = _facing;
|
||||
}
|
||||
|
||||
static void Place(SkopjeFPSCamera fps, Camera cam)
|
||||
{
|
||||
Quaternion look = ReadLook(fps, cam);
|
||||
_look = look;
|
||||
_hasLook = true;
|
||||
|
||||
bool fresh = !_hasPlacement || (cam.transform.position - _placedPos).sqrMagnitude > 0.0004f;
|
||||
if (fresh)
|
||||
_baseFov = cam.fieldOfView;
|
||||
|
||||
float adsTarget = fps.adsMode ? 1f : 0f;
|
||||
_ads = Mathf.MoveTowards(_ads, adsTarget, Time.deltaTime / 0.12f);
|
||||
|
||||
float distance = Mathf.Lerp(Clamp(ThirdPersonPlugin.Distance.Value, 0.8f, 8f), Clamp(ThirdPersonPlugin.AdsDistance.Value, 0.6f, 6f), _ads);
|
||||
float shoulder = Mathf.Lerp(Clamp(ThirdPersonPlugin.Shoulder.Value, 0f, 2f), Clamp(ThirdPersonPlugin.AdsShoulder.Value, 0f, 1.5f), _ads);
|
||||
float height = Clamp(ThirdPersonPlugin.Height.Value, -1f, 2f);
|
||||
float boom = new Vector3(shoulder, height, -distance).magnitude;
|
||||
if (!_hasPlacement)
|
||||
_boom = boom;
|
||||
_boom = Mathf.Lerp(_boom, boom, 1f - Mathf.Exp(-16f * Time.deltaTime));
|
||||
|
||||
Vector3 pivot = Pivot(fps.Player);
|
||||
Vector3 offset = look * new Vector3(shoulder, height, -distance);
|
||||
if (offset.sqrMagnitude < 0.0001f)
|
||||
offset = look * Vector3.back;
|
||||
Vector3 dir = offset.normalized;
|
||||
float allowed = PullIn(fps.Player, pivot, dir, _boom);
|
||||
Vector3 camPos = pivot + dir * allowed;
|
||||
|
||||
Vector3 aim = AimPoint(fps.Player, pivot, look * Vector3.forward);
|
||||
Vector3 toAim = aim - camPos;
|
||||
Vector3 up = look * Vector3.up;
|
||||
if (toAim.sqrMagnitude < 0.0001f || Mathf.Abs(Vector3.Dot(toAim.normalized, up.normalized)) > 0.995f)
|
||||
cam.transform.rotation = look;
|
||||
else
|
||||
cam.transform.rotation = Quaternion.LookRotation(toAim, up);
|
||||
|
||||
cam.transform.position = camPos;
|
||||
cam.fieldOfView = _baseFov + Clamp(ThirdPersonPlugin.FovBonus.Value, 0f, 40f) * (1f - _ads);
|
||||
_placedPos = camPos;
|
||||
_hasPlacement = true;
|
||||
KeepBodyYaw(fps.Player);
|
||||
|
||||
if (!_logged)
|
||||
{
|
||||
_logged = true;
|
||||
ThirdPersonPlugin.LogSource.LogInfo("Right-shoulder camera is active. The body does not turn with the mouse.");
|
||||
}
|
||||
}
|
||||
|
||||
static void KeepBodyYaw(SkopjePlayer player)
|
||||
{
|
||||
if (!_hasFacing || player == null)
|
||||
return;
|
||||
|
||||
Transform body = player.transform;
|
||||
Vector3 up = body.up;
|
||||
Vector3 forward = Vector3.ProjectOnPlane(_facing * Vector3.forward, up);
|
||||
if (forward.sqrMagnitude < 0.0001f)
|
||||
return;
|
||||
|
||||
body.rotation = Quaternion.LookRotation(forward.normalized, up);
|
||||
}
|
||||
|
||||
static Quaternion ReadLook(SkopjeFPSCamera fps, Camera cam)
|
||||
{
|
||||
if (fps.VCamera != null && fps.VCamera.transform != cam.transform)
|
||||
return fps.VCamera.transform.rotation;
|
||||
if (fps.SocketTransform != null && fps.SocketTransform != cam.transform)
|
||||
return fps.SocketTransform.rotation;
|
||||
if (_hasLook && _hasPlacement && (cam.transform.position - _placedPos).sqrMagnitude < 0.0004f)
|
||||
return _look;
|
||||
return cam.transform.rotation;
|
||||
}
|
||||
|
||||
static Vector3 Pivot(SkopjePlayer player)
|
||||
{
|
||||
float height = Clamp(ThirdPersonPlugin.PivotHeight.Value, 0.4f, 2.4f);
|
||||
var body = player.SkopjeCharacterController;
|
||||
if (body != null)
|
||||
{
|
||||
if (body.standingHeight > 0.5f)
|
||||
height = Mathf.Min(height, body.standingHeight * 0.78f);
|
||||
float crouch = body.CrouchedHeightMult > 0.05f ? body.CrouchedHeightMult : 0.55f;
|
||||
height *= Mathf.Lerp(1f, crouch, Mathf.Clamp01(body.currentCrouch01));
|
||||
}
|
||||
|
||||
return player.transform.position + Vector3.up * height;
|
||||
}
|
||||
|
||||
static Vector3 AimPoint(SkopjePlayer player, Vector3 origin, Vector3 direction)
|
||||
{
|
||||
float reach = Clamp(ThirdPersonPlugin.AimDistance.Value, 5f, 200f);
|
||||
if (direction.sqrMagnitude < 0.0001f)
|
||||
direction = Vector3.forward;
|
||||
direction.Normalize();
|
||||
|
||||
int count = Physics.RaycastNonAlloc(origin, direction, Hits, reach, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore);
|
||||
float best = float.MaxValue;
|
||||
Vector3 point = origin + direction * reach;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (Hits[i].collider == null || IsOwn(player, Hits[i].collider))
|
||||
continue;
|
||||
if (Hits[i].distance < best)
|
||||
{
|
||||
best = Hits[i].distance;
|
||||
point = Hits[i].point;
|
||||
}
|
||||
}
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
static float PullIn(SkopjePlayer player, Vector3 origin, Vector3 direction, float distance)
|
||||
{
|
||||
const float radius = 0.18f;
|
||||
int count = Physics.SphereCastNonAlloc(origin, radius, direction, Hits, distance, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore);
|
||||
float best = distance;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (Hits[i].collider == null || IsOwn(player, Hits[i].collider))
|
||||
continue;
|
||||
float room = Mathf.Max(0.35f, Hits[i].distance);
|
||||
if (room < best)
|
||||
best = room;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
static void HoldPresentation(SkopjePlayer player)
|
||||
{
|
||||
GameObject third = player.ThirdPerson;
|
||||
if (third != null && !third.activeSelf)
|
||||
third.SetActive(true);
|
||||
|
||||
GameObject pack = player.ThirdPersonBackpackParent;
|
||||
if (pack != null && !pack.activeSelf)
|
||||
pack.SetActive(true);
|
||||
|
||||
var anim = player.TPAnimationController;
|
||||
if (anim != null)
|
||||
{
|
||||
if (!_capturedIk)
|
||||
{
|
||||
_aimWasEnabled = anim.AimAtMotor != null && anim.AimAtMotor.enabled;
|
||||
_lookWasEnabled = anim.LookAtMotor != null && anim.LookAtMotor.enabled;
|
||||
_capturedIk = true;
|
||||
}
|
||||
|
||||
if (anim.AimAtMotor != null)
|
||||
anim.AimAtMotor.enabled = false;
|
||||
if (anim.LookAtMotor != null)
|
||||
anim.LookAtMotor.enabled = false;
|
||||
}
|
||||
|
||||
var follow = player.ThirdPersonFollowCam;
|
||||
if (follow != null && follow.Look != null && follow.Look.enabled)
|
||||
{
|
||||
_followWasEnabled = true;
|
||||
follow.Look.enabled = false;
|
||||
_disabledFollow = true;
|
||||
}
|
||||
|
||||
if (Time.unscaledTime >= _nextScan)
|
||||
{
|
||||
_nextScan = Time.unscaledTime + 0.25f;
|
||||
RebuildHidden(player);
|
||||
}
|
||||
|
||||
for (int i = 0; i < Hidden.Count; i++)
|
||||
{
|
||||
if (Hidden[i] != null)
|
||||
Hidden[i].forceRenderingOff = true;
|
||||
}
|
||||
|
||||
_holding = true;
|
||||
}
|
||||
|
||||
static void RebuildHidden(SkopjePlayer player)
|
||||
{
|
||||
var next = new List<Renderer>();
|
||||
Transform third = player.ThirdPerson != null ? player.ThirdPerson.transform : null;
|
||||
Collect(player.FirstPerson, third, next);
|
||||
|
||||
var weapons = player.WeaponsController;
|
||||
if (weapons != null)
|
||||
{
|
||||
if (weapons.Socket != null)
|
||||
Collect(weapons.Socket.gameObject, third, next);
|
||||
if (weapons.ThrowSocket != null)
|
||||
Collect(weapons.ThrowSocket.gameObject, third, next);
|
||||
}
|
||||
|
||||
for (int i = 0; i < Hidden.Count; i++)
|
||||
{
|
||||
Renderer old = Hidden[i];
|
||||
if (old != null && !next.Contains(old))
|
||||
old.forceRenderingOff = false;
|
||||
}
|
||||
|
||||
Hidden.Clear();
|
||||
Hidden.AddRange(next);
|
||||
}
|
||||
|
||||
static void Collect(GameObject root, Transform thirdPerson, List<Renderer> into)
|
||||
{
|
||||
if (root == null)
|
||||
return;
|
||||
if (thirdPerson != null && (root.transform == thirdPerson || root.transform.IsChildOf(thirdPerson)))
|
||||
return;
|
||||
|
||||
Renderer[] found = root.GetComponentsInChildren<Renderer>(true);
|
||||
for (int i = 0; i < found.Length; i++)
|
||||
{
|
||||
Renderer renderer = found[i];
|
||||
if (renderer == null)
|
||||
continue;
|
||||
if (thirdPerson != null && renderer.transform.IsChildOf(thirdPerson))
|
||||
continue;
|
||||
if (!into.Contains(renderer))
|
||||
into.Add(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
static void Release()
|
||||
{
|
||||
for (int i = 0; i < Hidden.Count; i++)
|
||||
{
|
||||
if (Hidden[i] != null)
|
||||
Hidden[i].forceRenderingOff = false;
|
||||
}
|
||||
|
||||
Hidden.Clear();
|
||||
|
||||
if (_player != null)
|
||||
{
|
||||
var anim = _player.TPAnimationController;
|
||||
if (_capturedIk && anim != null)
|
||||
{
|
||||
if (anim.AimAtMotor != null)
|
||||
anim.AimAtMotor.enabled = _aimWasEnabled;
|
||||
if (anim.LookAtMotor != null)
|
||||
anim.LookAtMotor.enabled = _lookWasEnabled;
|
||||
}
|
||||
|
||||
if (_disabledFollow && _player.ThirdPersonFollowCam != null && _player.ThirdPersonFollowCam.Look != null)
|
||||
_player.ThirdPersonFollowCam.Look.enabled = _followWasEnabled;
|
||||
|
||||
try
|
||||
{
|
||||
_player.RefreshActiveModels();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
_capturedIk = false;
|
||||
_disabledFollow = false;
|
||||
_holding = false;
|
||||
_hasFacing = false;
|
||||
_hasIncoming = false;
|
||||
_hasPlacement = false;
|
||||
_hasLook = false;
|
||||
_ads = 0f;
|
||||
_logged = false;
|
||||
}
|
||||
|
||||
static bool CanControl(SkopjePlayer player)
|
||||
{
|
||||
if (!ThirdPersonPlugin.Enabled.Value || player == null || !player.IsOwner)
|
||||
return false;
|
||||
if (player.IsDead || player.IsFallen || player.IsSpectating || player.IsPaused || player.InInventory)
|
||||
return false;
|
||||
if (player.InsideVehicle != null)
|
||||
return false;
|
||||
|
||||
var cine = player._cinematicController;
|
||||
if (cine != null && cine.PlayingCinematic != null && cine.PlayingCinematic.PlayingCinematic != null)
|
||||
return false;
|
||||
|
||||
return player.SkopjeFPSCamera != null;
|
||||
}
|
||||
|
||||
static bool IsOwn(SkopjePlayer player, Collider collider)
|
||||
{
|
||||
if (player == null || collider == null)
|
||||
return false;
|
||||
Transform transform = collider.transform;
|
||||
Transform root = player.transform;
|
||||
return transform == root || transform.IsChildOf(root);
|
||||
}
|
||||
|
||||
static Quaternion FlatYaw(Quaternion rotation)
|
||||
{
|
||||
Vector3 forward = Vector3.ProjectOnPlane(rotation * Vector3.forward, Vector3.up);
|
||||
if (forward.sqrMagnitude < 0.0001f)
|
||||
forward = Vector3.ProjectOnPlane(rotation * Vector3.up, Vector3.up);
|
||||
if (forward.sqrMagnitude < 0.0001f)
|
||||
forward = Vector3.forward;
|
||||
return Quaternion.LookRotation(forward.normalized, Vector3.up);
|
||||
}
|
||||
|
||||
static float Clamp(float value, float min, float max)
|
||||
{
|
||||
return Mathf.Clamp(value, min, max);
|
||||
}
|
||||
|
||||
static void Fail(Exception ex)
|
||||
{
|
||||
if (_errors++ < 5 && ThirdPersonPlugin.LogSource != null)
|
||||
ThirdPersonPlugin.LogSource.LogWarning("Third person failed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v6.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"Skopje.ThirdPerson/1.0.0": {
|
||||
"runtime": {
|
||||
"Skopje.ThirdPerson.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Skopje.ThirdPerson/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Skopje.ThirdPerson")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Skopje.ThirdPerson")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Skopje.ThirdPerson")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
8ec25d9c2fd66f388ffb60470d85e452afd95523e8f8b9133f1a26f62c293384
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net6.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v6.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EntryPointFilePath =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = Skopje.ThirdPerson
|
||||
build_property.ProjectDir = c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 6.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
18339d4439978cc6f6c93b5159fc999229a5968512d398389890b72d566d8679
|
||||
@@ -0,0 +1,12 @@
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\ThirdPersonPlugin.csproj.AssemblyReference.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\ThirdPersonPlugin.GeneratedMSBuildEditorConfig.editorconfig
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\ThirdPersonPlugin.AssemblyInfoInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\ThirdPersonPlugin.AssemblyInfo.cs
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\ThirdPersonPlugin.csproj.CoreCompileInputs.cache
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\bin\Release\net6.0\Skopje.ThirdPerson.deps.json
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\bin\Release\net6.0\Skopje.ThirdPerson.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\bin\Release\net6.0\Skopje.ThirdPerson.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\Skopje.ThirdPerson.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\refint\Skopje.ThirdPerson.dll
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\Skopje.ThirdPerson.pdb
|
||||
c:\Users\andre\Desktop\SkopjeMods\ThirdPerson\obj\Release\net6.0\ref\Skopje.ThirdPerson.dll
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj",
|
||||
"projectName": "Skopje.ThirdPerson",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net6.0": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net6.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj",
|
||||
"projectName": "Skopje.ThirdPerson",
|
||||
"projectPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj",
|
||||
"packagesPath": "C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget",
|
||||
"outputPath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\andre\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net6.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net6.0": {
|
||||
"framework": "net6.0",
|
||||
"targetAlias": "net6.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"downloadDependencies": [
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.NETCore.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App.Ref",
|
||||
"version": "[6.0.36, 6.0.36]"
|
||||
}
|
||||
],
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.301\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "5HEtZERsN5U=",
|
||||
"success": true,
|
||||
"projectFilePath": "c:\\Users\\andre\\Desktop\\SkopjeMods\\ThirdPerson\\ThirdPersonPlugin.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.netcore.app.ref\\6.0.36\\microsoft.netcore.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.windowsdesktop.app.ref\\6.0.36\\microsoft.windowsdesktop.app.ref.6.0.36.nupkg.sha512",
|
||||
"C:\\Users\\andre\\AppData\\Local\\Temp\\cursor-sandbox-cache\\a0404811fc6e51c4c5761fd11591789f\\nuget\\microsoft.aspnetcore.app.ref\\6.0.36\\microsoft.aspnetcore.app.ref.6.0.36.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Il2CppInterop.Common" Version="1.5.3" />
|
||||
<PackageReference Include="Il2CppInterop.HarmonySupport" Version="1.5.3" />
|
||||
<PackageReference Include="Il2CppInterop.Runtime" Version="1.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Reflection.PortableExecutable;
|
||||
|
||||
var built = new[]
|
||||
{
|
||||
@"c:\Users\andre\Desktop\SkopjeMods\_fix\Il2CppInterop\bin\Il2CppInterop.Runtime\net6.0\Il2CppInterop.Runtime.dll",
|
||||
@"c:\Users\andre\Desktop\SkopjeMods\_fix\Il2CppInterop\bin\Il2CppInterop.Common\netstandard2.0\Il2CppInterop.Common.dll",
|
||||
};
|
||||
var builtMembers = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
foreach (var dll in built)
|
||||
{
|
||||
using var fs = File.OpenRead(dll);
|
||||
using var pe = new PEReader(fs);
|
||||
var reader = pe.GetMetadataReader();
|
||||
var asm = reader.GetAssemblyDefinition();
|
||||
Console.WriteLine("BUILT " + Path.GetFileName(dll) + " " + asm.Version);
|
||||
foreach (var ah in reader.AssemblyReferences)
|
||||
{
|
||||
var a = reader.GetAssemblyReference(ah);
|
||||
var n = reader.GetString(a.Name);
|
||||
Console.WriteLine(" ref " + n + " " + a.Version);
|
||||
}
|
||||
foreach (var th in reader.TypeDefinitions)
|
||||
{
|
||||
var t = reader.GetTypeDefinition(th);
|
||||
var name = reader.GetString(t.Namespace) + "." + reader.GetString(t.Name);
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var mh in t.GetMethods()) set.Add(reader.GetString(reader.GetMethodDefinition(mh).Name));
|
||||
foreach (var fh in t.GetFields()) set.Add(reader.GetString(reader.GetFieldDefinition(fh).Name));
|
||||
builtMembers[name] = set;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var caller in new[]
|
||||
{
|
||||
@"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\core\Il2CppInterop.HarmonySupport.dll",
|
||||
@"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\core\BepInEx.Unity.IL2CPP.dll",
|
||||
@"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\plugins\Skopje.BossHealth.dll",
|
||||
})
|
||||
{
|
||||
using var fs = File.OpenRead(caller);
|
||||
using var pe = new PEReader(fs);
|
||||
var reader = pe.GetMetadataReader();
|
||||
int miss = 0;
|
||||
foreach (var mh in reader.MemberReferences)
|
||||
{
|
||||
var m = reader.GetMemberReference(mh);
|
||||
if (m.Parent.Kind != HandleKind.TypeReference) continue;
|
||||
var tr = reader.GetTypeReference((TypeReferenceHandle)m.Parent);
|
||||
if (tr.ResolutionScope.Kind != HandleKind.AssemblyReference) continue;
|
||||
var scope = reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)tr.ResolutionScope).Name);
|
||||
if (scope != "Il2CppInterop.Runtime" && scope != "Il2CppInterop.Common") continue;
|
||||
var typeName = reader.GetString(tr.Namespace) + "." + reader.GetString(tr.Name);
|
||||
var member = reader.GetString(m.Name);
|
||||
if (!builtMembers.TryGetValue(typeName, out var set) || !set.Contains(member))
|
||||
{
|
||||
miss++;
|
||||
Console.WriteLine("MISS " + Path.GetFileName(caller) + " " + typeName + "::" + member);
|
||||
}
|
||||
}
|
||||
Console.WriteLine(Path.GetFileName(caller) + " missing " + miss);
|
||||
}
|
||||
return;
|
||||
|
||||
var bep = @"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\core\BepInEx.Unity.IL2CPP.dll";
|
||||
var newDlls = new[]
|
||||
{
|
||||
@"C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\il2cppinterop.runtime\1.5.3\lib\net6.0\Il2CppInterop.Runtime.dll",
|
||||
@"C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\il2cppinterop.common\1.5.3\lib\netstandard2.0\Il2CppInterop.Common.dll",
|
||||
@"C:\Users\andre\AppData\Local\Temp\cursor-sandbox-cache\a0404811fc6e51c4c5761fd11591789f\nuget\il2cppinterop.harmonysupport\1.5.3\lib\net6.0\Il2CppInterop.HarmonySupport.dll",
|
||||
};
|
||||
|
||||
var members = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
|
||||
foreach (var dll in newDlls)
|
||||
{
|
||||
if (!File.Exists(dll)) { Console.WriteLine("MISSING " + dll); continue; }
|
||||
using var fs = File.OpenRead(dll);
|
||||
using var pe = new PEReader(fs);
|
||||
var reader = pe.GetMetadataReader();
|
||||
foreach (var th in reader.TypeDefinitions)
|
||||
{
|
||||
var t = reader.GetTypeDefinition(th);
|
||||
var name = reader.GetString(t.Namespace) + "." + reader.GetString(t.Name);
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var mh in t.GetMethods())
|
||||
{
|
||||
var m = reader.GetMethodDefinition(mh);
|
||||
set.Add(reader.GetString(m.Name));
|
||||
}
|
||||
foreach (var fh in t.GetFields())
|
||||
set.Add(reader.GetString(reader.GetFieldDefinition(fh).Name));
|
||||
members[name] = set;
|
||||
}
|
||||
}
|
||||
|
||||
using var bfs = File.OpenRead(bep);
|
||||
using var bpe = new PEReader(bfs);
|
||||
var br = bpe.GetMetadataReader();
|
||||
int missing = 0;
|
||||
foreach (var mh in br.MemberReferences)
|
||||
{
|
||||
var m = br.GetMemberReference(mh);
|
||||
if (m.Parent.Kind != HandleKind.TypeReference) continue;
|
||||
var tr = br.GetTypeReference((TypeReferenceHandle)m.Parent);
|
||||
if (tr.ResolutionScope.Kind != HandleKind.AssemblyReference) continue;
|
||||
var scope = br.GetString(br.GetAssemblyReference((AssemblyReferenceHandle)tr.ResolutionScope).Name);
|
||||
if (!scope.StartsWith("Il2CppInterop")) continue;
|
||||
var typeName = br.GetString(tr.Namespace) + "." + br.GetString(tr.Name);
|
||||
var member = br.GetString(m.Name);
|
||||
if (!members.TryGetValue(typeName, out var set) || !set.Contains(member))
|
||||
{
|
||||
missing++;
|
||||
Console.WriteLine($"MISSING {scope} {typeName}::{member}");
|
||||
}
|
||||
}
|
||||
Console.WriteLine("bepinex missing count " + missing);
|
||||
|
||||
var interopDir = @"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\interop";
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
int interopMissing = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(interopDir, "*.dll"))
|
||||
{
|
||||
using var fs = File.OpenRead(file);
|
||||
using var pe = new PEReader(fs);
|
||||
if (!pe.HasMetadata) continue;
|
||||
var reader = pe.GetMetadataReader();
|
||||
foreach (var mh in reader.MemberReferences)
|
||||
{
|
||||
var m = reader.GetMemberReference(mh);
|
||||
if (m.Parent.Kind != HandleKind.TypeReference) continue;
|
||||
var tr = reader.GetTypeReference((TypeReferenceHandle)m.Parent);
|
||||
if (tr.ResolutionScope.Kind != HandleKind.AssemblyReference) continue;
|
||||
var scope = reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)tr.ResolutionScope).Name);
|
||||
if (scope != "Il2CppInterop.Runtime" && scope != "Il2CppInterop.Common") continue;
|
||||
var typeName = reader.GetString(tr.Namespace) + "." + reader.GetString(tr.Name);
|
||||
var member = reader.GetString(m.Name);
|
||||
var key = typeName + "::" + member;
|
||||
if (!seen.Add(key)) continue;
|
||||
if (!members.TryGetValue(typeName, out var set) || !set.Contains(member))
|
||||
{
|
||||
interopMissing++;
|
||||
if (interopMissing <= 40)
|
||||
{
|
||||
var raw = reader.GetBlobReader(m.Signature);
|
||||
Console.WriteLine("INTEROP MISSING " + key + " in " + Path.GetFileName(file) + " sig=" + Convert.ToHexString(raw.ReadBytes(raw.Length)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Console.WriteLine("interop missing count " + interopMissing);
|
||||
|
||||
static string ReadType(MetadataReader reader, BlobReader blob)
|
||||
{
|
||||
var et = blob.ReadByte();
|
||||
switch (et)
|
||||
{
|
||||
case 0x01: return "void";
|
||||
case 0x02: return "bool";
|
||||
case 0x03: return "char";
|
||||
case 0x04: return "sbyte";
|
||||
case 0x05: return "byte";
|
||||
case 0x06: return "short";
|
||||
case 0x07: return "ushort";
|
||||
case 0x08: return "int";
|
||||
case 0x09: return "uint";
|
||||
case 0x0A: return "long";
|
||||
case 0x0B: return "ulong";
|
||||
case 0x0C: return "float";
|
||||
case 0x0D: return "double";
|
||||
case 0x0E: return "string";
|
||||
case 0x0F: return ReadType(reader, blob) + "*";
|
||||
case 0x10: return ReadType(reader, blob) + "&";
|
||||
case 0x18: return "nint";
|
||||
case 0x19: return "nuint";
|
||||
case 0x1C: return "object";
|
||||
case 0x1D: return ReadType(reader, blob) + "[]";
|
||||
case 0x13: return "!!" + blob.ReadCompressedInteger();
|
||||
case 0x1E: return "!" + blob.ReadCompressedInteger();
|
||||
case 0x11:
|
||||
case 0x12:
|
||||
{
|
||||
var coded = blob.ReadCompressedInteger();
|
||||
var index = coded >> 2;
|
||||
var tag = coded & 3;
|
||||
if (tag == 0)
|
||||
{
|
||||
var td = reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(index));
|
||||
return reader.GetString(td.Namespace) + "." + reader.GetString(td.Name);
|
||||
}
|
||||
if (tag == 1)
|
||||
{
|
||||
var tr = reader.GetTypeReference(MetadataTokens.TypeReferenceHandle(index));
|
||||
return reader.GetString(tr.Namespace) + "." + reader.GetString(tr.Name);
|
||||
}
|
||||
if (tag == 2)
|
||||
{
|
||||
return "typespec" + index;
|
||||
}
|
||||
return "type" + tag + ":" + index;
|
||||
}
|
||||
case 0x15:
|
||||
{
|
||||
var g = ReadType(reader, blob);
|
||||
var n = blob.ReadCompressedInteger();
|
||||
var args = new List<string>();
|
||||
for (int i = 0; i < n; i++) args.Add(ReadType(reader, blob));
|
||||
return g + "<" + string.Join(",", args) + ">";
|
||||
}
|
||||
default: return "et" + et.ToString("X2");
|
||||
}
|
||||
}
|
||||
|
||||
var runtimePath = @"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\core\Il2CppInterop.Runtime.dll";
|
||||
using (var fs = File.OpenRead(runtimePath))
|
||||
using (var pe = new PEReader(fs))
|
||||
{
|
||||
var reader = pe.GetMetadataReader();
|
||||
foreach (var th in reader.TypeDefinitions)
|
||||
{
|
||||
var t = reader.GetTypeDefinition(th);
|
||||
foreach (var mh in t.GetMethods())
|
||||
{
|
||||
var m = reader.GetMethodDefinition(mh);
|
||||
if (reader.GetString(m.Name) != "WrapNativeGenericArrayPointer") continue;
|
||||
var raw = reader.GetBlobReader(m.Signature);
|
||||
Console.WriteLine("OLD RAW " + Convert.ToHexString(raw.ReadBytes(raw.Length)) + " parent=" + reader.GetString(t.Name));
|
||||
var blob = reader.GetBlobReader(m.Signature);
|
||||
var cc = blob.ReadByte();
|
||||
int gen = 0;
|
||||
if ((cc & 0x10) != 0) gen = blob.ReadCompressedInteger();
|
||||
var pc = blob.ReadCompressedInteger();
|
||||
var ret = ReadType(reader, blob);
|
||||
var ps = new List<string>();
|
||||
for (int i = 0; i < pc; i++) ps.Add(ReadType(reader, blob));
|
||||
Console.WriteLine($"OLD {reader.GetString(t.Namespace)}.{reader.GetString(t.Name)} gen={gen} {ret} ({string.Join(", ", ps)})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sample = Directory.GetFiles(@"D:\SteamLibrary\steamapps\common\Skopje83\BepInEx\interop", "UnityEngine.CoreModule.dll").First();
|
||||
using (var fs = File.OpenRead(sample))
|
||||
using (var pe = new PEReader(fs))
|
||||
{
|
||||
var reader = pe.GetMetadataReader();
|
||||
foreach (var mh in reader.MemberReferences)
|
||||
{
|
||||
var m = reader.GetMemberReference(mh);
|
||||
if (reader.GetString(m.Name) != "WrapNativeGenericArrayPointer") continue;
|
||||
string parent;
|
||||
if (m.Parent.Kind == HandleKind.TypeReference)
|
||||
{
|
||||
var tr = reader.GetTypeReference((TypeReferenceHandle)m.Parent);
|
||||
parent = "typeref " + reader.GetString(tr.Namespace) + "." + reader.GetString(tr.Name);
|
||||
}
|
||||
else parent = m.Parent.Kind.ToString();
|
||||
var raw = reader.GetBlobReader(m.Signature);
|
||||
var bytes = raw.ReadBytes(raw.Length);
|
||||
Console.WriteLine("REF parent=" + parent + " sig=" + Convert.ToHexString(bytes));
|
||||
}
|
||||
}
|
||||
foreach (var dll in newDlls)
|
||||
{
|
||||
using var fs = File.OpenRead(dll);
|
||||
using var pe = new PEReader(fs);
|
||||
var reader = pe.GetMetadataReader();
|
||||
Console.WriteLine("REFS " + Path.GetFileName(dll));
|
||||
foreach (var ah in reader.AssemblyReferences)
|
||||
{
|
||||
var a = reader.GetAssemblyReference(ah);
|
||||
Console.WriteLine(" " + reader.GetString(a.Name) + " " + a.Version);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user