Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
57 changes: 57 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
root = true

[*]
indent_style = space
indent_size = 4
# Editor Guidelines VS/Rider extension: 180-col guide
guidelines = 180

[*.cs]
indent_size = 4
tab_width = 4

# A BOM is what stops an editor from misreading a UTF-8 file as CP1252 and silently
# round-tripping non-ASCII characters into mojibake (it happened to the tooltip pip
# glyphs in EpicLoot.cs). Prefer \uXXXX escapes for glyphs in string literals anyway.
charset = utf-8-bom

# --- One True Brace Style (1TBS) ---
csharp_new_line_before_open_brace = none
csharp_new_line_before_else = false
csharp_new_line_before_catch = false
csharp_new_line_before_finally = false
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true

# Always use braces (1TBS mandates them, even on single-line blocks)
csharp_prefer_braces = true:suggestion

# Block / switch indentation
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_block_contents = true
csharp_indent_braces = false

# Keep formatting deviations as hints, never warnings/errors
dotnet_diagnostic.IDE0055.severity = suggestion

# --- Reduce modernization nagging (merged from both old files) ---
dotnet_diagnostic.IDE0003.severity = none # Remove qualification
dotnet_diagnostic.IDE0031.severity = none # Use null propagation
dotnet_diagnostic.IDE0051.severity = none # Remove unused private members
dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter
dotnet_diagnostic.IDE0066.severity = none # Convert switch to expression
dotnet_diagnostic.IDE0074.severity = none # Use compound assignment
dotnet_diagnostic.IDE0090.severity = none # 'new' expression can be simplified
dotnet_diagnostic.IDE0130.severity = none # Namespace doesn't match folder
dotnet_diagnostic.IDE0270.severity = none # Use coalesce expression

# Localization files are read with BOM detection but must never carry one for
# other tooling; plain UTF-8 is the contract.
[*.{json,md}]
charset = utf-8

# --- Generated / third-party Unity code: skip style analysis ---
[ValheimUnity/**.cs]
generated_code = true
15 changes: 14 additions & 1 deletion .gitignore
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
/**/obj/
/packages/

# Always ignore the valheim assembly metadata as it gets updated each time a build happens
ValheimUnity/Assets/ExternalLibraries/**.meta
ValheimUnity/Assets/Exports.meta
EpicLoot/ValheimUnity/Assets/ExternalLibraries/EpicLoot.dll

# Never ignore Asset meta data
!/[Aa]ssets/**/*.meta

Expand Down Expand Up @@ -62,13 +67,15 @@ sysinfo.txt
*.apk
*.unitypackage

# Releases
EpicLoot/Releases/

# Crashlytics generated file
crashlytics-build.properties

*.swp
/valheim_Data/BepInEx-out/plugins/ExtendedItemDataFramework.dll
/valheim_Data/BepInEx-out/plugins/EpicLoot/translations.json
/valheim_Data/BepInEx-out/plugins/EpicLoot/recipes.json
/valheim_Data/BepInEx-out/plugins/EpicLoot/magiceffects.json
/valheim_Data/BepInEx-out/plugins/EpicLoot/loottables.json
/valheim_Data/BepInEx-out/plugins/EpicLoot/legendaries.json
Expand All @@ -92,3 +99,9 @@ ValheimUnity/Packages/packages-lock.json
Common
ValheimMods.sln
*.env
/EpicLoot/shardstone_testing_matrix.csv
/.gitignore
/Common/Common.projitems
/EpicLoot/EpicLoot.csproj
/EpicLoot/EpicLoot.sln
/EpicLoot/shardstone_testing_matrix.csv
6 changes: 3 additions & 3 deletions AdvancedPortals/AdvancedPortals.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</ItemGroup>
<Import Project="..\Common\Common.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="CopyAssembly" AfterTargets="Build">
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(SolutionDir)ValheimUnity\Assets\ExternalLibraries" />
</Target>
<Target Name="CopyAssembly" AfterTargets="Build">
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(SolutionDir)ValheimUnity\Assets\ExternalLibraries" />
</Target>
</Project>
2 changes: 1 addition & 1 deletion AdvancedPortals/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Advanced Portals

Author: [RandyKnapp](https://discord.gg/ZNhYeavv3C)
Source: [Github](https://github.com/OrianaVenture/Randy_Vapok_ValheimMods/tree/main/AdvancedPortals)
Source: [Github](https://github.com/RandyKnapp/ValheimMods/tree/main/AdvancedPortals)
Patreon: [patreon.com/randyknapp](https://www.patreon.com/randyknapp)
Discord: [RandyKnapp's Mod Community](https://discord.gg/ZNhYeavv3C)

Expand Down
5 changes: 5 additions & 0 deletions BuildTasks.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<Copy SourceFiles="$(BepinexPath)core\MonoMod.Utils.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries" />
<Copy SourceFiles="$(BepinexPath)core\MonoMod.RuntimeDetour.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries" />
<Copy SourceFiles="$(TargetDir)Jotunn.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries" />
<!-- EpicLoot.dll runtime dependency closure (needed when authoring/baking the enchanting UI prefabs
against the merged EpicLoot assembly in the Unity editor). Guarded so a missing package/lib on a
given machine doesn't fail the copy target. -->
<Copy SourceFiles="$(SolutionDir)Packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries" Condition="Exists('$(SolutionDir)Packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll')" />
<Copy SourceFiles="$(SolutionDir)Libs\System.Runtime.Serialization.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries" Condition="Exists('$(SolutionDir)Libs\System.Runtime.Serialization.dll')" />
<Copy SourceFiles="$(GamePathManaged)\Assembly-CSharp.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries\valheim" />
<Copy SourceFiles="$(GamePathManaged)\assembly_valheim.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries\valheim" />
<Copy SourceFiles="$(GamePathManaged)\assembly_utils.dll" DestinationFolder="$(SolutionUnity)Assets\ExternalLibraries\valheim" />
Expand Down
6 changes: 5 additions & 1 deletion Common/Common.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<Import_RootNamespace>Common</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)src\*.cs" />
<!-- Recursive by design: every .cs under src/ is compiled into each consuming mod. Do NOT add explicit
<Compile Include> entries alongside this - Visual Studio adds them automatically when you add a file
through Solution Explorer, and they collide with the glob ("Duplicate 'Compile' items were included").
If that happens, delete the explicit entries and keep this line. -->
<Compile Include="$(MSBuildThisFileDirectory)src\**\*.cs" />
</ItemGroup>
</Project>
46 changes: 46 additions & 0 deletions Common/src/Modding/AssetBundleLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Reflection;
using UnityEngine;

namespace Common {
/// <summary>
/// Loads an asset bundle embedded in a mod assembly as a manifest resource.
///
/// Replaces the copy of this method that lived in EpicLoot, Jam, AdvancedPortals and
/// EquipmentAndQuickSlots. Those used Assembly.GetCallingAssembly(); that is unsafe here, because the
/// caller can be another type in this shared project rather than the mod, so the assembly is explicit.
/// </summary>
public static class AssetBundleLoader {
/// <summary>
/// Loads the bundle embedded as "&lt;assembly name&gt;.&lt;filename&gt;". Returns null (and logs) on failure.
/// </summary>
public static AssetBundle LoadFromResources(string filename, Assembly assembly) {
if (assembly == null) { throw new ArgumentNullException(nameof(assembly)); }

string resourceName = $"{assembly.GetName().Name}.{filename}";
try {
using (Stream stream = assembly.GetManifestResourceStream(resourceName)) {
if (stream == null) {
ModLogger.LogError($"Embedded asset bundle '{resourceName}' not found in {assembly.GetName().Name}.");
return null;
}
return AssetBundle.LoadFromStream(stream);
}
} catch (Exception e) {
ModLogger.LogError($"Failed to load embedded asset bundle '{resourceName}': {e.Message}");
return null;
}
}

/// <summary>
/// Loads the bundle and assigns it to <see cref="ModContext.AssetBundle"/> so the shared loaders
/// can find it without being handed one.
/// </summary>
public static AssetBundle LoadIntoContext(string filename, Assembly assembly) {
AssetBundle bundle = LoadFromResources(filename, assembly);
if (bundle != null) { ModContext.AssetBundle = bundle; }
return bundle;
}
}
}
97 changes: 97 additions & 0 deletions Common/src/Modding/ConfigBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using BepInEx.Configuration;
using Jotunn.Extensions;

namespace Common {
/// <summary>
/// Binding helpers over <see cref="ModContext.Cfg"/>.
///
/// BindServerConfig marks entries IsAdminOnly, which is what makes Jotunn's SynchronizationManager
/// treat them as server-authoritative; BindClientConfig leaves them local to each player.
///
/// Prefer the ...InOrder variants for new config files: they keep the settings grouped the way the
/// code declares them instead of alphabetised. See the remarks on <see cref="BindServerConfigInOrder{T}"/>.
/// </summary>
public static class ConfigBinder {
private static ConfigFile Cfg => ModContext.Cfg;

// -- Server synced (admin only) ------------------------------------------------------------

public static ConfigEntry<bool> BindServerConfig(string category, string key, bool value, string description,
AcceptableValueBase acceptableValues = null, bool advanced = false) {
return Cfg.Bind(category, key, value,
new ConfigDescription(description, acceptableValues,
new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced }));
}

public static ConfigEntry<int> BindServerConfig(string category, string key, int value, string description,
bool advanced = false, int valMin = 0, int valMax = 150) {
return Cfg.Bind(category, key, value,
new ConfigDescription(description, new AcceptableValueRange<int>(valMin, valMax),
new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced }));
}

public static ConfigEntry<float> BindServerConfig(string category, string key, float value, string description,
bool advanced = false, float valMin = 0f, float valMax = 150f) {
return Cfg.Bind(category, key, value,
new ConfigDescription(description, new AcceptableValueRange<float>(valMin, valMax),
new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced }));
}

public static ConfigEntry<string> BindServerConfig(string category, string key, string value, string description,
AcceptableValueList<string> acceptableValues = null, bool advanced = false) {
return Cfg.Bind(category, key, value,
new ConfigDescription(description, acceptableValues,
new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced }));
}

/// <summary>Generic escape hatch for types without a dedicated overload above.</summary>
public static ConfigEntry<T> BindServerConfig<T>(string category, string key, T value, string description,
AcceptableValueBase acceptableValues, bool advanced = false) {
return Cfg.Bind(category, key, value,
new ConfigDescription(description, acceptableValues,
new ConfigurationManagerAttributes { IsAdminOnly = true, IsAdvanced = advanced }));
}

// -- Client local --------------------------------------------------------------------------

public static ConfigEntry<T> BindClientConfig<T>(string category, string key, T value, string description,
AcceptableValueBase acceptableValues = null, bool advanced = false) {
return Cfg.Bind(category, key, value,
new ConfigDescription(description, acceptableValues,
new ConfigurationManagerAttributes { IsAdminOnly = false, IsAdvanced = advanced }));
}

// -- Ordered ---------------------------------------------------------------------------------

/// <summary>
/// Server-synced entry bound through Jotunn's ordered binder.
/// </summary>
/// <remarks>
/// ConfigurationManager sorts sections alphabetically and, within a section, by the Order
/// attribute. Jotunn's BindConfigInOrder exploits both: it prefixes the section with the position
/// it was first bound in ("2 - Balance") and hands each entry a descending Order, so what the
/// player sees matches the grouping the code declares rather than an alphabetical jumble.
///
/// Two consequences to plan for:
/// - Keep a config file to **nine sections or fewer**. The prefix is not zero padded, so a tenth
/// section sorts as "10 - ..." between "1 - ..." and "2 - ...".
/// - The prefix is part of the section name written to disk, so inserting a section renames every
/// section after it and orphans the player's saved values. Add new sections at the end, or
/// migrate the old names (EpicLoot's ELConfig does the latter).
/// </remarks>
public static ConfigEntry<T> BindServerConfigInOrder<T>(string category, string key, T value, string description,
AcceptableValueBase acceptableValues = null, bool advanced = false) {
return Cfg.BindConfigInOrder(category, key, value, description, synced: true,
acceptableValues: acceptableValues,
configAttributes: new ConfigurationManagerAttributes { IsAdvanced = advanced });
}

/// <summary>Client-local counterpart of <see cref="BindServerConfigInOrder{T}"/>.</summary>
public static ConfigEntry<T> BindClientConfigInOrder<T>(string category, string key, T value, string description,
AcceptableValueBase acceptableValues = null, bool advanced = false) {
return Cfg.BindConfigInOrder(category, key, value, description, synced: false,
acceptableValues: acceptableValues,
configAttributes: new ConfigurationManagerAttributes { IsAdvanced = advanced });
}
}
}
57 changes: 57 additions & 0 deletions Common/src/Modding/ConfigChangeDebouncer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Common {
// Debounces config apply work so a burst of rapid SettingChanged events for one entry
// (a user typing into a field, a config file reload, or a server config sync) collapses
// into a single apply once the edits settle, instead of re-doing the heavy work (recipe
// rebuilds, world scans) on every intermediate value.
//
// Mirrors the EnqueueWorldUpdate/DrainWorldUpdates coroutine pattern (BepInEx ThreadingHelper,
// yielding null each frame - WaitForSeconds is not reliably honoured by that runner). No locking;
// like EnqueueWorldUpdate this assumes the config handlers run on the main thread.
public static class ConfigChangeDebouncer {
// Latest action to run per key (the changed ConfigEntry instance).
private static readonly Dictionary<object, Action> pendingActions = new Dictionary<object, Action>();
// Time (Time.realtimeSinceStartup) at which each key's action should fire.
private static readonly Dictionary<object, float> fireAt = new Dictionary<object, float>();
// Keys with a coroutine already waiting, so we don't start a second one.
private static readonly HashSet<object> running = new HashSet<object>();

// Schedules action to run after ModContext.ConfigApplyDelay seconds. Re-calling with the same
// key before it fires replaces the action and resets the timer (true debounce + coalesce).
// A delay <= 0 applies immediately (lets admins disable the delay).
public static void Schedule(object key, Action action) {
float delay = ModContext.ConfigApplyDelay != null ? ModContext.ConfigApplyDelay.Value : 0f;
if (delay <= 0f) {
action();
return;
}
// During game shutdown the ThreadingHelper's MonoBehaviour is destroyed while config entries can
// still fire SettingChanged (e.g. Jotunn reverting server-synced values on disconnect). Calling
// StartCoroutine on a destroyed behaviour throws ArgumentNullException, and there's nothing left
// to update anyway, so drop the change. The Unity '==' overload treats a destroyed object as null.
BepInEx.ThreadingHelper host = BepInEx.ThreadingHelper.Instance;
if (host == null) { return; }
pendingActions[key] = action;
fireAt[key] = Time.realtimeSinceStartup + delay;
if (running.Contains(key)) { return; }
running.Add(key);
host.StartCoroutine(Run(key));
}

private static IEnumerator Run(object key) {
// Wait until the entry has been idle for the full delay; re-scheduling pushes fireAt out.
while (fireAt.TryGetValue(key, out float at) && Time.realtimeSinceStartup < at) {
yield return null;
}
pendingActions.TryGetValue(key, out Action action);
pendingActions.Remove(key);
fireAt.Remove(key);
running.Remove(key);
action?.Invoke();
}
}
}
Loading
Loading