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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.1.102

- Require server approval from the Endgame beta Steam ID list before entering MWL_MapRoom1. Leaving remains unrestricted.
- Add a server-synced Endgame beta enable setting that can restore normal Map Room entry without a restart.
- Reserve rights to original PraetorisClient and Valheim Endgame code and assets while preserving third-party license notices.
- Include Valheim Endgame 0.5.0 and its tablet configuration in this package.
- Allow tablets to be enchanted with the released Epic Loot 0.14.13, prevent new tablet sockets and shardstones, and restore the enchanting result display.
- Add Star Level System and YamlDotNet as package dependencies for Endgame.

## 0.1.101

- Allow everyone to gather loose flint, branches, and stones inside Protective Wards. Other pickables and ward protections are unchanged.
Expand Down
26 changes: 8 additions & 18 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
MIT License
PraetorisClient

Copyright (c) 2025 warp
Copyright (c) 2025-2026 warpalicious. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Except with prior written permission from the copyright holder, no permission
is granted to copy, modify, distribute, sublicense, or sell PraetorisClient's
original source code or assets.

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Third-party components retain their respective licenses. Their notices are
preserved in THIRD_PARTY_NOTICES.md. This notice does not change the rights
granted under third-party licenses.
4 changes: 2 additions & 2 deletions 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("0.1.100")]
[assembly: AssemblyFileVersion("0.1.100")]
[assembly: AssemblyVersion("0.1.102")]
[assembly: AssemblyFileVersion("0.1.102")]
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

PraetorisClient is the shared client-and-server mod for Praetoris-specific gameplay, creative-zone, administration, and network measurement features.

This package also includes Valheim Endgame 0.5.0, which adds tablet encounters and map rooms. Tablets can be enchanted with the released Epic Loot 0.14.13. New tablets cannot receive sockets or shardstones.

Remove a separately installed ValheimEndgame test DLL before installing this combined package.

PraetorisClient's and Valheim Endgame's original code and assets are all rights reserved. See `LICENSE` and `LICENSE.ValheimEndgame`, respectively. Third-party licenses apply only to their respective components; their notices remain in `THIRD_PARTY_NOTICES.md` and `licenses/`.

## Requirements and integrations

- [BepInEx](https://github.com/BepInEx/BepInEx) is required.
- [Jotunn](https://github.com/Valheim-Modding/Jotunn) is required.
- Epic Loot is required by the current build. PraetorisClient starts its Epic Loot extensions during plugin startup.
- Star Level System and YamlDotNet are required by the bundled Valheim Endgame mod.
- ValheimRcon is optional. When present on the server, it receives the Server Chest commands.
- Shudnal ExtraSlots is optional. When present on a client, its items are included in creative inventory reports.
- VBNetTweaks is optional. When present, its `ZDOQueueLimit` supplies the socket metric queue budget.
Expand Down
12 changes: 11 additions & 1 deletion Source/Core/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
namespace PraetorisClient
{
[BepInPlugin(ModGUID, ModName, ModVersion)]
[BepInDependency(EndgameCompatibility.EndgameGuid, BepInDependency.DependencyFlags.SoftDependency)]
[BepInDependency(Jotunn.Main.ModGuid)]
[BepInDependency(EpicLootApiBridge.PluginGuid, BepInDependency.DependencyFlags.SoftDependency)]
[BepInDependency("org.tristan.rcon", BepInDependency.DependencyFlags.SoftDependency)]
Expand All @@ -32,7 +33,7 @@ namespace PraetorisClient
public class PraetorisClientPlugin : BaseUnityPlugin
{
private const string ModName = "PraetorisClient";
private const string ModVersion = "0.1.101";
private const string ModVersion = "0.1.102";
private const string Author = "warpalicious";
private const string ModGUID = Author + "." + ModName;
private const string EpicLootGuid = "randyknapp.mods.epicloot";
Expand Down Expand Up @@ -103,6 +104,8 @@ public class PraetorisClientPlugin : BaseUnityPlugin
internal static ConfigEntry<float> CreatureOwnerWardUpdateIntervalSeconds = null!;
internal static ConfigEntry<bool> DebugCreatureOwnerWard = null!;
internal static ConfigEntry<string> NetworkWardAllowedSteamIds = null!;
internal static ConfigEntry<string> EndgameBetaAllowedSteamIds = null!;
internal static ConfigEntry<bool> EndgameBetaEnabled = null!;
internal static ConfigEntry<bool> DebugServerChest = null!;
internal static ConfigEntry<string> FreeDeparturePortGuid = null!;
internal static ConfigEntry<bool> BlockPeerServerSyncConfigSync = null!;
Expand Down Expand Up @@ -140,6 +143,7 @@ public void Awake()

Instance = this;
BindConfig();
EndgameBetaAccess.Initialize();
PrefabManager.OnPrefabsRegistered += BossSpawnMessageSuppression.Refresh;
SynchronizationManager.OnConfigurationSynchronized += OnConfigurationSynchronized;
PraetorisMagicEffects.Register();
Expand All @@ -166,6 +170,7 @@ public void Awake()
FrameTimeMonitor.Initialize();
RpcTraceTelemetry.Initialize();
ApplyHarmonyPatches(epicLootLoaded);
EndgameCompatibility.Initialize();
CraftyBoxesWardGuard.TryApply(_harmony);
PublicBasicPickables.TryApply(_harmony);
ShippingPortDepartures.Initialize(_harmony);
Expand Down Expand Up @@ -226,6 +231,7 @@ private void Update()

private void OnDestroy()
{
EndgameCompatibility.Shutdown();
GuardStoneFeature.GuardStoneMapPins.Clear();
GuardStoneFeature.SpawnIslandWards.ClearSurvey();
PrefabManager.OnPrefabsRegistered -= BossSpawnMessageSuppression.Refresh;
Expand Down Expand Up @@ -294,6 +300,10 @@ private void BindConfig()
SyncedDescription("Prevents trophy placement on the original spawn temple boss stones. Disable to allow normal placement. Changes apply without a restart. Reset stones and buildable boss stones remain usable."));
NetworkWardAllowedSteamIds = Config.Bind("NetworkWard", "AllowedSteamIds", "",
"Server-only whitelist for Network Ward access. Comma-separated SteamID64 values (Steam_ prefix also accepted). Empty denies everyone, including admins. Client settings cannot grant access. Requires authenticated Steam connections.");
EndgameBetaEnabled = Config.Bind("EndgameBeta", "Enabled", true,
SyncedDescription("Restricts entry to MWL_MapRoom1 to the server beta list. Disable to restore normal gateway entry. Changes apply to connected clients without a restart."));
EndgameBetaAllowedSteamIds = Config.Bind("EndgameBeta", "AllowedSteamIds", "",
"Server-only list for entry to MWL_MapRoom1. Comma-separated SteamID64 values (Steam_ prefix also accepted). Empty denies everyone, including admins. Changes apply on the next entry. Client settings cannot grant access. Requires authenticated Steam connections.");
LinkApiUrl = Config.Bind("BotApi", "LinkApiUrl", "", "Compatible bot Valheim link endpoint. Prefer the PRAETORISCLIENT_LINK_API_URL environment variable on dedicated servers.");
BotApiKey = Config.Bind("BotApi", "ApiKey", "", "API key sent to the bot in the X-API-Key header. Prefer the PRAETORISCLIENT_BOT_API_KEY environment variable on dedicated servers.");
LinkCommand = Config.Bind("Linking", "LinkCommand", "!link", "In-game chat command consumed before it is sent as chat.");
Expand Down
195 changes: 195 additions & 0 deletions Source/Endgame/EndgameBetaAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
using System;
using System.Collections;
using System.IO;
using BepInEx.Configuration;
using HarmonyLib;
using Jotunn.Entities;
using Jotunn.Managers;
using PraetorisClient.NetworkWardFeature;
using UnityEngine;

namespace PraetorisClient
{
internal sealed class EndgameBetaAccess : MonoBehaviour
{
private static CustomRPC _rpc = null!;
private static EndgameBetaAccess? _instance;
private ZNet? _session;
private ZRpc? _server;
private Teleport? _gateway;
private Teleport? _approvedGateway;
private int _request;
private bool _pending;
private float _deadline;
private float _retryAt;
private float _refreshAt;
private static DateTime _configWriteUtc;

internal static void Initialize()
{
_rpc = NetworkManager.Instance.AddRPC("EndgameBetaAccess", OnRequest, OnResponse);
_instance = PraetorisClientPlugin.Instance!.gameObject.AddComponent<EndgameBetaAccess>();
}

private static bool IsEntrance(Teleport gateway)
{
Transform? exterior = gateway.transform.parent;
while (exterior != null && exterior.name != "Exterior") exterior = exterior.parent;
if (exterior == null || exterior.parent == null) return false;
Transform root = exterior.parent;
if (root.name != "MWL_MapRoom1" && root.name != "MWL_MapRoom1(Clone)") return false;
return root.Find("Exterior/Blueprint/Gateway") == gateway.transform &&
root.Find("Interior/ExteriorGateway") == gateway.m_targetPoint?.transform;
}

private bool CurrentConnection() => _session != null && _session == ZNet.instance &&
!_session.IsServer() && _server != null && ReferenceEquals(_server, _session.GetServerRPC()) &&
_server.IsConnected() && ZRoutedRpc.instance != null &&
Player.m_localPlayer != null && !Player.m_localPlayer.IsDead();

private void Request(Teleport gateway)
{
if (_pending || Time.unscaledTime < _retryAt) return;
_session = ZNet.instance;
_server = _session?.GetServerRPC();
if (!CurrentConnection())
{
Notice("Endgame beta access requires approval from a connected Steam server.");
_retryAt = Time.unscaledTime + 1;
return;
}
_gateway = gateway;
_request = _request == int.MaxValue ? 1 : _request + 1;
_pending = true;
_deadline = Time.unscaledTime + 5;
ZPackage package = new ZPackage();
package.Write(_request);
_rpc.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), package);
}

private void Update()
{
if (ZNet.instance != null && ZNet.instance.IsServer())
{
if (Time.unscaledTime >= _refreshAt)
{
_refreshAt = Time.unscaledTime + 1;
RefreshServerConfig();
}
return;
}
if (!PraetorisClientPlugin.EndgameBetaEnabled.Value)
{
_pending = false;
_gateway = null;
return;
}
if (!_pending) return;
if (!CurrentConnection()) { _pending = false; _gateway = null; return; }
if (Time.unscaledTime < _deadline) return;
_pending = false;
_gateway = null;
_retryAt = Time.unscaledTime + 1;
Notice("Unable to verify Endgame beta access. Please try again.");
}

private static IEnumerator OnRequest(long sender, ZPackage package)
{
if (ZNet.instance == null || !ZNet.instance.IsServer() || package.Size() != sizeof(int)) yield break;
ZNetPeer peer = ZNet.instance.GetPeer(sender);
if (peer == null || !peer.IsReady()) yield break;
int request = package.ReadInt();
ZSteamSocket? steam = NetworkWardAccess.FindSteamSocket(peer.m_rpc.GetSocket());
bool allowed = RefreshServerConfig() && (!PraetorisClientPlugin.EndgameBetaEnabled.Value ||
steam != null && NetworkWardAccessList.Contains(PraetorisClientPlugin.EndgameBetaAllowedSteamIds.Value, steam.GetHostName()));
PraetorisClientPlugin.Log.LogInfo("Endgame beta entry " + (allowed ? "approved." : "denied."));
ZPackage response = new ZPackage();
response.Write(request);
response.Write(allowed);
_rpc.SendPackage(sender, response);
yield break;
}

private static bool RefreshServerConfig()
{
try
{
ConfigFile config = PraetorisClientPlugin.Instance!.Config;
if (!File.Exists(config.ConfigFilePath)) return false;
DateTime written = File.GetLastWriteTimeUtc(config.ConfigFilePath);
if (written == _configWriteUtc) return true;
ConfigFile snapshot = new ConfigFile(config.ConfigFilePath, false) { SaveOnConfigSet = false };
bool enabled = snapshot.Bind("EndgameBeta", "Enabled", true).Value;
string allowed = snapshot.Bind("EndgameBeta", "AllowedSteamIds", "").Value;
config.Reload();
bool repairEnabled = PraetorisClientPlugin.EndgameBetaEnabled.Value != enabled;
PraetorisClientPlugin.EndgameBetaEnabled.Value = enabled;
PraetorisClientPlugin.EndgameBetaAllowedSteamIds.Value = allowed;
if (repairEnabled) config.Reload();
_configWriteUtc = File.GetLastWriteTimeUtc(config.ConfigFilePath);
return true;
}
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
{
PraetorisClientPlugin.Log.LogWarning("Endgame beta access denied: cannot read server configuration. " + exception.Message);
return false;
}
}

private static IEnumerator OnResponse(long sender, ZPackage package)
{
EndgameBetaAccess? access = _instance;
if (access == null || !access._pending || !access.CurrentConnection() ||
sender != ZRoutedRpc.instance.GetServerPeerID() ||
package.Size() != sizeof(int) + sizeof(bool) || Time.unscaledTime >= access._deadline) yield break;
int request = package.ReadInt();
bool allowed = package.ReadBool();
if (request != access._request) yield break;
access._pending = false;
access._retryAt = Time.unscaledTime + 1;
Teleport? gateway = access._gateway;
access._gateway = null;
if (!allowed)
{
Notice("Valheim Endgame is currently in closed beta. You do not have access.");
yield break;
}
Player player = Player.m_localPlayer;
if (gateway == null || !gateway.isActiveAndEnabled || !IsEntrance(gateway) || player.IsTeleporting() ||
Vector3.Distance(player.transform.position, gateway.transform.position) > 5) yield break;
access._approvedGateway = gateway;
try { gateway.Interact(player, false, false); }
finally { access._approvedGateway = null; }
yield break;
}

private static void Notice(string message)
{
Player.m_localPlayer?.Message(MessageHud.MessageType.Center, message);
PraetorisClientPlugin.Log.LogInfo(message);
}

private void OnDestroy()
{
_pending = false;
_approvedGateway = _gateway = null;
if (_instance == this) _instance = null;
}

[HarmonyPatch(typeof(Teleport), nameof(Teleport.Interact))]
private static class GateEntry
{
private static bool Prefix(Teleport __instance, Humanoid character, bool hold, ref bool __result)
{
if (!PraetorisClientPlugin.EndgameBetaEnabled.Value || !IsEntrance(__instance)) return true;
if (_instance != null && _instance._approvedGateway == __instance) return true;
__result = false;
if (hold || character != Player.m_localPlayer || character is not Player player ||
player.IsDead() || player.IsTeleporting() ||
Vector3.Distance(player.transform.position, __instance.transform.position) > 5) return false;
_instance!.Request(__instance);
return false;
}
}
}
}
Loading
Loading