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