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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 22 additions & 146 deletions AdvancedPortals/AdvancedPortals.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Common;
using HarmonyLib;
using JetBrains.Annotations;
using Jotunn.Configs;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine;
Expand All @@ -25,87 +22,23 @@ public class AdvancedPortals : BaseUnityPlugin
{
public const string PluginId = "randyknapp.mods.advancedportals";
public const string DisplayName = "Advanced Portals";
public const string Version = "1.1.3";
public const string Version = "1.2.0";

private static string ConfigFileName = PluginId + ".cfg";
private static string ConfigFileFullPath = BepInEx.Paths.ConfigPath + Path.DirectorySeparatorChar + ConfigFileName;

public static readonly string[] PortalPrefabs = { "portal_ancient", "portal_obsidian", "portal_blackmarble" };

public static readonly List<GameObject> RegisteredPrefabs = new List<GameObject>();

public static ConfigEntry<bool> AncientPortalEnabled;
public static string AncientPortalRecipeDefault = "ElderBark:20,Iron:5,SurtlingCore:2";
public static ConfigEntry<string> AncientPortalRecipe = null!;
public static ConfigEntry<string> AncientPortalAllowedItems = null!;
public static ConfigEntry<bool> AncientPortalAllowEverything = null!;

public static ConfigEntry<bool> ObsidianPortalEnabled;
public static string ObsidianPortalRecipeDefault = "Obsidian:20,Silver:5,SurtlingCore:2";
public static ConfigEntry<string> ObsidianPortalRecipe;
public static ConfigEntry<string> ObsidianPortalAllowedItems = null!;
public static ConfigEntry<bool> ObsidianPortalAllowEverything = null!;
public static ConfigEntry<bool> ObsidianPortalAllowPreviousPortalItems = null!;

public static ConfigEntry<bool> BlackMarblePortalEnabled;
public static string BlackMarblePortalRecipeDefault = "BlackMarble:20,BlackMetal:5,Eitr:2";
public static ConfigEntry<string> BlackMarblePortalRecipe;
public static ConfigEntry<string> BlackMarblePortalAllowedItems = null!;
public static ConfigEntry<bool> BlackMarblePortalAllowEverything = null!;
public static ConfigEntry<bool> BlackMarblePortalAllowPreviousPortalItems = null!;

public static readonly ManualLogSource APLogger = BepInEx.Logging.Logger.CreateLogSource(DisplayName);
private Harmony _harmony;

[UsedImplicitly]
private void Awake()
{
AddConfig("Portal 1 - Ancient", "Ancient Portal Enabled",
"Enable the Ancient Portal",
true, true, ref AncientPortalEnabled);
AddConfig("Portal 1 - Ancient", "Ancient Portal Recipe",
"The items needed to build the Ancient Portal. A comma separated list of ITEM:QUANTITY pairs separated by a colon.",
true, AncientPortalRecipeDefault, ref AncientPortalRecipe);
AddConfig("Portal 1 - Ancient", "Ancient Portal Allowed Items",
"A comma separated list of the item types allowed through the Ancient Portal",
true, "Copper, CopperOre, CopperScrap, Tin, TinOre, Bronze, BronzeScrap", ref AncientPortalAllowedItems);
AddConfig("Portal 1 - Ancient", "Ancient Portal Allow Everything",
"Allow all items through the Ancient Portal (overrides Allowed Items)",
true, false, ref AncientPortalAllowEverything);

AddConfig("Portal 2 - Obsidian", "Obsidian Portal Enabled",
"Enable the Obsidian Portal",
true, true, ref ObsidianPortalEnabled);
AddConfig("Portal 2 - Obsidian", "Obsidian Portal Recipe",
"The items needed to build the Obsidian Portal. A comma separated list of ITEM:QUANTITY pairs separated by a colon.",
true, ObsidianPortalRecipeDefault, ref ObsidianPortalRecipe);
AddConfig("Portal 2 - Obsidian", "Obsidian Portal Allowed Items",
"A comma separated list of the item types allowed through the Obsidian Portal",
true, "Iron, IronScrap, IronOre", ref ObsidianPortalAllowedItems);
AddConfig("Portal 2 - Obsidian", "Obsidian Portal Allow Everything",
"Allow all items through the Obsidian Portal (overrides Allowed Items)",
true, false, ref ObsidianPortalAllowEverything);
AddConfig("Portal 2 - Obsidian", "Obsidian Portal Use All Previous",
"Additionally allow all items from the Ancient Portal",
true, true, ref ObsidianPortalAllowPreviousPortalItems);
// Wire the shared Common support layer (config binder, piece loader, drawers) to this plugin
// before anything else in Common is used -- ConfigBinder and ModLogger read it for the config
// file and log source.
ModContext.Initialize(this, APLogger, "AdvancedPortals");

AddConfig("Portal 3 - Black Marble", "Black Marble Portal Enabled",
"Enable the Black Marble Portal",
true, true, ref BlackMarblePortalEnabled);
AddConfig("Portal 3 - Black Marble", "Black Marble Portal Recipe",
"The items needed to build the Black Marble Portal. A comma separated list of ITEM:QUANTITY pairs separated by a colon.",
true, BlackMarblePortalRecipeDefault, ref BlackMarblePortalRecipe);
AddConfig("Portal 3 - Black Marble", "Black Marble Portal Allowed Items",
"A comma separated list of the item types allowed through the Black Marble Portal",
true, "Silver, SilverOre, BlackMetal, BlackMetalScrap", ref BlackMarblePortalAllowedItems);
AddConfig("Portal 3 - Black Marble", "Black Marble Portal Allow Everything",
"Allow all items through the Black Marble Portal (overrides Allowed Items)",
true, true, ref BlackMarblePortalAllowEverything);
AddConfig("Portal 3 - Black Marble", "Black Marble Portal Use All Previous",
"Additionally allow all items from the Obsidian and Ancient Portal",
true, true, ref BlackMarblePortalAllowPreviousPortalItems);

AssetBundle assetBundle = LoadAssetBundle("advancedportals");
AssetBundle assetBundle = AssetBundleLoader.LoadIntoContext("advancedportals", typeof(AdvancedPortals).Assembly);
if (assetBundle == null)
{
// Everything below needs the bundle. Bailing here reports one clear cause instead of a
Expand All @@ -114,35 +47,11 @@ private void Awake()
return;
}

LoadBuildPiece(assetBundle, "portal_ancient", new PieceConfig()
{
Name = "$item_elderbark $piece_portal",
Description = "$piece_portal_description",
PieceTable = "Hammer",
Category = "Misc",
CraftingStation = "piece_workbench",
Requirements = UpdatePortals.MakeRecipeFromConfig("Ancient Portal", AncientPortalRecipe.Value).ToArray()
});

LoadBuildPiece(assetBundle, "portal_obsidian", new PieceConfig()
{
Name = "$item_obsidian $piece_portal",
Description = "$piece_portal_description",
PieceTable = "Hammer",
Category = "Misc",
CraftingStation = "piece_workbench",
Requirements = UpdatePortals.MakeRecipeFromConfig("Obsidian Portal", ObsidianPortalRecipe.Value).ToArray()
});

LoadBuildPiece(assetBundle, "portal_blackmarble", new PieceConfig()
{
Name = "$item_blackmarble $piece_portal",
Description = "$piece_portal_description",
PieceTable = "Hammer",
Category = "Misc",
CraftingStation = "piece_workbench",
Requirements = UpdatePortals.MakeRecipeFromConfig("Black Marble Portal", BlackMarblePortalRecipe.Value).ToArray()
});
// SaveOnConfigSet writes the whole .cfg once per bound entry. Batch the portal registrations
// and flush once instead; this is a measurable chunk of mod load time.
ModContext.SaveOnSet(false);
Portals.RegisterAll();
ModContext.SaveOnSet(true);

// Fix up mocked portal connect effects
GameObject fxAncient = assetBundle.LoadAsset<GameObject>("fx_portal_connected_ancient");
Expand All @@ -155,11 +64,12 @@ private void Awake()
_harmony = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), PluginId);
SetupWatcher();

// Ensure configurations apply in singleplayer games
PieceManager.OnPiecesRegistered += UpdatePortalsOnGameStart;
// Apply the teleport allow-lists once the pieces exist. PieceLoader owns the build side
// (enabled, station, category, cost) and applies that on its own hook.
PieceManager.OnPiecesRegistered += Portals.ApplyAllTeleportRules;

// Add Prefabs to Portal Connections
foreach (string portalPrefab in PortalPrefabs)
foreach (string portalPrefab in Portals.PrefabNames)
{
AddPortal.Hashes.Add(portalPrefab.GetStableHashCode());
}
Expand Down Expand Up @@ -209,6 +119,11 @@ private void ReadConfigValues(object sender, FileSystemEventArgs e)
long time = now.Ticks - _lastReloadTime.Ticks;
if (!File.Exists(ConfigFileFullPath) || time < RELOAD_DELAY) return;

// A connected client must not reload: Jotunn has already replaced these values with the
// server's, and a reload would clobber them with whatever this machine happens to have on disk.
// The main menu (no ZNet yet) and a singleplayer or host session both still reload.
if (ZNet.instance != null && !ZNet.instance.IsServer()) return;

try
{
APLogger.LogInfo("Attempting to reload configuration...");
Expand All @@ -220,48 +135,9 @@ private void ReadConfigValues(object sender, FileSystemEventArgs e)
return;
}

if (ZNet.instance != null && !ZNet.instance.IsDedicated())
{
UpdatePortals.UpdatePortalConfigurations();
}

// Nothing to apply by hand: Config.Reload raises SettingChanged for every entry whose value
// actually changed, and PieceLoader and Portals both act on that themselves.
_lastReloadTime = now;
}

private readonly ConfigurationManagerAttributes AdminConfig = new ConfigurationManagerAttributes { IsAdminOnly = true };
private readonly ConfigurationManagerAttributes ClientConfig = new ConfigurationManagerAttributes { IsAdminOnly = false };

private void AddConfig<T>(string section, string key, string description, bool synced, T value, ref ConfigEntry<T> configEntry)
{
string extendedDescription = GetExtendedDescription(description, synced);
configEntry = Config.Bind(section, key, value,
new ConfigDescription(extendedDescription, null, synced ? AdminConfig : ClientConfig));
}

public string GetExtendedDescription(string description, bool synchronizedSetting)
{
return description + (synchronizedSetting ? " [Synced with Server]" : " [Not Synced with Server]");
}

// The assembly is named explicitly rather than taken from Assembly.GetCallingAssembly(). When
// another mod hooks Awake, MonoMod recompiles it as a dynamic method (DMD<...::Awake>), and the
// "calling assembly" is then that dynamic assembly, not this one. The resource lookup misses,
// LoadFromStream(null) throws "ArgumentNullException: stream", and the mod fails to load —
// intermittently, since it depends on which other mods are present.
public static AssetBundle LoadAssetBundle(string filename)
{
return AssetBundleLoader.LoadFromResources(filename, typeof(AdvancedPortals).Assembly);
}

private static void LoadBuildPiece(AssetBundle assetBundle, string assetName, PieceConfig piececonfig)
{
GameObject prefab = assetBundle.LoadAsset<GameObject>(assetName);
PieceManager.Instance.AddPiece(new CustomPiece(prefab, true, piececonfig));
}

private static void UpdatePortalsOnGameStart()
{
UpdatePortals.UpdatePortalConfigurations();
}
}
}
9 changes: 9 additions & 0 deletions AdvancedPortals/Package/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
**1.2.0**
* Portals now use the shared configurable-piece system. Each portal gains new server-synced settings that apply live, without a restart:
* **Requires Workbench** / **Workbench** - whether a crafting station is needed at all, and which one.
* **Piece Category** - which hammer build tab the portal appears in.
* **Building Cost** - now supports a per-ingredient refund flag.
* Disabling a portal is now reversible in-game. Previously a disabled portal stayed gone until a restart.
* Fixed: editing your own config file while connected to a server no longer overwrites the values the server sent you.
* **Breaking:** the config file has been reorganised and your previous customisations will not carry over. Each portal now has its own section named after it (`[Ancient Portal]`), and the build cost format changed from `Item:Quantity,Item:Quantity` to `Item,Quantity,Refundable|Item,Quantity,Refundable`. Delete `randyknapp.mods.advancedportals.cfg` and re-apply any changes you had made.

**1.1.3**
* Fixed an issue with the AllowEverything config option no longer being used in the teleport check.

Expand Down
39 changes: 29 additions & 10 deletions AdvancedPortals/Package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,40 @@ Adds three new portals to provide a lore-friendly and balanced way to reduce the
* **Black Marble Portal:** Allows teleporting anything
* *Requires:* 20 Black Marble, BlackMetal 5, 2 Refined Eitr

## Version 1.1.0!
## Version 1.2.0!

As of the 1.1.0 update Jotunn is required to run this mod. A version check will be performed on server connection to ensure all players have the mod installed properly.
Portals are now fully configurable building pieces. Alongside the recipe and teleport settings, you can
change each portal's build category, whether it needs a crafting station at all, and which station that is.
Every setting applies live, without restarting the game.

Configurations should sync on servers and live update on changing the randyknapp.mods.advancedportals.cfg file.
**Upgrading from 1.1.x:** the config file has been reorganised and your previous customisations will not
carry over. Delete `randyknapp.mods.advancedportals.cfg` and re-apply any changes you had made.

## Configuration:
Jotunn is required. A version check runs on server connection to make sure every player has the mod
installed properly. Settings are synced from the server and can be edited live, either in the in-game
Configuration Manager or by editing `randyknapp.mods.advancedportals.cfg`.

Each portal can be configured:
## Configuration:

* Enabled: Enable building the portal. Existing portals of this type will not be removed.
* Recipe: Items needed to build the portal in the format "ITEM1:QUANTITY,ITEM2:QUANTITY,..." where each ITEM is the item ID ([found here](https://valheim-modding.github.io/Jotunn/data/objects/item-list.html)), and QUANTITY is an integer.
* Allowed Items: Items allowed to teleport through the portal in the format: "ITEM1,ITEM2,ITEM3,..." where ITEM is the item ID.
* Allow Everything: Allow all items through the portal.
* Use All Previous: For the Obsidian portal also include the Allowed Items from the Ancient portal. For the Black Marble portal also include the Allowed Items from both the Ancient and Obsidian portals.
Each portal has its own config section named after it: `[Ancient Portal]`, `[Obsidian Portal]`,
`[Black Marble Portal]`. Every setting is server-synced and only editable by an admin.

* **Enabled:** Allow building this portal. Existing portals of this type are not removed.
* **Building Cost:** What it costs to build, as `ITEM,QUANTITY,REFUNDABLE` entries separated by `|`.
ITEM is the item ID ([found here](https://valheim-modding.github.io/Jotunn/data/objects/item-list.html)),
QUANTITY is a whole number, and REFUNDABLE is `true` or `false` for whether you get it back on removal.
For example: `ElderBark,20,true|Iron,5,true|SurtlingCore,2,true`
* **Requires Workbench:** Whether a crafting station is needed to build this portal at all.
* **Workbench:** Which crafting station is required, e.g. `piece_workbench`, `forge`, `blackforge`,
`piece_artisanstation`. Ignored when Requires Workbench is off.
* **Piece Category:** Which tab of the hammer build menu the portal appears in.
* **Allowed Items:** Items allowed to teleport through the portal, as `ITEM1, ITEM2, ITEM3, ...`
* **Allow Everything:** Allow all items through the portal. Overrides Allowed Items.
* **Use All Previous:** Also allow everything the portals listed above this one allow. The Obsidian
portal inherits from the Ancient portal; the Black Marble portal inherits from both.

In the in-game Configuration Manager each portal is a single collapsible row under **Building Pieces**,
with a table editor for the build cost. The teleport settings appear under the portal's own section.

## Installation:

Expand Down
2 changes: 1 addition & 1 deletion AdvancedPortals/Package/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "AdvancedPortals",
"version_number": "1.1.3",
"version_number": "1.2.0",
"website_url": "https://discord.gg/ZNhYeavv3C",
"description": "Add new, lore-friendly and balanced portals to allow some items to be teleported.",
"dependencies": [
Expand Down
4 changes: 2 additions & 2 deletions AdvancedPortals/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.3")]
[assembly: AssemblyFileVersion("1.1.3")]
[assembly: AssemblyVersion("1.2.0")]
[assembly: AssemblyFileVersion("1.2.0")]
6 changes: 1 addition & 5 deletions AdvancedPortals/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,9 @@ version_const: Version
deploy:
enabled: true

# The Thunderstore/Hexium team these publish under.
# The Thunderstore/Hexium team these publish under.
author_name: RandyKnapp

# Copy the build into this machine's mod manager profiles after every build
deploy:
enabled: true

publish:
thunderstore:
enabled: true
Expand Down
Loading
Loading