Files
MxValheim/MxValheim/MxValheim.cs
2026-02-16 04:04:49 -05:00

344 lines
13 KiB
C#

using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using MxValheim.EventSystem;
using MxValheim.KillFeed;
using MxValheim.Patch;
using MxValheim.Patch.HUD;
using Newtonsoft.Json;
using Splatform;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using static MxValheim.Patch.Doors_Patch;
[BepInPlugin(ModGUID, ModName, ModVersion)]
public class MxValheimMod : BaseUnityPlugin
{
public static MxValheimMod Instance; // Singleton reference
public static KillFeed_Patch kfp = new KillFeed_Patch();
public static Doors_Patch dpatch = new Doors_Patch();
private const string ModGUID = "ovh.mxdev.mxvalheim";
private const string ModName = "MxValheim";
private const string ModVersion = "1.6.2";
public static ConfigEntry<bool> Config_Locked;
public static ConfigEntry<int> Config_OreMultiplier;
public static ConfigEntry<float> Config_rangeMultiplier;
public static ConfigEntry<float> Config_bowDrawSpeedBonusPerLevel;
public static ConfigEntry<bool> Config_rainDamage;
public static ConfigEntry<float> Config_boatSpeed;
public static ConfigEntry<bool> Config_autoDoorCloseEnabled;
public static ConfigEntry<float> Config_autoDoorClose;
public static string modPath = Path.Combine(Paths.PluginPath, "MxValheim");
public static string internalConfigsPath = Path.Combine(modPath, "Configs");
public static string internalDataPath = Path.Combine(modPath, "Data");
public static string internalDataEventSystem = Path.Combine(internalDataPath, "EventSystem.json");
public static string WeightConfigPath => Path.Combine(internalConfigsPath, "items_weight.json");
public static string AutoDoorConfigPath => Path.Combine(internalConfigsPath, "auto_doors.json");
public static DoorRoot CachedConfig;
public static Dictionary<string, float> WeightSettings = new Dictionary<string, float>();
public static bool initExpBar = true;
// Data structures
public class KillData
{
public string attackerName;
public string weaponPrefabName;
public int victimLevel;
public bool isBoss;
}
public static GameObject _hudRoot;
public static Text _killText;
public static Image _weaponIconSlot;
public static Image _victimIconSlot;
public static Image _panelImage;
public static Outline _border;
public static Color borderColor = Color.white;
public static CanvasGroup _canvasGroup;
public static float _fadeDuration = 0.5f;
public static float _displayTimer;
public static readonly Queue<string> _msgQueue = new Queue<string>();
public static readonly Queue<Color> _borderColQueue = new Queue<Color>();
public static readonly Queue<Sprite> _iconQueue = new Queue<Sprite>();
public static readonly Queue<Sprite> _victimIconQueue = new Queue<Sprite>();
public static readonly Dictionary<Character, KillData> _activeTrackers = new Dictionary<Character, KillData>();
public static readonly Queue<ZNetView> _doorQueueNview = new Queue<ZNetView>();
public static readonly Queue<Door> _doorQueueDoor = new Queue<Door>();
public static readonly Queue<string> _doorQueueName = new Queue<string>();
public static float _doorTimer;
public static readonly Queue<RandomEvent> _serverEvent = new Queue<RandomEvent>();
void Awake()
{
Instance = this;
Config_OreMultiplier = Config.Bind("General", "OreMultiplier", 3, "How many items should drop for every 1 ore/scrap found.");
Config_rangeMultiplier = Config.Bind("General", "CraftingRangeMultiplier", 2.0f, "Multiplier for the workbench build/crafting range. Default is 2x.");
Config_bowDrawSpeedBonusPerLevel = Config.Bind("General", "BowDrawSpeedBonusPercentPerLevel", 1.0f, "Shorten the bow draw speed by this percent for every bow upgrade level.");
Config_rainDamage = Config.Bind("General", "RainDamage", true, "Set to true to stop rain damage, false to return to vanilla behavior.");
Config_boatSpeed = Config.Bind("General", "BoatSpeedMultiplier", 2.0f, "Your boat/raft will move without wind at a speed multiplied by this value.");
Config_autoDoorCloseEnabled = Config.Bind("General", "AutoDoorCloseEnabled", true, "Your doors will auto close if enabled. See AutoDoorCloseTimer for the desired time.");
Config_autoDoorClose = Config.Bind("General", "AutoDoorCloseTimer", 5.0f, "Your doors will auto close after the specified timer duration.");
LoadLocalization();
LoadJsonConfig();
LoadDoorConfig();
_doorTimer = MxValheimMod.Config_autoDoorClose.Value;
Harmony harmony = new Harmony(ModGUID);
harmony.PatchAll();
}
[HarmonyPatch(typeof(Localization), nameof(Localization.SetupLanguage))]
public static class Localization_SetupLanguage_Patch
{
public static void Postfix()
{
LoadLocalization();
}
}
// --- TEST COMMAND: Type 'testkill' in F5 console ---
[HarmonyPatch(typeof(Terminal), nameof(Terminal.InputText))]
public static class ConsoleInputPatch
{
static void Postfix(Terminal __instance)
{
string text = __instance.m_input.text;
if (text.ToLower() == "listicons")
{
}
}
}
[HarmonyPatch(typeof(Game), nameof(Game.Start))]
public static class GameStartPatch
{
static void Postfix()
{
if (ZRoutedRpc.instance != null && kfp != null)
{
// We use the explicit 'Method' delegate to avoid the ArgumentException
ZRoutedRpc.instance.Register<string, string, string, string, int>("RPC_MxKillMsg",
new RoutedMethod<string, string, string, string, int>.Method(kfp.OnReceiveKillMsg));
ZRoutedRpc.instance.Register<string, string>("RPC_ServerCreateUser", Profiles.ServerReceiveCreateUser);
ZRoutedRpc.instance.Register<string>("RPC_RequestProfile", Profiles.ServerHandleProfileRequest);
ZRoutedRpc.instance.Register<string>("RPC_ClientReceiveProfile", Profiles.LoadProfile);
ZRoutedRpc.instance.Register<int>("RPC_ServerSetExperience", Profiles.ServerSetExperience);
ZRoutedRpc.instance.Register<int>("RPC_ServerSetLevel", Profiles.ServerSetLevel);
}
}
}
void Update()
{
// Only run if we are actually in the game world
if (Player.m_localPlayer != null)
{
string pid = GetUserPlayerID(Player.m_localPlayer).ToString();
Experience.UpdateProgressBar();
}
// Only run if we are actually in the game world
if (Player.m_localPlayer != null)
{
if (Splash.m_canvasObject == null)
{
Splash spl = new Splash();
spl.CreateOverlay();
}
if (Splash.m_textComponent != null)
{
Splash.m_textComponent.text = $"<color=#677074>Mx</color><color=#d1954a>Valheim {MxValheimMod.ModVersion}</color>";
}
if (Clock.m_canvasObject == null)
{
Clock clk = new Clock();
clk.CreateOverlay();
if (initExpBar)
{
string pid = GetUserPlayerID(Player.m_localPlayer).ToString();
Experience.UpdateProgressBar();
initExpBar = false;
}
}
if (Clock.m_textComponent != null)
{
Clock clk = new Clock();
string timeString = clk.GetFormattedTime();
int day = EnvMan.instance.GetDay();
string pid = GetUserPlayerID(Player.m_localPlayer).ToString();
Clock.m_textComponent.text = $"<color=#32a852>{Localization.instance.Localize("$clock_string_rank")} {Profiles.serverLevel}</color> <color=#677074>|</color> <color=#32a852>{Localization.instance.Localize("$clock_string_day")} {day}</color> <color=#677074>|</color> <color=#d1954a>{timeString}</color>";
}
}
// Door Logic
if (_doorQueueNview.Count > 0)
{
if (_doorTimer > 0)
{
_doorTimer -= Time.deltaTime;
}
if (_doorTimer <= 0)
{
dpatch.CloseNextDoor(_doorQueueDoor.Dequeue(), _doorQueueNview.Dequeue(), _doorQueueName.Dequeue());
_doorTimer = MxValheimMod.Config_autoDoorClose.Value;
}
}
// Door Logic
/////////////
// KillFeed Logic
ZNet zn = new ZNet();
if (zn.IsDedicated() || Player.m_localPlayer == null) return;
if (_msgQueue == null || _msgQueue.Count == 0 && _displayTimer <= 0) return;
// Logic to trigger next message
if (_msgQueue.Count > 0 && _displayTimer <= 0)
{
borderColor = new Color(0.141f, 0.141f, 0.153f);
kfp.ShowNextMessage(_msgQueue.Dequeue(), _iconQueue.Dequeue(), _victimIconQueue.Dequeue(), _borderColQueue.Dequeue());
}
if (_displayTimer > 0)
{
_displayTimer -= Time.deltaTime;
if (_canvasGroup != null && _hudRoot != null)
{
RectTransform pRect = _panelImage.GetComponent<RectTransform>();
// FADE AND SLIDE LOGIC
if (_displayTimer > 4.5f)
{ // Fading in (first 0.5s)
_canvasGroup.alpha = (5f - _displayTimer) * 2;
}
else if (_displayTimer < 1f)
{ // Fading out (last 1s)
_canvasGroup.alpha = _displayTimer;
}
else
{
_canvasGroup.alpha = 1f;
pRect.anchoredPosition = new Vector2(0, -40);
}
}
if (_displayTimer <= 0 && _hudRoot != null) _hudRoot.SetActive(false);
}
// KillFeed Logic
/////////////////
}
public static long GetUserPlayerID(Player player)
{
return player.GetPlayerID();
}
public static void LoadLocalization()
{
if (Localization.instance == null) return;
string modPath = Path.Combine(Paths.PluginPath, "MxValheim");
string translationsPath = Path.Combine(modPath, "Translations");
string lang = Localization.instance.GetSelectedLanguage();
string filePath = Path.Combine(translationsPath, $"{lang}.json");
if (!File.Exists(filePath))
{
filePath = Path.Combine(translationsPath, "English.json");
}
if (File.Exists(filePath))
{
try
{
string json = File.ReadAllText(filePath);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
// Get the method via Reflection to bypass "Inaccessible" errors
MethodInfo addWordMethod = typeof(Localization).GetMethod("AddWord",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (addWordMethod != null)
{
foreach (var entry in dict)
{
// Parameters: (instance to run on, array of arguments)
addWordMethod.Invoke(Localization.instance, new object[] { entry.Key, entry.Value });
}
Debug.Log($"[MxValheim] Successfully injected {dict.Count} strings for {lang}.");
}
else
{
Debug.LogError("[MxValheim] Critical Error: Could not find AddWord method in game code.");
}
}
catch (Exception e)
{
Debug.LogError($"[MxValheim] Error loading JSON: {e.Message}");
}
}
}
private bool LoadJsonConfig()
{
try
{
if (!File.Exists(WeightConfigPath))
{
WeightSettings = new Dictionary<string, float> { { "Wood", 1.0f }, { "Stone", 1.0f } };
File.WriteAllText(WeightConfigPath, JsonConvert.SerializeObject(WeightSettings, Newtonsoft.Json.Formatting.Indented));
return true;
}
string json = File.ReadAllText(WeightConfigPath);
WeightSettings = JsonConvert.DeserializeObject<Dictionary<string, float>>(json);
Logger.LogInfo($"Successfully parsed {WeightSettings.Count} items.");
return true;
}
catch (Exception ex)
{
Logger.LogWarning($"Could not read JSON (might be busy): {ex.Message}");
return false;
}
}
public void LoadDoorConfig()
{
if (System.IO.File.Exists(AutoDoorConfigPath))
{
string json = System.IO.File.ReadAllText(AutoDoorConfigPath);
CachedConfig = JsonConvert.DeserializeObject<DoorRoot>(json);
}
}
internal static void ShowKillMessage(string v)
{
throw new NotImplementedException();
}
}