Commit initial BossHealth, DamageNumbers, Horde, ThirdPerson mods

This commit is contained in:
Andrej Stojkovikj committed 2026-10-17 23:55:15 +02:00
1 parent 7bd44d1d99
commit 9c123865bc
176 files changed
+10367

No files matched your search

+33
View File
@@ -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;
}
}
+980
View File
@@ -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);
}
}
}
}
+60
View File
@@ -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": ""
}
}
}
@@ -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
@@ -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 =
@@ -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
+90
View File
@@ -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"
}
}
}
}
+12
View File
@@ -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": []
}
+628
View File
@@ -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;
}
}
}
+60
View File
@@ -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": ""
}
}
}
@@ -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
@@ -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 =
@@ -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
+90
View File
@@ -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"
}
}
}
}
+12
View File
@@ -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": []
}
+262
View File
@@ -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);
}
}
}
+484
View File
@@ -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;
}
}
}
}
+52
View File
@@ -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.
@@ -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.
+90
View File
@@ -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"
}
}
}
}
+12
View File
@@ -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
View File
@@ -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.
+16
View File
@@ -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/`.
+67
View File
@@ -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}");
// }
}
}
}
+37
View File
@@ -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>
+145
View File
@@ -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);
}
}
}
+64
View File
@@ -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>
+485
View File
@@ -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": ""
}
}
}
@@ -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.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
@@ -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 =
@@ -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
@@ -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" />
+90
View File
@@ -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"
}
}
}
}
+12
View File
@@ -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": []
}
+16
View File
@@ -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>
+274
View File
@@ -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